From 89bc3bdd5bb1fb0668f76981b05f8712de33b89d Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Wed, 15 Apr 2020 00:16:11 +0100
Subject: [PATCH 1/3] consolidate and extract copyPlaintext, copyNode and
selectText
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
.../keybackup/CreateKeyBackupDialog.js | 14 +---
.../CreateSecretStorageDialog.js | 14 +---
src/components/views/dialogs/ShareDialog.js | 76 ++++++++-----------
src/components/views/messages/TextualBody.js | 26 ++-----
src/utils/strings.ts | 75 ++++++++++++++++++
5 files changed, 117 insertions(+), 88 deletions(-)
create mode 100644 src/utils/strings.ts
diff --git a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js
index 3a480a2579..7e5e0afb79 100644
--- a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js
+++ b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js
@@ -25,6 +25,7 @@ import { _t } from '../../../../languageHandler';
import { accessSecretStorage } from '../../../../CrossSigningManager';
import SettingsStore from '../../../../settings/SettingsStore';
import AccessibleButton from "../../../../components/views/elements/AccessibleButton";
+import {copyNode} from "../../../../utils/strings";
const PHASE_PASSPHRASE = 0;
const PHASE_PASSPHRASE_CONFIRM = 1;
@@ -37,16 +38,6 @@ const PHASE_OPTOUT_CONFIRM = 6;
const PASSWORD_MIN_SCORE = 4; // So secure, many characters, much complex, wow, etc, etc.
const PASSPHRASE_FEEDBACK_DELAY = 500; // How long after keystroke to offer passphrase feedback, ms.
-// XXX: copied from ShareDialog: factor out into utils
-function selectText(target) {
- const range = document.createRange();
- range.selectNodeContents(target);
-
- const selection = window.getSelection();
- selection.removeAllRanges();
- selection.addRange(range);
-}
-
/*
* Walks the user through the process of creating an e2e key backup
* on the server.
@@ -101,8 +92,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
}
_onCopyClick = () => {
- selectText(this._recoveryKeyNode);
- const successful = document.execCommand('copy');
+ const successful = copyNode(this._recoveryKeyNode);
if (successful) {
this.setState({
copied: true,
diff --git a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js
index d63db617d5..cfad49c38d 100644
--- a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js
+++ b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js
@@ -24,6 +24,7 @@ import FileSaver from 'file-saver';
import { _t } from '../../../../languageHandler';
import Modal from '../../../../Modal';
import { promptForBackupPassphrase } from '../../../../CrossSigningManager';
+import {copyNode} from "../../../../utils/strings";
const PHASE_LOADING = 0;
const PHASE_MIGRATE = 1;
@@ -38,16 +39,6 @@ const PHASE_CONFIRM_SKIP = 8;
const PASSWORD_MIN_SCORE = 4; // So secure, many characters, much complex, wow, etc, etc.
const PASSPHRASE_FEEDBACK_DELAY = 500; // How long after keystroke to offer passphrase feedback, ms.
-// XXX: copied from ShareDialog: factor out into utils
-function selectText(target) {
- const range = document.createRange();
- range.selectNodeContents(target);
-
- const selection = window.getSelection();
- selection.removeAllRanges();
- selection.addRange(range);
-}
-
/*
* Walks the user through the process of creating a passphrase to guard Secure
* Secret Storage in account data.
@@ -169,8 +160,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
}
_onCopyClick = () => {
- selectText(this._recoveryKeyNode);
- const successful = document.execCommand('copy');
+ const successful = copyNode(this._recoveryKeyNode);
if (successful) {
this.setState({
copied: true,
diff --git a/src/components/views/dialogs/ShareDialog.js b/src/components/views/dialogs/ShareDialog.js
index 1bc9decd39..ebd1f8d1eb 100644
--- a/src/components/views/dialogs/ShareDialog.js
+++ b/src/components/views/dialogs/ShareDialog.js
@@ -23,6 +23,7 @@ import QRCode from 'qrcode-react';
import {RoomPermalinkCreator, makeGroupPermalink, makeUserPermalink} from "../../../utils/permalinks/Permalinks";
import * as ContextMenu from "../../structures/ContextMenu";
import {toRightOf} from "../../structures/ContextMenu";
+import {copyPlaintext, selectText} from "../../../utils/strings";
const socials = [
{
@@ -81,45 +82,26 @@ export default class ShareDialog extends React.Component {
linkSpecificEvent: this.props.target instanceof MatrixEvent,
permalinkCreator,
};
-
- this._link = createRef();
- }
-
- static _selectText(target) {
- const range = document.createRange();
- range.selectNodeContents(target);
-
- const selection = window.getSelection();
- selection.removeAllRanges();
- selection.addRange(range);
}
static onLinkClick(e) {
e.preventDefault();
- const {target} = e;
- ShareDialog._selectText(target);
+ selectText(e.target);
}
- onCopyClick(e) {
+ async onCopyClick(e) {
e.preventDefault();
+ const target = e.target; // copy target before we go async and React throws it away
- ShareDialog._selectText(this._link.current);
-
- let successful;
- try {
- successful = document.execCommand('copy');
- } catch (err) {
- console.error('Failed to copy: ', err);
- }
-
- const buttonRect = e.target.getBoundingClientRect();
+ const successful = await copyPlaintext(this.getUrl());
+ const buttonRect = target.getBoundingClientRect();
const GenericTextContextMenu = sdk.getComponent('context_menus.GenericTextContextMenu');
const {close} = ContextMenu.createMenu(GenericTextContextMenu, {
...toRightOf(buttonRect, 2),
message: successful ? _t('Copied!') : _t('Failed to copy'),
});
// Drop a reference to this close handler for componentWillUnmount
- this.closeCopiedTooltip = e.target.onmouseleave = close;
+ this.closeCopiedTooltip = target.onmouseleave = close;
}
onLinkSpecificEventCheckboxClick() {
@@ -134,10 +116,32 @@ export default class ShareDialog extends React.Component {
if (this.closeCopiedTooltip) this.closeCopiedTooltip();
}
- render() {
- let title;
+ getUrl() {
let matrixToUrl;
+ if (this.props.target instanceof Room) {
+ if (this.state.linkSpecificEvent) {
+ const events = this.props.target.getLiveTimeline().getEvents();
+ matrixToUrl = this.state.permalinkCreator.forEvent(events[events.length - 1].getId());
+ } else {
+ matrixToUrl = this.state.permalinkCreator.forRoom();
+ }
+ } else if (this.props.target instanceof User || this.props.target instanceof RoomMember) {
+ matrixToUrl = makeUserPermalink(this.props.target.userId);
+ } else if (this.props.target instanceof Group) {
+ matrixToUrl = makeGroupPermalink(this.props.target.groupId);
+ } else if (this.props.target instanceof MatrixEvent) {
+ if (this.state.linkSpecificEvent) {
+ matrixToUrl = this.props.permalinkCreator.forEvent(this.props.target.getId());
+ } else {
+ matrixToUrl = this.props.permalinkCreator.forRoom();
+ }
+ }
+ return matrixToUrl;
+ }
+
+ render() {
+ let title;
let checkbox;
if (this.props.target instanceof Room) {
@@ -155,18 +159,10 @@ export default class ShareDialog extends React.Component {
;
}
-
- if (this.state.linkSpecificEvent) {
- matrixToUrl = this.state.permalinkCreator.forEvent(events[events.length - 1].getId());
- } else {
- matrixToUrl = this.state.permalinkCreator.forRoom();
- }
} else if (this.props.target instanceof User || this.props.target instanceof RoomMember) {
title = _t('Share User');
- matrixToUrl = makeUserPermalink(this.props.target.userId);
} else if (this.props.target instanceof Group) {
title = _t('Share Community');
- matrixToUrl = makeGroupPermalink(this.props.target.groupId);
} else if (this.props.target instanceof MatrixEvent) {
title = _t('Share Room Message');
checkbox =
@@ -178,14 +174,9 @@ export default class ShareDialog extends React.Component {
{ _t('Link to selected message') }
;
-
- if (this.state.linkSpecificEvent) {
- matrixToUrl = this.props.permalinkCreator.forEvent(this.props.target.getId());
- } else {
- matrixToUrl = this.props.permalinkCreator.forRoom();
- }
}
+ const matrixToUrl = this.getUrl();
const encodedUrl = encodeURIComponent(matrixToUrl);
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
@@ -196,8 +187,7 @@ export default class ShareDialog extends React.Component {
>
-
diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js
index 27514d0e23..882e331675 100644
--- a/src/components/views/messages/TextualBody.js
+++ b/src/components/views/messages/TextualBody.js
@@ -34,6 +34,7 @@ import {pillifyLinks, unmountPills} from '../../../utils/pillify';
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
import {isPermalinkHost} from "../../../utils/permalinks/Permalinks";
import {toRightOf} from "../../structures/ContextMenu";
+import {copyPlaintext} from "../../../utils/strings";
export default createReactClass({
displayName: 'TextualBody',
@@ -69,23 +70,6 @@ export default createReactClass({
};
},
- copyToClipboard: function(text) {
- const textArea = document.createElement("textarea");
- textArea.value = text;
- document.body.appendChild(textArea);
- textArea.select();
-
- let successful = false;
- try {
- successful = document.execCommand('copy');
- } catch (err) {
- console.log('Unable to copy');
- }
-
- document.body.removeChild(textArea);
- return successful;
- },
-
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
UNSAFE_componentWillMount: function() {
this._content = createRef();
@@ -277,17 +261,17 @@ export default createReactClass({
Array.from(ReactDOM.findDOMNode(this).querySelectorAll('.mx_EventTile_body pre')).forEach((p) => {
const button = document.createElement("span");
button.className = "mx_EventTile_copyButton";
- button.onclick = (e) => {
+ button.onclick = async () => {
const copyCode = button.parentNode.getElementsByTagName("pre")[0];
- const successful = this.copyToClipboard(copyCode.textContent);
+ const successful = await copyPlaintext(copyCode.textContent);
- const buttonRect = e.target.getBoundingClientRect();
+ const buttonRect = button.getBoundingClientRect();
const GenericTextContextMenu = sdk.getComponent('context_menus.GenericTextContextMenu');
const {close} = ContextMenu.createMenu(GenericTextContextMenu, {
...toRightOf(buttonRect, 2),
message: successful ? _t('Copied!') : _t('Failed to copy'),
});
- e.target.onmouseleave = close;
+ button.onmouseleave = close;
};
// Wrap a div around so that the copy button can be correctly positioned
diff --git a/src/utils/strings.ts b/src/utils/strings.ts
new file mode 100644
index 0000000000..7d1fa0049d
--- /dev/null
+++ b/src/utils/strings.ts
@@ -0,0 +1,75 @@
+/*
+Copyright 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+/**
+ * Copy plaintext to user's clipboard
+ * It will overwrite user's selection range
+ * In certain browsers it may only work if triggered by a user action or may ask user for permissions
+ * Tries to use new async clipboard API if available
+ * @param text the plaintext to put in the user's clipboard
+ */
+export async function copyPlaintext(text: string): Promise {
+ try {
+ if (navigator && navigator.clipboard && navigator.clipboard.writeText) {
+ await navigator.clipboard.writeText(text);
+ return true;
+ } else {
+ const textArea = document.createElement("textarea");
+ textArea.value = text;
+
+ // Avoid scrolling to bottom
+ textArea.style.top = "0";
+ textArea.style.left = "0";
+ textArea.style.position = "fixed";
+
+ document.body.appendChild(textArea);
+ const selection = document.getSelection();
+ const range = document.createRange();
+ // range.selectNodeContents(textArea);
+ range.selectNode(textArea);
+ selection.removeAllRanges();
+ selection.addRange(range);
+
+ const successful = document.execCommand("copy");
+ selection.removeAllRanges();
+ document.body.removeChild(textArea);
+ return successful;
+ }
+ } catch (e) {
+ console.error(e);
+ }
+ return false;
+}
+
+export function selectText(target: Element) {
+ const range = document.createRange();
+ range.selectNodeContents(target);
+
+ const selection = window.getSelection();
+ selection.removeAllRanges();
+ selection.addRange(range);
+}
+
+/**
+ * Copy rich text to user's clipboard
+ * It will overwrite user's selection range
+ * In certain browsers it may only work if triggered by a user action or may ask user for permissions
+ * @param ref pointer to the node to copy
+ */
+export function copyNode(ref: Element): boolean {
+ selectText(ref);
+ return document.execCommand('copy');
+}
From 276b5b874c1a8e7c4fa826c7fba0507fcc9babbe Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Wed, 15 Apr 2020 00:22:19 +0100
Subject: [PATCH 2/3] Convert ShareDialog to Typescript
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
.../{ShareDialog.js => ShareDialog.tsx} | 45 +++++++++++++------
1 file changed, 32 insertions(+), 13 deletions(-)
rename src/components/views/dialogs/{ShareDialog.js => ShareDialog.tsx} (86%)
diff --git a/src/components/views/dialogs/ShareDialog.js b/src/components/views/dialogs/ShareDialog.tsx
similarity index 86%
rename from src/components/views/dialogs/ShareDialog.js
rename to src/components/views/dialogs/ShareDialog.tsx
index ebd1f8d1eb..375cb65b5f 100644
--- a/src/components/views/dialogs/ShareDialog.js
+++ b/src/components/views/dialogs/ShareDialog.tsx
@@ -1,5 +1,6 @@
/*
Copyright 2018 New Vector Ltd
+Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -14,9 +15,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React, {createRef} from 'react';
-import PropTypes from 'prop-types';
-import {Room, User, Group, RoomMember, MatrixEvent} from 'matrix-js-sdk';
+import * as React from 'react';
+import * as PropTypes from 'prop-types';
+import {Room} from "matrix-js-sdk/src/models/room";
+import {User} from "matrix-js-sdk/src/models/user";
+import {Group} from "matrix-js-sdk/src/models/group";
+import {RoomMember} from "matrix-js-sdk/src/models/room-member";
+import {MatrixEvent} from "matrix-js-sdk/src/models/event";
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import QRCode from 'qrcode-react';
@@ -53,7 +58,18 @@ const socials = [
},
];
-export default class ShareDialog extends React.Component {
+interface IProps {
+ onFinished: () => void;
+ target: Room | User | Group | RoomMember | MatrixEvent;
+ permalinkCreator: RoomPermalinkCreator;
+}
+
+interface IState {
+ linkSpecificEvent: boolean;
+ permalinkCreator: RoomPermalinkCreator;
+}
+
+export default class ShareDialog extends React.PureComponent {
static propTypes = {
onFinished: PropTypes.func.isRequired,
target: PropTypes.oneOfType([
@@ -65,6 +81,8 @@ export default class ShareDialog extends React.Component {
]).isRequired,
};
+ protected closeCopiedTooltip: () => void;
+
constructor(props) {
super(props);
@@ -206,17 +224,18 @@ export default class ShareDialog extends React.Component {
- {
- socials.map((social) =>
(
+
- )
- }
+
+ )) }
From a37ecbbb34ef83ef6f38bae2895a95c325401cfe Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Wed, 15 Apr 2020 19:24:33 +0100
Subject: [PATCH 3/3] update console.error
---
src/utils/strings.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/utils/strings.ts b/src/utils/strings.ts
index 7d1fa0049d..5856682445 100644
--- a/src/utils/strings.ts
+++ b/src/utils/strings.ts
@@ -49,7 +49,7 @@ export async function copyPlaintext(text: string): Promise {
return successful;
}
} catch (e) {
- console.error(e);
+ console.error("copyPlaintext failed", e);
}
return false;
}