From 087a4ad7cec1d33e5426a91ac934037fe35de259 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Thu, 10 Nov 2022 20:52:55 -0600 Subject: [PATCH 1/6] Add copy permalink action --- .../room/timeline/tiles/BaseMessageTile.js | 6 ++ src/matrix/room/PowerLevels.js | 3 +- src/platform/web/dom/ImageHandle.js | 2 +- src/platform/web/dom/utils.js | 35 -------- src/platform/web/dom/utils.ts | 79 +++++++++++++++++++ .../session/room/timeline/BaseMessageView.js | 2 + .../web/ui/session/room/timeline/VideoView.js | 2 +- 7 files changed, 91 insertions(+), 38 deletions(-) delete mode 100644 src/platform/web/dom/utils.js create mode 100644 src/platform/web/dom/utils.ts diff --git a/src/domain/session/room/timeline/tiles/BaseMessageTile.js b/src/domain/session/room/timeline/tiles/BaseMessageTile.js index cfa27a94..2cc3e572 100644 --- a/src/domain/session/room/timeline/tiles/BaseMessageTile.js +++ b/src/domain/session/room/timeline/tiles/BaseMessageTile.js @@ -17,6 +17,8 @@ limitations under the License. import {SimpleTile} from "./SimpleTile.js"; import {ReactionsViewModel} from "../ReactionsViewModel.js"; import {getIdentifierColorNumber, avatarInitials, getAvatarHttpUrl} from "../../../../avatar"; +import {copyPlaintext} from "../../../../../platform/web/dom/utils"; + export class BaseMessageTile extends SimpleTile { constructor(entry, options) { @@ -45,6 +47,10 @@ export class BaseMessageTile extends SimpleTile { return `https://matrix.to/#/${encodeURIComponent(this._room.id)}/${encodeURIComponent(this._entry.id)}`; } + copyPermalink() { + copyPlaintext(this.permaLink); + } + get senderProfileLink() { return `https://matrix.to/#/${encodeURIComponent(this.sender)}`; } diff --git a/src/matrix/room/PowerLevels.js b/src/matrix/room/PowerLevels.js index 76e062ef..63a5b0b0 100644 --- a/src/matrix/room/PowerLevels.js +++ b/src/matrix/room/PowerLevels.js @@ -66,10 +66,11 @@ export class PowerLevels { /** @param {string} action either "invite", "kick", "ban" or "redact". */ _getActionLevel(action) { - const level = this._plEvent?.content[action]; + const level = this._plEvent?.content?.[action]; if (typeof level === "number") { return level; } else { + // TODO: Why does this default to 50? return 50; } } diff --git a/src/platform/web/dom/ImageHandle.js b/src/platform/web/dom/ImageHandle.js index 4ac3a6cd..e41486f9 100644 --- a/src/platform/web/dom/ImageHandle.js +++ b/src/platform/web/dom/ImageHandle.js @@ -15,7 +15,7 @@ limitations under the License. */ import {BlobHandle} from "./BlobHandle.js"; -import {domEventAsPromise} from "./utils.js"; +import {domEventAsPromise} from "./utils"; export class ImageHandle { static async fromBlob(blob) { diff --git a/src/platform/web/dom/utils.js b/src/platform/web/dom/utils.js deleted file mode 100644 index 43a26640..00000000 --- a/src/platform/web/dom/utils.js +++ /dev/null @@ -1,35 +0,0 @@ -/* -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. -*/ - -export function domEventAsPromise(element, successEvent) { - return new Promise((resolve, reject) => { - let detach; - const handleError = evt => { - detach(); - reject(evt.target.error); - }; - const handleSuccess = () => { - detach(); - resolve(); - }; - detach = () => { - element.removeEventListener(successEvent, handleSuccess); - element.removeEventListener("error", handleError); - }; - element.addEventListener(successEvent, handleSuccess); - element.addEventListener("error", handleError); - }); -} diff --git a/src/platform/web/dom/utils.ts b/src/platform/web/dom/utils.ts new file mode 100644 index 00000000..4c17f1af --- /dev/null +++ b/src/platform/web/dom/utils.ts @@ -0,0 +1,79 @@ +/* +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. +*/ + +export function domEventAsPromise(element, successEvent): Promise { + return new Promise((resolve, reject) => { + let detach; + const handleError = evt => { + detach(); + reject(evt.target.error); + }; + const handleSuccess = () => { + detach(); + resolve(); + }; + detach = () => { + element.removeEventListener(successEvent, handleSuccess); + element.removeEventListener("error", handleError); + }; + element.addEventListener(successEvent, handleSuccess); + element.addEventListener("error", handleError); + }); +} + +// Copies the given text to clipboard and returns a boolean of whether the action was +// successful +export async function copyPlaintext(text: string): Promise { + try { + if (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(); + if (!selection) { + console.error('copyPlaintext: Unable to copy text to clipboard in fallback mode because `selection` was null/undefined'); + return false; + } + + 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); + if(!successful) { + console.error('copyPlaintext: Unable to copy text to clipboard in fallback mode because the `copy` command is unsupported or disabled'); + } + return successful; + } + } catch (err) { + console.error("copyPlaintext: Ran into an error", err); + } + return false; +} diff --git a/src/platform/web/ui/session/room/timeline/BaseMessageView.js b/src/platform/web/ui/session/room/timeline/BaseMessageView.js index ee0a37db..845c4a4b 100644 --- a/src/platform/web/ui/session/room/timeline/BaseMessageView.js +++ b/src/platform/web/ui/session/room/timeline/BaseMessageView.js @@ -120,6 +120,8 @@ export class BaseMessageView extends TemplateView { } else if (vm.canRedact) { options.push(Menu.option(vm.i18n`Delete`, () => vm.redact()).setDestructive()); } + + options.push(Menu.option(vm.i18n`Copy permalink`, () => vm.copyPermalink())); return options; } diff --git a/src/platform/web/ui/session/room/timeline/VideoView.js b/src/platform/web/ui/session/room/timeline/VideoView.js index 340cae6d..9b092ed0 100644 --- a/src/platform/web/ui/session/room/timeline/VideoView.js +++ b/src/platform/web/ui/session/room/timeline/VideoView.js @@ -15,7 +15,7 @@ limitations under the License. */ import {BaseMediaView} from "./BaseMediaView.js"; -import {domEventAsPromise} from "../../../../dom/utils.js"; +import {domEventAsPromise} from "../../../../dom/utils"; export class VideoView extends BaseMediaView { renderMedia(t) { From f0d53fe40fcfbce03fb2e47de5299225d6a5db24 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Thu, 10 Nov 2022 20:56:27 -0600 Subject: [PATCH 2/6] Remove newline --- src/platform/web/ui/session/room/timeline/BaseMessageView.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/platform/web/ui/session/room/timeline/BaseMessageView.js b/src/platform/web/ui/session/room/timeline/BaseMessageView.js index 845c4a4b..acd093e2 100644 --- a/src/platform/web/ui/session/room/timeline/BaseMessageView.js +++ b/src/platform/web/ui/session/room/timeline/BaseMessageView.js @@ -120,7 +120,6 @@ export class BaseMessageView extends TemplateView { } else if (vm.canRedact) { options.push(Menu.option(vm.i18n`Delete`, () => vm.redact()).setDestructive()); } - options.push(Menu.option(vm.i18n`Copy permalink`, () => vm.copyPermalink())); return options; } From 29aac096415bdf193138885ecff7d144e479d859 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Thu, 10 Nov 2022 21:11:38 -0600 Subject: [PATCH 3/6] Add types to function parameters --- src/platform/web/dom/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/platform/web/dom/utils.ts b/src/platform/web/dom/utils.ts index 4c17f1af..8013b491 100644 --- a/src/platform/web/dom/utils.ts +++ b/src/platform/web/dom/utils.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -export function domEventAsPromise(element, successEvent): Promise { +export function domEventAsPromise(element: HTMLElement, successEvent: string): Promise { return new Promise((resolve, reject) => { let detach; const handleError = evt => { From 35a08e3b051baaec5e2da6653a7ad9a349d22866 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Fri, 18 Nov 2022 12:13:47 -0600 Subject: [PATCH 4/6] Update language to "Copy matrix.to permalink" --- src/platform/web/ui/session/room/timeline/BaseMessageView.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/platform/web/ui/session/room/timeline/BaseMessageView.js b/src/platform/web/ui/session/room/timeline/BaseMessageView.js index acd093e2..d35e8c5a 100644 --- a/src/platform/web/ui/session/room/timeline/BaseMessageView.js +++ b/src/platform/web/ui/session/room/timeline/BaseMessageView.js @@ -120,7 +120,7 @@ export class BaseMessageView extends TemplateView { } else if (vm.canRedact) { options.push(Menu.option(vm.i18n`Delete`, () => vm.redact()).setDestructive()); } - options.push(Menu.option(vm.i18n`Copy permalink`, () => vm.copyPermalink())); + options.push(Menu.option(vm.i18n`Copy matrix.to permalink`, () => vm.copyPermalink())); return options; } From acba597e8bfefb49d066dfdca129cecbaf356b88 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Mon, 27 Mar 2023 15:31:46 -0500 Subject: [PATCH 5/6] Label magic number --- src/matrix/room/PowerLevels.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/matrix/room/PowerLevels.js b/src/matrix/room/PowerLevels.js index 63a5b0b0..aefbfaf1 100644 --- a/src/matrix/room/PowerLevels.js +++ b/src/matrix/room/PowerLevels.js @@ -16,6 +16,9 @@ limitations under the License. export const EVENT_TYPE = "m.room.power_levels"; +// See https://spec.matrix.org/latest/client-server-api/#mroompower_levels +const STATE_DEFAULT_POWER_LEVEL = 50; + export class PowerLevels { constructor({powerLevelEvent, createEvent, ownUserId, membership}) { this._plEvent = powerLevelEvent; @@ -70,8 +73,7 @@ export class PowerLevels { if (typeof level === "number") { return level; } else { - // TODO: Why does this default to 50? - return 50; + return STATE_DEFAULT_POWER_LEVEL; } } From 98d4dfd8e6aa77760a53ac11c5544f6f5af42482 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Mon, 27 Mar 2023 15:37:28 -0500 Subject: [PATCH 6/6] Move copy function to platform --- src/domain/session/room/timeline/tiles/BaseMessageTile.js | 3 +-- src/platform/web/Platform.js | 5 +++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/domain/session/room/timeline/tiles/BaseMessageTile.js b/src/domain/session/room/timeline/tiles/BaseMessageTile.js index 3412b3f9..90da4bff 100644 --- a/src/domain/session/room/timeline/tiles/BaseMessageTile.js +++ b/src/domain/session/room/timeline/tiles/BaseMessageTile.js @@ -17,7 +17,6 @@ limitations under the License. import {SimpleTile} from "./SimpleTile.js"; import {ReactionsViewModel} from "../ReactionsViewModel.js"; import {getIdentifierColorNumber, avatarInitials, getAvatarHttpUrl} from "../../../../avatar"; -import {copyPlaintext} from "../../../../../platform/web/dom/utils"; export class BaseMessageTile extends SimpleTile { @@ -47,7 +46,7 @@ export class BaseMessageTile extends SimpleTile { } copyPermalink() { - copyPlaintext(this.permaLink); + this.platform.copyPlaintext(this.permaLink); } get senderProfileLink() { diff --git a/src/platform/web/Platform.js b/src/platform/web/Platform.js index be8c9970..e2f722e6 100644 --- a/src/platform/web/Platform.js +++ b/src/platform/web/Platform.js @@ -43,6 +43,7 @@ import {MediaDevicesWrapper} from "./dom/MediaDevices"; import {DOMWebRTC} from "./dom/WebRTC"; import {ThemeLoader} from "./theming/ThemeLoader"; import {TimeFormatter} from "./dom/TimeFormatter"; +import {copyPlaintext} from "./dom/utils"; function addScript(src) { return new Promise(function (resolve, reject) { @@ -283,6 +284,10 @@ export class Platform { } } + async copyPlaintext(text) { + return await copyPlaintext(text); + } + restart() { document.location.reload(); }