From 53db386731422c88d4e24e299e9e56a150dad3cf Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 10 Aug 2020 22:06:30 -0600 Subject: [PATCH 001/221] Add support for blurhash (MSC2448) MSC: https://github.com/matrix-org/matrix-doc/pull/2448 While the image loads, we can show a blurred version of it (calculated at upload time) so we don't have a blank space in the timeline. --- package.json | 1 + src/ContentMessages.tsx | 10 +++- .../views/elements/BlurhashPlaceholder.tsx | 56 +++++++++++++++++++ src/components/views/messages/MImageBody.js | 18 +++--- yarn.lock | 5 ++ 5 files changed, 81 insertions(+), 9 deletions(-) create mode 100644 src/components/views/elements/BlurhashPlaceholder.tsx diff --git a/package.json b/package.json index 548b33f353..989672d414 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "@babel/runtime": "^7.10.5", "await-lock": "^2.0.1", "blueimp-canvas-to-blob": "^3.27.0", + "blurhash": "^1.1.3", "browser-encrypt-attachment": "^0.3.0", "browser-request": "^0.3.3", "classnames": "^2.2.6", diff --git a/src/ContentMessages.tsx b/src/ContentMessages.tsx index 6f55a75d0c..7e57b34ff7 100644 --- a/src/ContentMessages.tsx +++ b/src/ContentMessages.tsx @@ -334,6 +334,7 @@ function uploadFile(matrixClient: MatrixClient, roomId: string, file: File | Blo if (file.type) { encryptInfo.mimetype = file.type; } + // TODO: Blurhash for encrypted media? return {"file": encryptInfo}; }); (prom as IAbortablePromise).abort = () => { @@ -344,11 +345,15 @@ function uploadFile(matrixClient: MatrixClient, roomId: string, file: File | Blo } else { const basePromise = matrixClient.uploadContent(file, { progressHandler: progressHandler, + onlyContentUri: false, }); - const promise1 = basePromise.then(function(url) { + const promise1 = basePromise.then(function(body) { if (canceled) throw new UploadCanceledError(); // If the attachment isn't encrypted then include the URL directly. - return {"url": url}; + return { + "url": body.content_uri, + "blurhash": body["xyz.amorgan.blurhash"], // TODO: Use `body.blurhash` when MSC2448 lands + }; }); promise1.abort = () => { canceled = true; @@ -550,6 +555,7 @@ export default class ContentMessages { return upload.promise.then(function(result) { content.file = result.file; content.url = result.url; + content.info['xyz.amorgan.blurhash'] = result.blurhash; // TODO: Use `blurhash` when MSC2448 lands }); }).then(() => { // Await previous message being sent into the room diff --git a/src/components/views/elements/BlurhashPlaceholder.tsx b/src/components/views/elements/BlurhashPlaceholder.tsx new file mode 100644 index 0000000000..bed45bfe5a --- /dev/null +++ b/src/components/views/elements/BlurhashPlaceholder.tsx @@ -0,0 +1,56 @@ +/* + 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. + */ + +import React from 'react'; +import {decode} from "blurhash"; + +interface IProps { + blurhash: string; + width: number; + height: number; +} + +export default class BlurhashPlaceholder extends React.PureComponent { + private canvas: React.RefObject = React.createRef(); + + public componentDidMount() { + this.draw(); + } + + public componentDidUpdate() { + this.draw(); + } + + private draw() { + if (!this.canvas.current) return; + + try { + const {width, height} = this.props; + + const pixels = decode(this.props.blurhash, Math.ceil(width), Math.ceil(height)); + const ctx = this.canvas.current.getContext("2d"); + const imgData = ctx.createImageData(width, height); + imgData.data.set(pixels); + ctx.putImageData(imgData, 0, 0); + } catch (e) { + console.error("Error rendering blurhash: ", e); + } + } + + public render() { + return ; + } +} diff --git a/src/components/views/messages/MImageBody.js b/src/components/views/messages/MImageBody.js index c92ae475bf..e0ac24c641 100644 --- a/src/components/views/messages/MImageBody.js +++ b/src/components/views/messages/MImageBody.js @@ -27,6 +27,7 @@ import { _t } from '../../../languageHandler'; import SettingsStore from "../../../settings/SettingsStore"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import InlineSpinner from '../elements/InlineSpinner'; +import BlurhashPlaceholder from "../elements/BlurhashPlaceholder"; export default class MImageBody extends React.Component { static propTypes = { @@ -53,6 +54,8 @@ export default class MImageBody extends React.Component { this.onClick = this.onClick.bind(this); this._isGif = this._isGif.bind(this); + const imageInfo = this.props.mxEvent.getContent().info; + this.state = { decryptedUrl: null, decryptedThumbnailUrl: null, @@ -63,6 +66,7 @@ export default class MImageBody extends React.Component { loadedImageDimensions: null, hover: false, showImage: SettingsStore.getValue("showImages"), + blurhash: imageInfo ? imageInfo['xyz.amorgan.blurhash'] : null, // TODO: Use `blurhash` when MSC2448 lands. }; this._image = createRef(); @@ -329,7 +333,8 @@ export default class MImageBody extends React.Component { infoWidth = content.info.w; infoHeight = content.info.h; } else { - // Whilst the image loads, display nothing. + // Whilst the image loads, display nothing. We also don't display a blurhash image + // because we don't really know what size of image we'll end up with. // // Once loaded, use the loaded image dimensions stored in `loadedImageDimensions`. // @@ -368,8 +373,7 @@ export default class MImageBody extends React.Component { if (content.file !== undefined && this.state.decryptedUrl === null) { placeholder = ; } else if (!this.state.imgLoaded) { - // Deliberately, getSpinner is left unimplemented here, MStickerBody overides - placeholder = this.getPlaceholder(); + placeholder = this.getPlaceholder(maxWidth, maxHeight); } let showPlaceholder = Boolean(placeholder); @@ -391,7 +395,7 @@ export default class MImageBody extends React.Component { if (!this.state.showImage) { img = ; - showPlaceholder = false; // because we're hiding the image, so don't show the sticker icon. + showPlaceholder = false; // because we're hiding the image, so don't show the placeholder. } if (this._isGif() && !SettingsStore.getValue("autoplayGifsAndVideos") && !this.state.hover) { @@ -433,9 +437,9 @@ export default class MImageBody extends React.Component { } // Overidden by MStickerBody - getPlaceholder() { - // MImageBody doesn't show a placeholder whilst the image loads, (but it could do) - return null; + getPlaceholder(width, height) { + if (!this.state.blurhash) return null; + return ; } // Overidden by MStickerBody diff --git a/yarn.lock b/yarn.lock index 98fe42ef13..f1cace67a1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2493,6 +2493,11 @@ blueimp-canvas-to-blob@^3.27.0: resolved "https://registry.yarnpkg.com/blueimp-canvas-to-blob/-/blueimp-canvas-to-blob-3.27.0.tgz#a2bd5c43587b95dedf0f6998603452d1bfcc9b9e" integrity sha512-AcIj+hCw6WquxzJuzC6KzgYmqxLFeTWe88KuY2BEIsW1zbEOfoinDAGlhyvFNGt+U3JElkVSK7anA1FaSdmmfA== +blurhash@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/blurhash/-/blurhash-1.1.3.tgz#dc325af7da836d07a0861d830bdd63694382483e" + integrity sha512-yUhPJvXexbqbyijCIE/T2NCXcj9iNPhWmOKbPTuR/cm7Q5snXYIfnVnz6m7MWOXxODMz/Cr3UcVkRdHiuDVRDw== + bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.4.0: version "4.11.9" resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.9.tgz#26d556829458f9d1e81fc48952493d0ba3507828" From 0e293bca912de8502b5a85edf209a43089576f29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 10 Mar 2021 15:34:45 +0100 Subject: [PATCH 002/221] Reorganize preferences MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- .../tabs/user/PreferencesUserSettingsTab.js | 61 +++++++++++++++---- 1 file changed, 49 insertions(+), 12 deletions(-) diff --git a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js index ae9cad4cfa..49003b03f3 100644 --- a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js @@ -29,6 +29,10 @@ export default class PreferencesUserSettingsTab extends React.Component { 'breadcrumbs', ]; + static KEYBINDINGS_SETTINGS = [ + 'ctrlFForSearch', + ]; + static COMPOSER_SETTINGS = [ 'MessageComposerInput.autoReplaceEmoji', 'MessageComposerInput.suggestEmoji', @@ -37,26 +41,34 @@ export default class PreferencesUserSettingsTab extends React.Component { 'MessageComposerInput.showStickersButton', ]; - static TIMELINE_SETTINGS = [ - 'showTypingNotifications', - 'autoplayGifsAndVideos', - 'urlPreviewsEnabled', - 'TextualBody.enableBigEmoji', - 'showReadReceipts', + static TIME_SETTINGS = [ 'showTwelveHourTimestamps', 'alwaysShowTimestamps', - 'showRedactions', + ]; + static CODE_BLOCKS_SETTINGS = [ 'enableSyntaxHighlightLanguageDetection', 'expandCodeByDefault', - 'scrollToBottomOnMessageSent', 'showCodeLineNumbers', - 'showJoinLeaves', - 'showAvatarChanges', - 'showDisplaynameChanges', + ]; + static IMAGES_AND_VIDEOS_SETTINGS = [ + 'urlPreviewsEnabled', + 'autoplayGifsAndVideos', 'showImages', + ]; + static THINGS_TO_HIDE_ON_TIMELINE_SETTINGS = [ + 'showTypingNotifications', + 'showRedactions', + 'showReadReceipts', + 'showJoinLeaves', + 'showDisplaynameChanges', 'showChatEffects', + 'showAvatarChanges', + ]; + + static TIMELINE_SETTINGS = [ + 'TextualBody.enableBigEmoji', + 'scrollToBottomOnMessageSent', 'Pill.shouldShowPillAvatar', - 'ctrlFForSearch', ]; static GENERAL_SETTINGS = [ @@ -184,11 +196,36 @@ export default class PreferencesUserSettingsTab extends React.Component { {this._renderGroup(PreferencesUserSettingsTab.ROOM_LIST_SETTINGS)} +
+ {_t("Keybindings")} + {this._renderGroup(PreferencesUserSettingsTab.KEYBINDINGS_SETTINGS)} +
+ +
+ {_t("Displaying time")} + {this._renderGroup(PreferencesUserSettingsTab.TIME_SETTINGS)} +
+
{_t("Composer")} {this._renderGroup(PreferencesUserSettingsTab.COMPOSER_SETTINGS)}
+
+ {_t("Code blocks")} + {this._renderGroup(PreferencesUserSettingsTab.CODE_BLOCKS_SETTINGS)} +
+ +
+ {_t("Images, GIFs and videos")} + {this._renderGroup(PreferencesUserSettingsTab.IMAGES_AND_VIDEOS_SETTINGS)} +
+ +
+ {_t("Hide things on the timeline")} + {this._renderGroup(PreferencesUserSettingsTab.THINGS_TO_HIDE_ON_TIMELINE_SETTINGS)} +
+
{_t("Timeline")} {this._renderGroup(PreferencesUserSettingsTab.TIMELINE_SETTINGS)} From 951a807e249d2049eeb5bba7c94876f0988cec2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 10 Mar 2021 15:34:50 +0100 Subject: [PATCH 003/221] i18n MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/i18n/strings/en_EN.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 267721b533..4f07d8fde2 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1284,7 +1284,12 @@ "Show tray icon and minimize window to it on close": "Show tray icon and minimize window to it on close", "Preferences": "Preferences", "Room list": "Room list", + "Keybindings": "Keybindings", + "Displaying time": "Displaying time", "Composer": "Composer", + "Code blocks": "Code blocks", + "Images, GIFs and videos": "Images, GIFs and videos", + "Hide things on the timeline": "Hide things on the timeline", "Timeline": "Timeline", "Autocomplete delay (ms)": "Autocomplete delay (ms)", "Read Marker lifetime (ms)": "Read Marker lifetime (ms)", From 192c0c4941a1aecad61b372d94c1759641e20b7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 30 Apr 2021 11:01:34 +0200 Subject: [PATCH 004/221] Fix method name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- .../settings/tabs/user/PreferencesUserSettingsTab.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx index 24e4d8099e..bc2c120654 100644 --- a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx @@ -236,12 +236,12 @@ export default class PreferencesUserSettingsTab extends React.Component<{}, ISta
{_t("Keybindings")} - {this._renderGroup(PreferencesUserSettingsTab.KEYBINDINGS_SETTINGS)} + {this.renderGroup(PreferencesUserSettingsTab.KEYBINDINGS_SETTINGS)}
{_t("Displaying time")} - {this._renderGroup(PreferencesUserSettingsTab.TIME_SETTINGS)} + {this.renderGroup(PreferencesUserSettingsTab.TIME_SETTINGS)}
@@ -251,17 +251,17 @@ export default class PreferencesUserSettingsTab extends React.Component<{}, ISta
{_t("Code blocks")} - {this._renderGroup(PreferencesUserSettingsTab.CODE_BLOCKS_SETTINGS)} + {this.renderGroup(PreferencesUserSettingsTab.CODE_BLOCKS_SETTINGS)}
{_t("Images, GIFs and videos")} - {this._renderGroup(PreferencesUserSettingsTab.IMAGES_AND_VIDEOS_SETTINGS)} + {this.renderGroup(PreferencesUserSettingsTab.IMAGES_AND_VIDEOS_SETTINGS)}
{_t("Hide things on the timeline")} - {this._renderGroup(PreferencesUserSettingsTab.THINGS_TO_HIDE_ON_TIMELINE_SETTINGS)} + {this.renderGroup(PreferencesUserSettingsTab.THINGS_TO_HIDE_ON_TIMELINE_SETTINGS)}
From 2c7139fb4de1ca37bb674718651d02cd2c4fcba4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 30 Apr 2021 11:03:22 +0200 Subject: [PATCH 005/221] Merge timeline section into one MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- .../tabs/user/PreferencesUserSettingsTab.tsx | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx index bc2c120654..404bc974be 100644 --- a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx @@ -71,7 +71,7 @@ export default class PreferencesUserSettingsTab extends React.Component<{}, ISta 'autoplayGifsAndVideos', 'showImages', ]; - static THINGS_TO_HIDE_ON_TIMELINE_SETTINGS = [ + static TIMELINE_SETTINGS = [ 'showTypingNotifications', 'showRedactions', 'showReadReceipts', @@ -79,14 +79,10 @@ export default class PreferencesUserSettingsTab extends React.Component<{}, ISta 'showDisplaynameChanges', 'showChatEffects', 'showAvatarChanges', - ]; - - static TIMELINE_SETTINGS = [ + 'Pill.shouldShowPillAvatar', 'TextualBody.enableBigEmoji', 'scrollToBottomOnMessageSent', - 'Pill.shouldShowPillAvatar', ]; - static GENERAL_SETTINGS = [ 'TagPanel.enableTagPanel', 'promptBeforeInviteUnknownUsers', @@ -259,11 +255,6 @@ export default class PreferencesUserSettingsTab extends React.Component<{}, ISta {this.renderGroup(PreferencesUserSettingsTab.IMAGES_AND_VIDEOS_SETTINGS)}
-
- {_t("Hide things on the timeline")} - {this.renderGroup(PreferencesUserSettingsTab.THINGS_TO_HIDE_ON_TIMELINE_SETTINGS)} -
-
{_t("Timeline")} {this.renderGroup(PreferencesUserSettingsTab.TIMELINE_SETTINGS)} From 70c8c6477248466c2bdf0528da709124d886d8c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 30 Apr 2021 11:03:55 +0200 Subject: [PATCH 006/221] Rename to Keyboard shortcuts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- .../views/settings/tabs/user/PreferencesUserSettingsTab.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx index 404bc974be..7dc25d1879 100644 --- a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx @@ -231,7 +231,7 @@ export default class PreferencesUserSettingsTab extends React.Component<{}, ISta
- {_t("Keybindings")} + {_t("Keyboard shortcuts")} {this.renderGroup(PreferencesUserSettingsTab.KEYBINDINGS_SETTINGS)}
From 5f6895487f31668cc76f35c5d504533c0caafcdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 30 Apr 2021 11:09:35 +0200 Subject: [PATCH 007/221] Rename ctrl+f MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/settings/Settings.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts index 2a26eeac13..2912f0a8ee 100644 --- a/src/settings/Settings.ts +++ b/src/settings/Settings.ts @@ -384,7 +384,7 @@ export const SETTINGS: {[setting: string]: ISetting} = { }, "ctrlFForSearch": { supportedLevels: LEVELS_ACCOUNT_SETTINGS, - displayName: isMac ? _td("Use Command + F to search") : _td("Use Ctrl + F to search"), + displayName: isMac ? _td("Use Command + F to search current room") : _td("Use Ctrl + F to search current room"), default: false, }, "MessageComposerInput.ctrlEnterToSend": { From e52fd3791e18d5e42ce5f2d2a50904d4430ffacc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 30 Apr 2021 11:10:45 +0200 Subject: [PATCH 008/221] i18n MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/i18n/strings/en_EN.json | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 69e77cf1bc..2e21016933 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -823,8 +823,8 @@ "Enable big emoji in chat": "Enable big emoji in chat", "Send typing notifications": "Send typing notifications", "Show typing notifications": "Show typing notifications", - "Use Command + F to search": "Use Command + F to search", - "Use Ctrl + F to search": "Use Ctrl + F to search", + "Use Command + F to search current room": "Use Command + F to search current room", + "Use Ctrl + F to search current room": "Use Ctrl + F to search current room", "Use Command + Enter to send a message": "Use Command + Enter to send a message", "Use Ctrl + Enter to send a message": "Use Ctrl + Enter to send a message", "Automatically replace plain text Emoji": "Automatically replace plain text Emoji", @@ -1296,12 +1296,11 @@ "Show tray icon and minimize window to it on close": "Show tray icon and minimize window to it on close", "Preferences": "Preferences", "Room list": "Room list", - "Keybindings": "Keybindings", + "Keyboard shortcuts": "Keyboard shortcuts", "Displaying time": "Displaying time", "Composer": "Composer", "Code blocks": "Code blocks", "Images, GIFs and videos": "Images, GIFs and videos", - "Hide things on the timeline": "Hide things on the timeline", "Timeline": "Timeline", "Autocomplete delay (ms)": "Autocomplete delay (ms)", "Read Marker lifetime (ms)": "Read Marker lifetime (ms)", From bebcb32e8fb4270632c4c2a2a85a2271a8b121b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 2 May 2021 16:23:35 +0200 Subject: [PATCH 009/221] Add dragCallbacks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/voip/CallView.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/components/views/voip/CallView.tsx b/src/components/views/voip/CallView.tsx index 6745713845..65547bb814 100644 --- a/src/components/views/voip/CallView.tsx +++ b/src/components/views/voip/CallView.tsx @@ -49,6 +49,13 @@ interface IProps { // This is sort of a proxy for a number of things but we currently have no // need to control those things separately, so this is simpler. pipMode?: boolean; + + // Callbacks for dragging the CallView in PIP mode + dragCallbacks?: { + onStartMoving: (event: React.MouseEvent) => void; + onMoving: (event: React.MouseEvent) => void; + onEndMoving: () => void; + } } interface IState { From 8948c7419cdbd587550e20720ea0bb6f11a44358 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 2 May 2021 16:24:47 +0200 Subject: [PATCH 010/221] Call dragCallbacks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/voip/CallView.tsx | 30 ++++++++++++++++---------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/src/components/views/voip/CallView.tsx b/src/components/views/voip/CallView.tsx index 65547bb814..c35e62448e 100644 --- a/src/components/views/voip/CallView.tsx +++ b/src/components/views/voip/CallView.tsx @@ -623,19 +623,27 @@ export default class CallView extends React.Component { ; } - header =
- - - -
-
{callRoom.name}
-
- {callTypeText} - {secondaryCallInfo} + header = ( +
+ + + +
+
{callRoom.name}
+
+ {callTypeText} + {secondaryCallInfo} +
+ {headerControls}
- {headerControls} -
; + ); myClassName = 'mx_CallView_pip'; } From c97bbe11a93eb9c49ce45b4f1a4c2df280b344a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 2 May 2021 16:26:03 +0200 Subject: [PATCH 011/221] Prep state and props for dragging MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/voip/CallPreview.tsx | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/components/views/voip/CallPreview.tsx b/src/components/views/voip/CallPreview.tsx index d31afddec9..8dca5314e5 100644 --- a/src/components/views/voip/CallPreview.tsx +++ b/src/components/views/voip/CallPreview.tsx @@ -50,6 +50,13 @@ interface IState { // Any other call we're displaying: only if the user is on two calls and not viewing either of the rooms // they belong to secondaryCall: MatrixCall; + + // Position of the CallPreview + translationX: number; + translationY: number; + + // True if the CallPreview is being dragged + moving: boolean; } // Splits a list of calls into one 'primary' one and a list @@ -106,9 +113,17 @@ export default class CallPreview extends React.Component { roomId, primaryCall: primaryCall, secondaryCall: secondaryCalls[0], + translationX: 0, + translationY: 0, + moving: false, }; } + private initX = 0; + private initY = 0; + private lastX = 0; + private lastY = 0; + public componentDidMount() { this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate); this.dispatcherRef = dis.register(this.onAction); From f64a9501955e5506b09e35009003133edf268dc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 2 May 2021 16:26:41 +0200 Subject: [PATCH 012/221] Prep basic methods for dragging MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/voip/CallPreview.tsx | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/components/views/voip/CallPreview.tsx b/src/components/views/voip/CallPreview.tsx index 8dca5314e5..68fcef6747 100644 --- a/src/components/views/voip/CallPreview.tsx +++ b/src/components/views/voip/CallPreview.tsx @@ -184,6 +184,29 @@ export default class CallPreview extends React.Component { }); } + private onStartMoving = (event: React.MouseEvent) => { + this.setState({moving: true}); + + this.initX = event.pageX - this.lastX; + this.initY = event.pageY - this.lastY; + } + + private onMoving = (event: React.MouseEvent) => { + if (!this.state.moving) return; + + this.lastX = event.pageX - this.initX; + this.lastY = event.pageY - this.initY; + + this.setState({ + translationX: this.lastX, + translationY: this.lastY, + }); + } + + private onEndMoving = () => { + this.setState({moving: false}); + } + public render() { if (this.state.primaryCall) { return ( From 11222e7a467a6e58007ae14e539856a21251bd20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 2 May 2021 16:26:54 +0200 Subject: [PATCH 013/221] Wire up dragging MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/voip/CallPreview.tsx | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/components/views/voip/CallPreview.tsx b/src/components/views/voip/CallPreview.tsx index 68fcef6747..499cdfb526 100644 --- a/src/components/views/voip/CallPreview.tsx +++ b/src/components/views/voip/CallPreview.tsx @@ -209,8 +209,26 @@ export default class CallPreview extends React.Component { public render() { if (this.state.primaryCall) { + const translatePixelsX = this.state.translationX + "px"; + const translatePixelsY = this.state.translationY + "px"; + const style = { + transform: `translateX(${translatePixelsX}) + translateY(${translatePixelsY})`, + }; + return ( - +
+ +
); } From 241e626e96a85fcf48a028f3af418ccd14e5235f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 2 May 2021 20:55:05 +0200 Subject: [PATCH 014/221] Don't listen for onMouseLeave MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This would cause problems because the moving element wouldn't catch up with the user Signed-off-by: Šimon Brandner --- src/components/views/voip/CallView.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/views/voip/CallView.tsx b/src/components/views/voip/CallView.tsx index c35e62448e..23a4fcca59 100644 --- a/src/components/views/voip/CallView.tsx +++ b/src/components/views/voip/CallView.tsx @@ -629,7 +629,6 @@ export default class CallView extends React.Component { onMouseDown={this.props.dragCallbacks?.onStartMoving} onMouseMove={this.props.dragCallbacks?.onMoving} onMouseUp={this.props.dragCallbacks?.onEndMoving} - onMouseLeave={this.props.dragCallbacks?.onEndMoving} > From 53b8fd3072f8dddcb4e5e8b44da6d77c3a1e6ac3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 2 May 2021 20:57:18 +0200 Subject: [PATCH 015/221] Listen for mousemove on document scale MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/voip/CallPreview.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/views/voip/CallPreview.tsx b/src/components/views/voip/CallPreview.tsx index 499cdfb526..17e2e9cf1a 100644 --- a/src/components/views/voip/CallPreview.tsx +++ b/src/components/views/voip/CallPreview.tsx @@ -126,12 +126,14 @@ export default class CallPreview extends React.Component { public componentDidMount() { this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate); + document.addEventListener("mousemove", this.onMoving); this.dispatcherRef = dis.register(this.onAction); MatrixClientPeg.get().on(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold); } public componentWillUnmount() { MatrixClientPeg.get().removeListener(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold); + document.removeEventListener("mousemove", this.onMoving); if (this.roomStoreToken) { this.roomStoreToken.remove(); } @@ -191,7 +193,7 @@ export default class CallPreview extends React.Component { this.initY = event.pageY - this.lastY; } - private onMoving = (event: React.MouseEvent) => { + private onMoving = (event: React.MouseEvent | MouseEvent) => { if (!this.state.moving) return; this.lastX = event.pageX - this.initX; From fca5347668465341ef0757c56de0adb78235741a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 2 May 2021 21:17:59 +0200 Subject: [PATCH 016/221] Add preventDefault() and stopPropagation() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This avoids text being selected while dragging Signed-off-by: Šimon Brandner --- src/components/views/voip/CallPreview.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/components/views/voip/CallPreview.tsx b/src/components/views/voip/CallPreview.tsx index 17e2e9cf1a..761458cb4c 100644 --- a/src/components/views/voip/CallPreview.tsx +++ b/src/components/views/voip/CallPreview.tsx @@ -187,6 +187,9 @@ export default class CallPreview extends React.Component { } private onStartMoving = (event: React.MouseEvent) => { + event.preventDefault(); + event.stopPropagation(); + this.setState({moving: true}); this.initX = event.pageX - this.lastX; @@ -194,6 +197,9 @@ export default class CallPreview extends React.Component { } private onMoving = (event: React.MouseEvent | MouseEvent) => { + event.preventDefault(); + event.stopPropagation(); + if (!this.state.moving) return; this.lastX = event.pageX - this.initX; From 51e80dd17228f5645c0b7bbab42206a0bffa43fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 3 May 2021 07:50:21 +0200 Subject: [PATCH 017/221] Remove onMoving listner from CallView MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is not necessary since we already listen for it in CallPreview Signed-off-by: Šimon Brandner --- src/components/views/voip/CallView.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/views/voip/CallView.tsx b/src/components/views/voip/CallView.tsx index 23a4fcca59..cbedfb3a3d 100644 --- a/src/components/views/voip/CallView.tsx +++ b/src/components/views/voip/CallView.tsx @@ -53,7 +53,6 @@ interface IProps { // Callbacks for dragging the CallView in PIP mode dragCallbacks?: { onStartMoving: (event: React.MouseEvent) => void; - onMoving: (event: React.MouseEvent) => void; onEndMoving: () => void; } } @@ -627,7 +626,6 @@ export default class CallView extends React.Component {
From 7042eb38ddb7f838b2fe8dc952aef7c02f45e3e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 3 May 2021 08:12:54 +0200 Subject: [PATCH 018/221] Listen for mouseup on the document MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/voip/CallPreview.tsx | 2 ++ src/components/views/voip/CallView.tsx | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/views/voip/CallPreview.tsx b/src/components/views/voip/CallPreview.tsx index 761458cb4c..aa2e71339e 100644 --- a/src/components/views/voip/CallPreview.tsx +++ b/src/components/views/voip/CallPreview.tsx @@ -127,6 +127,7 @@ export default class CallPreview extends React.Component { public componentDidMount() { this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate); document.addEventListener("mousemove", this.onMoving); + document.addEventListener("mouseup", this.onEndMoving); this.dispatcherRef = dis.register(this.onAction); MatrixClientPeg.get().on(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold); } @@ -134,6 +135,7 @@ export default class CallPreview extends React.Component { public componentWillUnmount() { MatrixClientPeg.get().removeListener(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold); document.removeEventListener("mousemove", this.onMoving); + document.removeEventListener("mouseup", this.onEndMoving); if (this.roomStoreToken) { this.roomStoreToken.remove(); } diff --git a/src/components/views/voip/CallView.tsx b/src/components/views/voip/CallView.tsx index cbedfb3a3d..1f555e5227 100644 --- a/src/components/views/voip/CallView.tsx +++ b/src/components/views/voip/CallView.tsx @@ -626,7 +626,6 @@ export default class CallView extends React.Component {
From adcdd72a0838e1eddd736cbafb0cb1883cfdf1bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 3 May 2021 15:07:25 +0200 Subject: [PATCH 019/221] preventDefault() and stopPropagation() only if moving MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/voip/CallPreview.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/voip/CallPreview.tsx b/src/components/views/voip/CallPreview.tsx index aa2e71339e..3b7a297841 100644 --- a/src/components/views/voip/CallPreview.tsx +++ b/src/components/views/voip/CallPreview.tsx @@ -199,11 +199,11 @@ export default class CallPreview extends React.Component { } private onMoving = (event: React.MouseEvent | MouseEvent) => { + if (!this.state.moving) return; + event.preventDefault(); event.stopPropagation(); - if (!this.state.moving) return; - this.lastX = event.pageX - this.initX; this.lastY = event.pageY - this.initY; From 0851cf4415f3dd04502326909cee1b6400ea73ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 3 May 2021 15:16:08 +0200 Subject: [PATCH 020/221] Simplifie things MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/voip/CallPreview.tsx | 6 +----- src/components/views/voip/CallView.tsx | 9 +++------ 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/src/components/views/voip/CallPreview.tsx b/src/components/views/voip/CallPreview.tsx index 3b7a297841..153258d2c8 100644 --- a/src/components/views/voip/CallPreview.tsx +++ b/src/components/views/voip/CallPreview.tsx @@ -232,11 +232,7 @@ export default class CallPreview extends React.Component { call={this.state.primaryCall} secondaryCall={this.state.secondaryCall} pipMode={true} - dragCallbacks={{ - onStartMoving: this.onStartMoving, - onMoving: this.onMoving, - onEndMoving: this.onEndMoving, - }} + onMouseDownOnHeader={this.onStartMoving} />
); diff --git a/src/components/views/voip/CallView.tsx b/src/components/views/voip/CallView.tsx index 1f555e5227..e8d3666c53 100644 --- a/src/components/views/voip/CallView.tsx +++ b/src/components/views/voip/CallView.tsx @@ -50,11 +50,8 @@ interface IProps { // need to control those things separately, so this is simpler. pipMode?: boolean; - // Callbacks for dragging the CallView in PIP mode - dragCallbacks?: { - onStartMoving: (event: React.MouseEvent) => void; - onEndMoving: () => void; - } + // Used for dragging the PiP CallView + onMouseDownOnHeader?: (event: React.MouseEvent) => void; } interface IState { @@ -625,7 +622,7 @@ export default class CallView extends React.Component { header = (
From b8cb72345ccbe115ba3bdb861467ee139ecff20d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 3 May 2021 15:39:00 +0200 Subject: [PATCH 021/221] Remove unnecessary margin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/voip/_CallView.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/res/css/views/voip/_CallView.scss b/res/css/views/voip/_CallView.scss index 7292e325df..18e7c215cb 100644 --- a/res/css/views/voip/_CallView.scss +++ b/res/css/views/voip/_CallView.scss @@ -39,7 +39,6 @@ limitations under the License. .mx_CallView_pip { width: 320px; padding-bottom: 8px; - margin-top: 10px; background-color: $voipcall-plinth-color; box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.20); border-radius: 8px; From fe5fb1885fc95243a9964942877268f7e6eef993 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 3 May 2021 15:39:24 +0200 Subject: [PATCH 022/221] Add styling for CallPreview and make it fixed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/voip/_CallPreview.scss | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 res/css/views/voip/_CallPreview.scss diff --git a/res/css/views/voip/_CallPreview.scss b/res/css/views/voip/_CallPreview.scss new file mode 100644 index 0000000000..92348fb465 --- /dev/null +++ b/res/css/views/voip/_CallPreview.scss @@ -0,0 +1,21 @@ +/* +Copyright 2021 Šimon Brandner + +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. +*/ + +.mx_CallPreview { + position: fixed; + left: 0; + top: 0; +} From 7faf9eb4ccd56dfe9ab308964471a5ff099c805b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 3 May 2021 15:39:37 +0200 Subject: [PATCH 023/221] Use styling for CallPreview MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/_components.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/res/css/_components.scss b/res/css/_components.scss index 0057f8a8fc..c476e577df 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -254,6 +254,7 @@ @import "./views/voip/_CallContainer.scss"; @import "./views/voip/_CallView.scss"; @import "./views/voip/_CallViewForRoom.scss"; +@import "./views/voip/_CallPreview.scss"; @import "./views/voip/_DialPad.scss"; @import "./views/voip/_DialPadContextMenu.scss"; @import "./views/voip/_DialPadModal.scss"; From 76f503666c5e7594751302a5a5fabc9babc8a64e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 3 May 2021 15:40:12 +0200 Subject: [PATCH 024/221] Add default offset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/voip/CallPreview.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/components/views/voip/CallPreview.tsx b/src/components/views/voip/CallPreview.tsx index 153258d2c8..74e1815631 100644 --- a/src/components/views/voip/CallPreview.tsx +++ b/src/components/views/voip/CallPreview.tsx @@ -29,6 +29,9 @@ import { MatrixClientPeg } from '../../../MatrixClientPeg'; import {replaceableComponent} from "../../../utils/replaceableComponent"; import { Action } from '../../../dispatcher/actions'; +const DEFAULT_X_OFFSET = 64; +const DEFAULT_Y_OFFSET = 64; + const SHOW_CALL_IN_STATES = [ CallState.Connected, CallState.InviteSent, @@ -113,16 +116,16 @@ export default class CallPreview extends React.Component { roomId, primaryCall: primaryCall, secondaryCall: secondaryCalls[0], - translationX: 0, - translationY: 0, + translationX: DEFAULT_X_OFFSET, + translationY: DEFAULT_Y_OFFSET, moving: false, }; } private initX = 0; private initY = 0; - private lastX = 0; - private lastY = 0; + private lastX = DEFAULT_X_OFFSET; + private lastY = DEFAULT_Y_OFFSET; public componentDidMount() { this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate); From 2c9231641b57ea7d5fa577c9aa9445347360665f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 3 May 2021 15:40:59 +0200 Subject: [PATCH 025/221] Add ref to callViewWrapper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/voip/CallPreview.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/components/views/voip/CallPreview.tsx b/src/components/views/voip/CallPreview.tsx index 74e1815631..4a0ccc93b9 100644 --- a/src/components/views/voip/CallPreview.tsx +++ b/src/components/views/voip/CallPreview.tsx @@ -15,7 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; +import React, { createRef } from 'react'; import CallView from "./CallView"; import RoomViewStore from '../../../stores/RoomViewStore'; @@ -122,6 +122,8 @@ export default class CallPreview extends React.Component { }; } + private callViewWrapper = createRef(); + private initX = 0; private initY = 0; private lastX = DEFAULT_X_OFFSET; @@ -230,7 +232,11 @@ export default class CallPreview extends React.Component { }; return ( -
+
Date: Mon, 3 May 2021 15:43:13 +0200 Subject: [PATCH 026/221] Add semicolons to event listeners MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/voip/CallPreview.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/views/voip/CallPreview.tsx b/src/components/views/voip/CallPreview.tsx index 4a0ccc93b9..366b0d30b3 100644 --- a/src/components/views/voip/CallPreview.tsx +++ b/src/components/views/voip/CallPreview.tsx @@ -191,7 +191,7 @@ export default class CallPreview extends React.Component { primaryCall: primaryCall, secondaryCall: secondaryCalls[0], }); - } + }; private onStartMoving = (event: React.MouseEvent) => { event.preventDefault(); @@ -201,7 +201,7 @@ export default class CallPreview extends React.Component { this.initX = event.pageX - this.lastX; this.initY = event.pageY - this.lastY; - } + }; private onMoving = (event: React.MouseEvent | MouseEvent) => { if (!this.state.moving) return; @@ -216,11 +216,11 @@ export default class CallPreview extends React.Component { translationX: this.lastX, translationY: this.lastY, }); - } + }; private onEndMoving = () => { this.setState({moving: false}); - } + }; public render() { if (this.state.primaryCall) { From d8d380c74de6ff27d9e0b6c764e1db35b583119e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 3 May 2021 17:22:45 +0200 Subject: [PATCH 027/221] Always keep the PiP CallView on the screen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/voip/CallPreview.tsx | 25 +++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/components/views/voip/CallPreview.tsx b/src/components/views/voip/CallPreview.tsx index 366b0d30b3..74d43dcb19 100644 --- a/src/components/views/voip/CallPreview.tsx +++ b/src/components/views/voip/CallPreview.tsx @@ -209,8 +209,29 @@ export default class CallPreview extends React.Component { event.preventDefault(); event.stopPropagation(); - this.lastX = event.pageX - this.initX; - this.lastY = event.pageY - this.initY; + const width = this.callViewWrapper.current.clientWidth; + const height = this.callViewWrapper.current.clientHeight; + + const precalculatedLastX = event.pageX - this.initX; + const precalculatedLastY = event.pageY - this.initY; + + // Avoid overflow on the x axis + if (precalculatedLastX + width >= window.innerWidth) { + this.lastX = window.innerWidth - width; + } else if (precalculatedLastX <= 0) { + this.lastX = 0; + } else { + this.lastX = precalculatedLastX; + } + + // Avoid overflow on the y axis + if (precalculatedLastY + height >= window.innerHeight) { + this.lastY = window.innerHeight - height; + } else if (precalculatedLastY <= 0) { + this.lastY = 0; + } else { + this.lastY = precalculatedLastY; + } this.setState({ translationX: this.lastX, From be2da6376e1c5b3a6be2c23a3bfbe89766e5bbc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 3 May 2021 17:49:55 +0200 Subject: [PATCH 028/221] Simplifie translation code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/voip/CallPreview.tsx | 25 ++++++++++++----------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/components/views/voip/CallPreview.tsx b/src/components/views/voip/CallPreview.tsx index 74d43dcb19..9a9ebd5e92 100644 --- a/src/components/views/voip/CallPreview.tsx +++ b/src/components/views/voip/CallPreview.tsx @@ -126,8 +126,6 @@ export default class CallPreview extends React.Component { private initX = 0; private initY = 0; - private lastX = DEFAULT_X_OFFSET; - private lastY = DEFAULT_Y_OFFSET; public componentDidMount() { this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate); @@ -199,8 +197,8 @@ export default class CallPreview extends React.Component { this.setState({moving: true}); - this.initX = event.pageX - this.lastX; - this.initY = event.pageY - this.lastY; + this.initX = event.pageX - this.state.translationX; + this.initY = event.pageY - this.state.translationY; }; private onMoving = (event: React.MouseEvent | MouseEvent) => { @@ -215,27 +213,30 @@ export default class CallPreview extends React.Component { const precalculatedLastX = event.pageX - this.initX; const precalculatedLastY = event.pageY - this.initY; + let translationX; + let translationY; + // Avoid overflow on the x axis if (precalculatedLastX + width >= window.innerWidth) { - this.lastX = window.innerWidth - width; + translationX = window.innerWidth - width; } else if (precalculatedLastX <= 0) { - this.lastX = 0; + translationX = 0; } else { - this.lastX = precalculatedLastX; + translationX = precalculatedLastX; } // Avoid overflow on the y axis if (precalculatedLastY + height >= window.innerHeight) { - this.lastY = window.innerHeight - height; + translationY = window.innerHeight - height; } else if (precalculatedLastY <= 0) { - this.lastY = 0; + translationY = 0; } else { - this.lastY = precalculatedLastY; + translationY = precalculatedLastY; } this.setState({ - translationX: this.lastX, - translationY: this.lastY, + translationX: translationX, + translationY: translationY, }); }; From 0bf2b01f84ec490ca48b8d07172202dab7907dcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 3 May 2021 18:11:02 +0200 Subject: [PATCH 029/221] Keep PiP in the window when resizing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/voip/CallPreview.tsx | 41 ++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/src/components/views/voip/CallPreview.tsx b/src/components/views/voip/CallPreview.tsx index 9a9ebd5e92..410b60dcb6 100644 --- a/src/components/views/voip/CallPreview.tsx +++ b/src/components/views/voip/CallPreview.tsx @@ -29,6 +29,9 @@ import { MatrixClientPeg } from '../../../MatrixClientPeg'; import {replaceableComponent} from "../../../utils/replaceableComponent"; import { Action } from '../../../dispatcher/actions'; +const PIP_VIEW_WIDTH = 320; +const PIP_VIEW_HEIGHT = 180; + const DEFAULT_X_OFFSET = 64; const DEFAULT_Y_OFFSET = 64; @@ -116,7 +119,7 @@ export default class CallPreview extends React.Component { roomId, primaryCall: primaryCall, secondaryCall: secondaryCalls[0], - translationX: DEFAULT_X_OFFSET, + translationX: window.innerWidth - DEFAULT_X_OFFSET - PIP_VIEW_WIDTH, translationY: DEFAULT_Y_OFFSET, moving: false, }; @@ -131,6 +134,7 @@ export default class CallPreview extends React.Component { this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate); document.addEventListener("mousemove", this.onMoving); document.addEventListener("mouseup", this.onEndMoving); + window.addEventListener("resize", this.onWindowSizeChanged); this.dispatcherRef = dis.register(this.onAction); MatrixClientPeg.get().on(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold); } @@ -139,6 +143,7 @@ export default class CallPreview extends React.Component { MatrixClientPeg.get().removeListener(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold); document.removeEventListener("mousemove", this.onMoving); document.removeEventListener("mouseup", this.onEndMoving); + window.removeEventListener("resize", this.onWindowSizeChanged); if (this.roomStoreToken) { this.roomStoreToken.remove(); } @@ -146,6 +151,40 @@ export default class CallPreview extends React.Component { SettingsStore.unwatchSetting(this.settingsWatcherRef); } + private onWindowSizeChanged = () => { + const width = this.callViewWrapper.current.clientWidth || PIP_VIEW_WIDTH; + const height = this.callViewWrapper.current.clientHeight || PIP_VIEW_HEIGHT; + + const precalculatedLastX = this.state.translationX; + const precalculatedLastY = this.state.translationY; + + let translationX; + let translationY; + + // Avoid overflow on the x axis + if (precalculatedLastX + width >= window.innerWidth) { + translationX = window.innerWidth - width; + } else if (precalculatedLastX <= 0) { + translationX = 0; + } else { + translationX = precalculatedLastX; + } + + // Avoid overflow on the y axis + if (precalculatedLastY + height >= window.innerHeight) { + translationY = window.innerHeight - height; + } else if (precalculatedLastY <= 0) { + translationY = 0; + } else { + translationY = precalculatedLastY; + } + + this.setState({ + translationX: translationX, + translationY: translationY, + }); + } + private onRoomViewStoreUpdate = (payload) => { if (RoomViewStore.getRoomId() === this.state.roomId) return; From 941a6e1c1bb48f2214638768f17018f4f0b4e77f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 3 May 2021 18:16:36 +0200 Subject: [PATCH 030/221] Don't duplicate code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/voip/CallPreview.tsx | 67 +++++++---------------- 1 file changed, 19 insertions(+), 48 deletions(-) diff --git a/src/components/views/voip/CallPreview.tsx b/src/components/views/voip/CallPreview.tsx index 410b60dcb6..9ab2b9441a 100644 --- a/src/components/views/voip/CallPreview.tsx +++ b/src/components/views/voip/CallPreview.tsx @@ -152,36 +152,37 @@ export default class CallPreview extends React.Component { } private onWindowSizeChanged = () => { + this.setTranslation(this.state.translationX, this.state.translationY); + } + + private setTranslation(inTranslationX: number, inTranslationY: number) { const width = this.callViewWrapper.current.clientWidth || PIP_VIEW_WIDTH; const height = this.callViewWrapper.current.clientHeight || PIP_VIEW_HEIGHT; - const precalculatedLastX = this.state.translationX; - const precalculatedLastY = this.state.translationY; - - let translationX; - let translationY; + let outTranslationX; + let outTranslationY; // Avoid overflow on the x axis - if (precalculatedLastX + width >= window.innerWidth) { - translationX = window.innerWidth - width; - } else if (precalculatedLastX <= 0) { - translationX = 0; + if (inTranslationX + width >= window.innerWidth) { + outTranslationX = window.innerWidth - width; + } else if (inTranslationX <= 0) { + outTranslationX = 0; } else { - translationX = precalculatedLastX; + outTranslationX = inTranslationX; } // Avoid overflow on the y axis - if (precalculatedLastY + height >= window.innerHeight) { - translationY = window.innerHeight - height; - } else if (precalculatedLastY <= 0) { - translationY = 0; + if (inTranslationY + height >= window.innerHeight) { + outTranslationY = window.innerHeight - height; + } else if (inTranslationY <= 0) { + outTranslationY = 0; } else { - translationY = precalculatedLastY; + outTranslationY = inTranslationY; } this.setState({ - translationX: translationX, - translationY: translationY, + translationX: outTranslationX, + translationY: outTranslationY, }); } @@ -246,37 +247,7 @@ export default class CallPreview extends React.Component { event.preventDefault(); event.stopPropagation(); - const width = this.callViewWrapper.current.clientWidth; - const height = this.callViewWrapper.current.clientHeight; - - const precalculatedLastX = event.pageX - this.initX; - const precalculatedLastY = event.pageY - this.initY; - - let translationX; - let translationY; - - // Avoid overflow on the x axis - if (precalculatedLastX + width >= window.innerWidth) { - translationX = window.innerWidth - width; - } else if (precalculatedLastX <= 0) { - translationX = 0; - } else { - translationX = precalculatedLastX; - } - - // Avoid overflow on the y axis - if (precalculatedLastY + height >= window.innerHeight) { - translationY = window.innerHeight - height; - } else if (precalculatedLastY <= 0) { - translationY = 0; - } else { - translationY = precalculatedLastY; - } - - this.setState({ - translationX: translationX, - translationY: translationY, - }); + this.setTranslation(event.pageX - this.initX, event.pageY - this.initY); }; private onEndMoving = () => { From 889b90fbc3cd564f6c6717d365f37e5fc21c2f79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 3 May 2021 18:33:24 +0200 Subject: [PATCH 031/221] Fix const values MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/voip/CallPreview.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/views/voip/CallPreview.tsx b/src/components/views/voip/CallPreview.tsx index 9ab2b9441a..b3e66f09b3 100644 --- a/src/components/views/voip/CallPreview.tsx +++ b/src/components/views/voip/CallPreview.tsx @@ -29,11 +29,11 @@ import { MatrixClientPeg } from '../../../MatrixClientPeg'; import {replaceableComponent} from "../../../utils/replaceableComponent"; import { Action } from '../../../dispatcher/actions'; -const PIP_VIEW_WIDTH = 320; -const PIP_VIEW_HEIGHT = 180; +const PIP_VIEW_WIDTH = 336; +const PIP_VIEW_HEIGHT = 232; -const DEFAULT_X_OFFSET = 64; -const DEFAULT_Y_OFFSET = 64; +const DEFAULT_X_OFFSET = 16; +const DEFAULT_Y_OFFSET = 48; const SHOW_CALL_IN_STATES = [ CallState.Connected, From f79339c2dadd86bac266c7f0eab7e3eb0317b479 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 3 May 2021 18:36:11 +0200 Subject: [PATCH 032/221] Add missing semicolon MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/voip/CallPreview.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/voip/CallPreview.tsx b/src/components/views/voip/CallPreview.tsx index b3e66f09b3..60153732d8 100644 --- a/src/components/views/voip/CallPreview.tsx +++ b/src/components/views/voip/CallPreview.tsx @@ -153,7 +153,7 @@ export default class CallPreview extends React.Component { private onWindowSizeChanged = () => { this.setTranslation(this.state.translationX, this.state.translationY); - } + }; private setTranslation(inTranslationX: number, inTranslationY: number) { const width = this.callViewWrapper.current.clientWidth || PIP_VIEW_WIDTH; From 9755da6f0915c8a3dec9726c33f60f4468f88037 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 3 May 2021 19:59:51 +0200 Subject: [PATCH 033/221] Add ? MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/voip/CallPreview.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/voip/CallPreview.tsx b/src/components/views/voip/CallPreview.tsx index 60153732d8..1b7c7f6e48 100644 --- a/src/components/views/voip/CallPreview.tsx +++ b/src/components/views/voip/CallPreview.tsx @@ -156,8 +156,8 @@ export default class CallPreview extends React.Component { }; private setTranslation(inTranslationX: number, inTranslationY: number) { - const width = this.callViewWrapper.current.clientWidth || PIP_VIEW_WIDTH; - const height = this.callViewWrapper.current.clientHeight || PIP_VIEW_HEIGHT; + const width = this.callViewWrapper.current?.clientWidth || PIP_VIEW_WIDTH; + const height = this.callViewWrapper.current?.clientHeight || PIP_VIEW_HEIGHT; let outTranslationX; let outTranslationY; From 00d093b4ffb83e4d100b706757e0e44a8f1fb376 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 19 May 2021 08:56:14 +0100 Subject: [PATCH 034/221] yarn lock --- yarn.lock | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/yarn.lock b/yarn.lock index 19c0646d32..57b1198019 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2180,6 +2180,11 @@ blueimp-canvas-to-blob@^3.28.0: resolved "https://registry.yarnpkg.com/blueimp-canvas-to-blob/-/blueimp-canvas-to-blob-3.28.0.tgz#c8ab4dc6bb08774a7f273798cdf94b0776adf6c8" integrity sha512-5q+YHzgGsuHQ01iouGgJaPJXod2AzTxJXmVv90PpGrRxU7G7IqgPqWXz+PBmt3520jKKi6irWbNV87DicEa7wg== +blurhash@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/blurhash/-/blurhash-1.1.3.tgz#dc325af7da836d07a0861d830bdd63694382483e" + integrity sha512-yUhPJvXexbqbyijCIE/T2NCXcj9iNPhWmOKbPTuR/cm7Q5snXYIfnVnz6m7MWOXxODMz/Cr3UcVkRdHiuDVRDw== + boolbase@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" From c21445c406fd077e17fd17661590739069a82995 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 20 May 2021 13:23:17 +0100 Subject: [PATCH 035/221] switch from MatrixClientPeg in ContentMessages for consistency --- src/ContentMessages.tsx | 19 +++++++++---------- src/components/structures/UploadBar.tsx | 5 ++++- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/ContentMessages.tsx b/src/ContentMessages.tsx index 65b6f1aba4..ba84b30733 100644 --- a/src/ContentMessages.tsx +++ b/src/ContentMessages.tsx @@ -18,7 +18,6 @@ limitations under the License. import React from "react"; import dis from './dispatcher/dispatcher'; -import {MatrixClientPeg} from './MatrixClientPeg'; import {MatrixClient} from "matrix-js-sdk/src/client"; import * as sdk from './index'; import { _t } from './languageHandler'; @@ -344,7 +343,7 @@ function uploadFile(matrixClient: MatrixClient, roomId: string, file: File | Blo }); (prom as IAbortablePromise).abort = () => { canceled = true; - if (uploadPromise) MatrixClientPeg.get().cancelUpload(uploadPromise); + if (uploadPromise) matrixClient.cancelUpload(uploadPromise); }; return prom; } else { @@ -362,7 +361,7 @@ function uploadFile(matrixClient: MatrixClient, roomId: string, file: File | Blo }); promise1.abort = () => { canceled = true; - MatrixClientPeg.get().cancelUpload(basePromise); + matrixClient.cancelUpload(basePromise); }; return promise1; } @@ -372,9 +371,9 @@ export default class ContentMessages { private inprogress: IUpload[] = []; private mediaConfig: IMediaConfig = null; - sendStickerContentToRoom(url: string, roomId: string, info: string, text: string, matrixClient: MatrixClient) { + sendStickerContentToRoom(url: string, roomId: string, info: object, text: string, matrixClient: MatrixClient) { const startTime = CountlyAnalytics.getTimestamp(); - const prom = MatrixClientPeg.get().sendStickerMessage(roomId, url, info, text).catch((e) => { + const prom = matrixClient.sendStickerMessage(roomId, url, info, text).catch((e) => { console.warn(`Failed to send content with URL ${url} to room ${roomId}`, e); throw e; }); @@ -416,7 +415,7 @@ export default class ContentMessages { if (!this.mediaConfig) { // hot-path optimization to not flash a spinner if we don't need to const modal = Modal.createDialog(Spinner, null, 'mx_Dialog_spinner'); - await this.ensureMediaConfigFetched(); + await this.ensureMediaConfigFetched(matrixClient); modal.close(); } @@ -471,7 +470,7 @@ export default class ContentMessages { return this.inprogress.filter(u => !u.canceled); } - cancelUpload(promise: Promise) { + cancelUpload(promise: Promise, matrixClient: MatrixClient) { let upload: IUpload; for (let i = 0; i < this.inprogress.length; ++i) { if (this.inprogress[i].promise === promise) { @@ -481,7 +480,7 @@ export default class ContentMessages { } if (upload) { upload.canceled = true; - MatrixClientPeg.get().cancelUpload(upload.promise); + matrixClient.cancelUpload(upload.promise); dis.dispatch({action: Action.UploadCanceled, upload}); } } @@ -623,11 +622,11 @@ export default class ContentMessages { return true; } - private ensureMediaConfigFetched() { + private ensureMediaConfigFetched(matrixClient: MatrixClient) { if (this.mediaConfig !== null) return; console.log("[Media Config] Fetching"); - return MatrixClientPeg.get().getMediaConfig().then((config) => { + return matrixClient.getMediaConfig().then((config) => { console.log("[Media Config] Fetched config:", config); return config; }).catch(() => { diff --git a/src/components/structures/UploadBar.tsx b/src/components/structures/UploadBar.tsx index e19e312f58..269c615698 100644 --- a/src/components/structures/UploadBar.tsx +++ b/src/components/structures/UploadBar.tsx @@ -26,6 +26,7 @@ import ProgressBar from "../views/elements/ProgressBar"; import AccessibleButton from "../views/elements/AccessibleButton"; import { IUpload } from "../../models/IUpload"; import {replaceableComponent} from "../../utils/replaceableComponent"; +import MatrixClientContext from "../../contexts/MatrixClientContext"; interface IProps { room: Room; @@ -38,6 +39,8 @@ interface IState { @replaceableComponent("structures.UploadBar") export default class UploadBar extends React.Component { + static contextType = MatrixClientContext; + private dispatcherRef: string; private mounted: boolean; @@ -82,7 +85,7 @@ export default class UploadBar extends React.Component { private onCancelClick = (ev) => { ev.preventDefault(); - ContentMessages.sharedInstance().cancelUpload(this.state.currentUpload.promise); + ContentMessages.sharedInstance().cancelUpload(this.state.currentUpload.promise, this.context); }; render() { From ba7604fd44679d020335160ed5f30f20f294184b Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 20 May 2021 13:24:19 +0100 Subject: [PATCH 036/221] fix types around sending stickers --- src/components/structures/RoomView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index c0f3c59457..1595bc6c53 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -1196,7 +1196,7 @@ export default class RoomView extends React.Component { }); }; - private injectSticker(url, info, text) { + private injectSticker(url: string, info: object, text: string) { if (this.context.isGuest()) { dis.dispatch({action: 'require_registration'}); return; From 563e6433b9eec30ec404abed1cbfcf2bb2279119 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 20 May 2021 15:47:11 +0100 Subject: [PATCH 037/221] Don't store blurhash in state, its immutable and pointless --- src/components/views/messages/MImageBody.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/components/views/messages/MImageBody.js b/src/components/views/messages/MImageBody.js index 14043b95f3..34320f1b31 100644 --- a/src/components/views/messages/MImageBody.js +++ b/src/components/views/messages/MImageBody.js @@ -60,7 +60,6 @@ export default class MImageBody extends React.Component { this.onClick = this.onClick.bind(this); this._isGif = this._isGif.bind(this); - const imageInfo = this.props.mxEvent.getContent().info; this.state = { decryptedUrl: null, @@ -72,7 +71,6 @@ export default class MImageBody extends React.Component { loadedImageDimensions: null, hover: false, showImage: SettingsStore.getValue("showImages"), - blurhash: imageInfo ? imageInfo['xyz.amorgan.blurhash'] : null, // TODO: Use `blurhash` when MSC2448 lands. }; this._image = createRef(); @@ -442,8 +440,9 @@ export default class MImageBody extends React.Component { // Overidden by MStickerBody getPlaceholder(width, height) { - if (!this.state.blurhash) return null; - return ; + const blurhash = this.props.mxEvent.getContent().info['xyz.amorgan.blurhash']; + if (!blurhash) return null; + return ; } // Overidden by MStickerBody From 8368b864406453d35c3d47b096a2febd9a83273c Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 21 May 2021 14:52:27 +0100 Subject: [PATCH 038/221] Wire up local blurhash encoding --- src/ContentMessages.tsx | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/src/ContentMessages.tsx b/src/ContentMessages.tsx index ba84b30733..9d9709a04d 100644 --- a/src/ContentMessages.tsx +++ b/src/ContentMessages.tsx @@ -17,6 +17,8 @@ limitations under the License. */ import React from "react"; +import { encode } from "blurhash"; + import dis from './dispatcher/dispatcher'; import {MatrixClient} from "matrix-js-sdk/src/client"; import * as sdk from './index'; @@ -47,6 +49,10 @@ const MAX_HEIGHT = 600; // 5669 px (x-axis) , 5669 px (y-axis) , per metre const PHYS_HIDPI = [0x00, 0x00, 0x16, 0x25, 0x00, 0x00, 0x16, 0x25, 0x01]; +const BLURHASH_FIELD = "xyz.amorgan.blurhash"; // MSC 2448 +const BLURHASH_X_COMPONENTS = 8; +const BLURHASH_Y_COMPONENTS = 8; + export class UploadCanceledError extends Error {} type ThumbnailableElement = HTMLImageElement | HTMLVideoElement; @@ -77,6 +83,7 @@ interface IThumbnail { }; w: number; h: number; + [BLURHASH_FIELD]: string; }; thumbnail: Blob; } @@ -124,7 +131,16 @@ function createThumbnail( const canvas = document.createElement("canvas"); canvas.width = targetWidth; canvas.height = targetHeight; - canvas.getContext("2d").drawImage(element, 0, 0, targetWidth, targetHeight); + const context = canvas.getContext("2d"); + context.drawImage(element, 0, 0, targetWidth, targetHeight); + const imageData = context.getImageData(0, 0, targetWidth, targetHeight); + const blurhash = encode( + imageData.data, + imageData.width, + imageData.height, + BLURHASH_X_COMPONENTS, + BLURHASH_Y_COMPONENTS, + ); canvas.toBlob(function(thumbnail) { resolve({ info: { @@ -136,8 +152,9 @@ function createThumbnail( }, w: inputWidth, h: inputHeight, + [BLURHASH_FIELD]: blurhash, }, - thumbnail: thumbnail, + thumbnail, }); }, mimeType); }); @@ -338,7 +355,6 @@ function uploadFile(matrixClient: MatrixClient, roomId: string, file: File | Blo if (file.type) { encryptInfo.mimetype = file.type; } - // TODO: Blurhash for encrypted media? return {"file": encryptInfo}; }); (prom as IAbortablePromise).abort = () => { @@ -349,15 +365,11 @@ function uploadFile(matrixClient: MatrixClient, roomId: string, file: File | Blo } else { const basePromise = matrixClient.uploadContent(file, { progressHandler: progressHandler, - onlyContentUri: false, }); - const promise1 = basePromise.then(function(body) { + const promise1 = basePromise.then(function(url) { if (canceled) throw new UploadCanceledError(); // If the attachment isn't encrypted then include the URL directly. - return { - "url": body.content_uri, - "blurhash": body["xyz.amorgan.blurhash"], // TODO: Use `body.blurhash` when MSC2448 lands - }; + return { url }; }); promise1.abort = () => { canceled = true; @@ -565,7 +577,6 @@ export default class ContentMessages { return upload.promise.then(function(result) { content.file = result.file; content.url = result.url; - content.info['xyz.amorgan.blurhash'] = result.blurhash; // TODO: Use `blurhash` when MSC2448 lands }); }).then(() => { // Await previous message being sent into the room From 6672cbcf8029f93a3e5b89724ec4346404fb5de0 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 21 May 2021 15:29:17 +0100 Subject: [PATCH 039/221] fix typo --- src/ContentMessages.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ContentMessages.tsx b/src/ContentMessages.tsx index 9d9709a04d..626e5cbf69 100644 --- a/src/ContentMessages.tsx +++ b/src/ContentMessages.tsx @@ -49,7 +49,7 @@ const MAX_HEIGHT = 600; // 5669 px (x-axis) , 5669 px (y-axis) , per metre const PHYS_HIDPI = [0x00, 0x00, 0x16, 0x25, 0x00, 0x00, 0x16, 0x25, 0x01]; -const BLURHASH_FIELD = "xyz.amorgan.blurhash"; // MSC 2448 +const BLURHASH_FIELD = "xyz.amorgan.blurhash"; // MSC2448 const BLURHASH_X_COMPONENTS = 8; const BLURHASH_Y_COMPONENTS = 8; From 3ed421dacf5e9df2f346dcfd549b69555bf6707b Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 21 May 2021 21:04:22 +0100 Subject: [PATCH 040/221] Tweak blurhash size --- src/ContentMessages.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ContentMessages.tsx b/src/ContentMessages.tsx index 626e5cbf69..0914bff41a 100644 --- a/src/ContentMessages.tsx +++ b/src/ContentMessages.tsx @@ -50,8 +50,8 @@ const MAX_HEIGHT = 600; const PHYS_HIDPI = [0x00, 0x00, 0x16, 0x25, 0x00, 0x00, 0x16, 0x25, 0x01]; const BLURHASH_FIELD = "xyz.amorgan.blurhash"; // MSC2448 -const BLURHASH_X_COMPONENTS = 8; -const BLURHASH_Y_COMPONENTS = 8; +const BLURHASH_X_COMPONENTS = 6; +const BLURHASH_Y_COMPONENTS = 6; export class UploadCanceledError extends Error {} From 254697644e956e032451b11c500de46d49410564 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 21 May 2021 21:04:36 +0100 Subject: [PATCH 041/221] Fix video thumbnailer --- src/ContentMessages.tsx | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/ContentMessages.tsx b/src/ContentMessages.tsx index 0914bff41a..98a4554a07 100644 --- a/src/ContentMessages.tsx +++ b/src/ContentMessages.tsx @@ -237,7 +237,8 @@ function infoForImageFile(matrixClient, roomId, imageFile) { } /** - * Load a file into a newly created video element. + * Load a file into a newly created video element and pull some strings + * in an attempt to guarantee the first frame will be showing. * * @param {File} videoFile The file to load in an video element. * @return {Promise} A promise that resolves with the video image element. @@ -246,20 +247,25 @@ function loadVideoElement(videoFile): Promise { return new Promise((resolve, reject) => { // Load the file into an html element const video = document.createElement("video"); + video.preload = "metadata"; + video.playsInline = true; + video.muted = true; const reader = new FileReader(); reader.onload = function(ev) { - video.src = ev.target.result as string; - - // Once ready, returns its size // Wait until we have enough data to thumbnail the first frame. - video.onloadeddata = function() { + video.onloadeddata = async function() { resolve(video); + video.pause(); }; video.onerror = function(e) { reject(e); }; + + video.src = ev.target.result as string; + video.load(); + video.play(); }; reader.onerror = function(e) { reject(e); From dbca9b4625fbde6c32edef7522f2a2668b484829 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 21 May 2021 21:19:41 +0100 Subject: [PATCH 042/221] use const for blurhash field --- src/ContentMessages.tsx | 2 +- src/components/views/messages/MImageBody.js | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/ContentMessages.tsx b/src/ContentMessages.tsx index 98a4554a07..44bb3edca9 100644 --- a/src/ContentMessages.tsx +++ b/src/ContentMessages.tsx @@ -49,7 +49,7 @@ const MAX_HEIGHT = 600; // 5669 px (x-axis) , 5669 px (y-axis) , per metre const PHYS_HIDPI = [0x00, 0x00, 0x16, 0x25, 0x00, 0x00, 0x16, 0x25, 0x01]; -const BLURHASH_FIELD = "xyz.amorgan.blurhash"; // MSC2448 +export const BLURHASH_FIELD = "xyz.amorgan.blurhash"; // MSC2448 const BLURHASH_X_COMPONENTS = 6; const BLURHASH_Y_COMPONENTS = 6; diff --git a/src/components/views/messages/MImageBody.js b/src/components/views/messages/MImageBody.js index 34320f1b31..6062375243 100644 --- a/src/components/views/messages/MImageBody.js +++ b/src/components/views/messages/MImageBody.js @@ -30,6 +30,7 @@ import InlineSpinner from '../elements/InlineSpinner'; import {replaceableComponent} from "../../../utils/replaceableComponent"; import {mediaFromContent} from "../../../customisations/Media"; import BlurhashPlaceholder from "../elements/BlurhashPlaceholder"; +import {BLURHASH_FIELD} from "../../../ContentMessages"; @replaceableComponent("views.messages.MImageBody") export default class MImageBody extends React.Component { @@ -440,7 +441,7 @@ export default class MImageBody extends React.Component { // Overidden by MStickerBody getPlaceholder(width, height) { - const blurhash = this.props.mxEvent.getContent().info['xyz.amorgan.blurhash']; + const blurhash = this.props.mxEvent.getContent().info[BLURHASH_FIELD]; if (!blurhash) return null; return ; } From 3a2e5389f6ccad7fc1ed3e039544d3fe596a104a Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 21 May 2021 21:20:00 +0100 Subject: [PATCH 043/221] Support blurhash for video posters --- src/components/views/messages/MVideoBody.tsx | 67 +++++++++++++++++--- 1 file changed, 58 insertions(+), 9 deletions(-) diff --git a/src/components/views/messages/MVideoBody.tsx b/src/components/views/messages/MVideoBody.tsx index 2efdce506e..44b246f0ad 100644 --- a/src/components/views/messages/MVideoBody.tsx +++ b/src/components/views/messages/MVideoBody.tsx @@ -16,6 +16,8 @@ limitations under the License. */ import React from 'react'; +import { decode } from "blurhash"; + import MFileBody from './MFileBody'; import { decryptFile } from '../../../utils/DecryptFile'; import { _t } from '../../../languageHandler'; @@ -23,6 +25,7 @@ import SettingsStore from "../../../settings/SettingsStore"; import InlineSpinner from '../elements/InlineSpinner'; import {replaceableComponent} from "../../../utils/replaceableComponent"; import {mediaFromContent} from "../../../customisations/Media"; +import {BLURHASH_FIELD} from "../../../ContentMessages"; interface IProps { /* the MatrixEvent to show */ @@ -37,6 +40,8 @@ interface IState { decryptedBlob: Blob|null, error: any|null, fetchingData: boolean, + posterLoading: boolean; + blurhashUrl: string; } @replaceableComponent("views.messages.MVideoBody") @@ -51,10 +56,12 @@ export default class MVideoBody extends React.PureComponent { decryptedThumbnailUrl: null, decryptedBlob: null, error: null, + posterLoading: false, + blurhashUrl: null, } } - thumbScale(fullWidth: number, fullHeight: number, thumbWidth: number, thumbHeight: number) { + thumbScale(fullWidth: number, fullHeight: number, thumbWidth = 480, thumbHeight = 360) { if (!fullWidth || !fullHeight) { // Cannot calculate thumbnail height for image: missing w/h in metadata. We can't even // log this because it's spammy @@ -92,8 +99,11 @@ export default class MVideoBody extends React.PureComponent { private getThumbUrl(): string|null { const content = this.props.mxEvent.getContent(); const media = mediaFromContent(content); - if (media.isEncrypted) { + + if (media.isEncrypted && this.state.decryptedThumbnailUrl) { return this.state.decryptedThumbnailUrl; + } else if (this.state.posterLoading) { + return this.state.blurhashUrl; } else if (media.hasThumbnail) { return media.thumbnailHttp; } else { @@ -101,18 +111,57 @@ export default class MVideoBody extends React.PureComponent { } } + private loadBlurhash() { + const info = this.props.mxEvent.getContent()?.info; + if (!info[BLURHASH_FIELD]) return; + + const canvas = document.createElement("canvas"); + + let width = info.w; + let height = info.h; + const scale = this.thumbScale(info.w, info.h); + if (scale) { + width = Math.floor(info.w * scale); + height = Math.floor(info.h * scale); + } + + canvas.width = width; + canvas.height = height; + + const pixels = decode(info[BLURHASH_FIELD], width, height); + const ctx = canvas.getContext("2d"); + const imgData = ctx.createImageData(width, height); + imgData.data.set(pixels); + ctx.putImageData(imgData, 0, 0); + + this.setState({ + blurhashUrl: canvas.toDataURL(), + posterLoading: true, + }); + + const content = this.props.mxEvent.getContent(); + const media = mediaFromContent(content); + if (media.hasThumbnail) { + const image = new Image(); + image.onload = () => { + this.setState({ posterLoading: false }); + }; + image.src = media.thumbnailHttp; + } + } + async componentDidMount() { const autoplay = SettingsStore.getValue("autoplayGifsAndVideos") as boolean; const content = this.props.mxEvent.getContent(); + this.loadBlurhash(); + if (content.file !== undefined && this.state.decryptedUrl === null) { let thumbnailPromise = Promise.resolve(null); - if (content.info && content.info.thumbnail_file) { - thumbnailPromise = decryptFile( - content.info.thumbnail_file, - ).then(function(blob) { - return URL.createObjectURL(blob); - }); + if (content?.info?.thumbnail_file) { + thumbnailPromise = decryptFile(content.info.thumbnail_file) + .then(blob => URL.createObjectURL(blob)); } + try { const thumbnailUrl = await thumbnailPromise; if (autoplay) { @@ -218,7 +267,7 @@ export default class MVideoBody extends React.PureComponent { let poster = null; let preload = "metadata"; if (content.info) { - const scale = this.thumbScale(content.info.w, content.info.h, 480, 360); + const scale = this.thumbScale(content.info.w, content.info.h); if (scale) { width = Math.floor(content.info.w * scale); height = Math.floor(content.info.h * scale); From 9c599b567df828ed5e888c846aa080b93c535ae2 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 21 May 2021 21:39:38 +0100 Subject: [PATCH 044/221] Fix alignment of image blurhashes and interplay with spinners --- src/components/views/messages/MImageBody.js | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/components/views/messages/MImageBody.js b/src/components/views/messages/MImageBody.js index 6062375243..8d3f1c4c10 100644 --- a/src/components/views/messages/MImageBody.js +++ b/src/components/views/messages/MImageBody.js @@ -372,10 +372,7 @@ export default class MImageBody extends React.Component { let placeholder = null; let gifLabel = null; - // e2e image hasn't been decrypted yet - if (content.file !== undefined && this.state.decryptedUrl === null) { - placeholder = ; - } else if (!this.state.imgLoaded) { + if (!this.state.imgLoaded) { placeholder = this.getPlaceholder(maxWidth, maxHeight); } @@ -414,9 +411,7 @@ export default class MImageBody extends React.Component { // Constrain width here so that spinner appears central to the loaded thumbnail maxWidth: infoWidth + "px", }}> -
- { placeholder } -
+ { placeholder }
} @@ -442,8 +437,10 @@ export default class MImageBody extends React.Component { // Overidden by MStickerBody getPlaceholder(width, height) { const blurhash = this.props.mxEvent.getContent().info[BLURHASH_FIELD]; - if (!blurhash) return null; - return ; + if (blurhash) return ; + return
+ +
; } // Overidden by MStickerBody From 3f9b6caacd76608cb1d22403442460223ab7090a Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 21 May 2021 21:46:18 +0100 Subject: [PATCH 045/221] fix missing rounded corners on blurhash placeholder --- res/css/views/messages/_MImageBody.scss | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/res/css/views/messages/_MImageBody.scss b/res/css/views/messages/_MImageBody.scss index 1c773c2f06..d04b3d5c93 100644 --- a/res/css/views/messages/_MImageBody.scss +++ b/res/css/views/messages/_MImageBody.scss @@ -14,6 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ +$timelineImageBorderRadius: 4px; + .mx_MImageBody { display: block; margin-right: 34px; @@ -25,7 +27,11 @@ limitations under the License. height: 100%; left: 0; top: 0; - border-radius: 4px; + border-radius: $timelineImageBorderRadius; + + > canvas { + border-radius: $timelineImageBorderRadius; + } } .mx_MImageBody_thumbnail_container { From 44e14a7d4305639b928e3a78a0b6f368f33f8424 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 24 May 2021 10:06:17 +0100 Subject: [PATCH 046/221] Support blurhashes on stickers --- src/components/views/messages/MStickerBody.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/views/messages/MStickerBody.js b/src/components/views/messages/MStickerBody.js index 54eb7649b4..d3dea2bfa4 100644 --- a/src/components/views/messages/MStickerBody.js +++ b/src/components/views/messages/MStickerBody.js @@ -18,6 +18,7 @@ import React from 'react'; import MImageBody from './MImageBody'; import * as sdk from '../../../index'; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import {BLURHASH_FIELD} from "../../../ContentMessages"; @replaceableComponent("views.messages.MStickerBody") export default class MStickerBody extends MImageBody { @@ -41,7 +42,8 @@ export default class MStickerBody extends MImageBody { // Placeholder to show in place of the sticker image if // img onLoad hasn't fired yet. - getPlaceholder() { + getPlaceholder(width, height) { + if (this.props.mxEvent.getContent().info[BLURHASH_FIELD]) return super.getPlaceholder(width, height); const TintableSVG = sdk.getComponent('elements.TintableSvg'); return ; } From ab3ccecc967208f7847669d3ba1678ea9f42ed9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 17 Jun 2021 15:52:55 +0200 Subject: [PATCH 047/221] Use UIStore MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/voip/CallPreview.tsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/components/views/voip/CallPreview.tsx b/src/components/views/voip/CallPreview.tsx index 730910e0e2..13c272bebb 100644 --- a/src/components/views/voip/CallPreview.tsx +++ b/src/components/views/voip/CallPreview.tsx @@ -27,6 +27,7 @@ import SettingsStore from "../../../settings/SettingsStore"; import { CallEvent, CallState, MatrixCall } from 'matrix-js-sdk/src/webrtc/call'; import { MatrixClientPeg } from '../../../MatrixClientPeg'; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import UIStore from '../../../stores/UIStore'; const PIP_VIEW_WIDTH = 336; const PIP_VIEW_HEIGHT = 232; @@ -118,7 +119,7 @@ export default class CallPreview extends React.Component { roomId, primaryCall: primaryCall, secondaryCall: secondaryCalls[0], - translationX: window.innerWidth - DEFAULT_X_OFFSET - PIP_VIEW_WIDTH, + translationX: UIStore.instance.windowWidth - DEFAULT_X_OFFSET - PIP_VIEW_WIDTH, translationY: DEFAULT_Y_OFFSET, moving: false, }; @@ -164,8 +165,8 @@ export default class CallPreview extends React.Component { let outTranslationY; // Avoid overflow on the x axis - if (inTranslationX + width >= window.innerWidth) { - outTranslationX = window.innerWidth - width; + if (inTranslationX + width >= UIStore.instance.windowWidth) { + outTranslationX = UIStore.instance.windowWidth - width; } else if (inTranslationX <= 0) { outTranslationX = 0; } else { @@ -173,8 +174,8 @@ export default class CallPreview extends React.Component { } // Avoid overflow on the y axis - if (inTranslationY + height >= window.innerHeight) { - outTranslationY = window.innerHeight - height; + if (inTranslationY + height >= UIStore.instance.windowHeight) { + outTranslationY = UIStore.instance.windowHeight - height; } else if (inTranslationY <= 0) { outTranslationY = 0; } else { From 88ba24f36219a59e66efb85c187c1d615c54e120 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 17 Jun 2021 18:34:58 +0200 Subject: [PATCH 048/221] Fix bugs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/voip/_CallContainer.scss | 4 ++-- res/css/views/voip/_VideoFeed.scss | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/res/css/views/voip/_CallContainer.scss b/res/css/views/voip/_CallContainer.scss index 8262075559..e51f20ff99 100644 --- a/res/css/views/voip/_CallContainer.scss +++ b/res/css/views/voip/_CallContainer.scss @@ -30,8 +30,8 @@ limitations under the License. pointer-events: initial; // restore pointer events so the user can leave/interact cursor: pointer; - .mx_CallView_video { - width: 350px; + .mx_VideoFeed_remote.mx_VideoFeed_voice { + min-height: 150px; } .mx_VideoFeed_local { diff --git a/res/css/views/voip/_VideoFeed.scss b/res/css/views/voip/_VideoFeed.scss index 7d85ac264e..4a3fbdf597 100644 --- a/res/css/views/voip/_VideoFeed.scss +++ b/res/css/views/voip/_VideoFeed.scss @@ -15,8 +15,6 @@ limitations under the License. */ .mx_VideoFeed_voice { - // We don't want to collide with the call controls that have 52px of height - padding-bottom: 52px; background-color: $inverted-bg-color; } From 946317ddf87bdb0178a7bf92824204e00a27166f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 18 Jun 2021 19:04:55 +0200 Subject: [PATCH 049/221] Move moving out of state MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/voip/CallPreview.tsx | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/components/views/voip/CallPreview.tsx b/src/components/views/voip/CallPreview.tsx index 13c272bebb..865a3f34af 100644 --- a/src/components/views/voip/CallPreview.tsx +++ b/src/components/views/voip/CallPreview.tsx @@ -60,9 +60,6 @@ interface IState { // Position of the CallPreview translationX: number; translationY: number; - - // True if the CallPreview is being dragged - moving: boolean; } // Splits a list of calls into one 'primary' one and a list @@ -121,7 +118,6 @@ export default class CallPreview extends React.Component { secondaryCall: secondaryCalls[0], translationX: UIStore.instance.windowWidth - DEFAULT_X_OFFSET - PIP_VIEW_WIDTH, translationY: DEFAULT_Y_OFFSET, - moving: false, }; } @@ -129,6 +125,7 @@ export default class CallPreview extends React.Component { private initX = 0; private initY = 0; + private moving = false; public componentDidMount() { CallHandler.sharedInstance().addListener(CallHandlerEvent.CallChangeRoom, this.updateCalls); @@ -240,14 +237,14 @@ export default class CallPreview extends React.Component { event.preventDefault(); event.stopPropagation(); - this.setState({moving: true}); + this.moving = true; this.initX = event.pageX - this.state.translationX; this.initY = event.pageY - this.state.translationY; }; private onMoving = (event: React.MouseEvent | MouseEvent) => { - if (!this.state.moving) return; + if (!this.moving) return; event.preventDefault(); event.stopPropagation(); @@ -256,7 +253,7 @@ export default class CallPreview extends React.Component { }; private onEndMoving = () => { - this.setState({moving: false}); + this.moving = false; }; public render() { From 61929e3fc2857140e12f7405cb5c3188d7327cf5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 18 Jun 2021 19:43:19 +0200 Subject: [PATCH 050/221] Implement snapping MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/voip/CallPreview.tsx | 60 +++++++++++++++++++---- 1 file changed, 50 insertions(+), 10 deletions(-) diff --git a/src/components/views/voip/CallPreview.tsx b/src/components/views/voip/CallPreview.tsx index 865a3f34af..e5a0487a4a 100644 --- a/src/components/views/voip/CallPreview.tsx +++ b/src/components/views/voip/CallPreview.tsx @@ -32,8 +32,12 @@ import UIStore from '../../../stores/UIStore'; const PIP_VIEW_WIDTH = 336; const PIP_VIEW_HEIGHT = 232; -const DEFAULT_X_OFFSET = 16; -const DEFAULT_Y_OFFSET = 48; +const PADDING = { + top: 58, + bottom: 58, + left: 76, + right: 8, +} const SHOW_CALL_IN_STATES = [ CallState.Connected, @@ -44,6 +48,7 @@ const SHOW_CALL_IN_STATES = [ CallState.WaitLocalMedia, ]; + interface IProps { } @@ -116,8 +121,8 @@ export default class CallPreview extends React.Component { roomId, primaryCall: primaryCall, secondaryCall: secondaryCalls[0], - translationX: UIStore.instance.windowWidth - DEFAULT_X_OFFSET - PIP_VIEW_WIDTH, - translationY: DEFAULT_Y_OFFSET, + translationX: UIStore.instance.windowWidth - PADDING.right - PIP_VIEW_WIDTH, + translationY: UIStore.instance.windowHeight - PADDING.bottom - PIP_VIEW_WIDTH, }; } @@ -132,7 +137,7 @@ export default class CallPreview extends React.Component { this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate); document.addEventListener("mousemove", this.onMoving); document.addEventListener("mouseup", this.onEndMoving); - window.addEventListener("resize", this.onWindowSizeChanged); + window.addEventListener("resize", this.snap); this.dispatcherRef = dis.register(this.onAction); MatrixClientPeg.get().on(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold); } @@ -142,7 +147,7 @@ export default class CallPreview extends React.Component { MatrixClientPeg.get().removeListener(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold); document.removeEventListener("mousemove", this.onMoving); document.removeEventListener("mouseup", this.onEndMoving); - window.removeEventListener("resize", this.onWindowSizeChanged); + window.removeEventListener("resize", this.snap); if (this.roomStoreToken) { this.roomStoreToken.remove(); } @@ -150,10 +155,6 @@ export default class CallPreview extends React.Component { SettingsStore.unwatchSetting(this.settingsWatcherRef); } - private onWindowSizeChanged = () => { - this.setTranslation(this.state.translationX, this.state.translationY); - }; - private setTranslation(inTranslationX: number, inTranslationY: number) { const width = this.callViewWrapper.current?.clientWidth || PIP_VIEW_WIDTH; const height = this.callViewWrapper.current?.clientHeight || PIP_VIEW_HEIGHT; @@ -185,6 +186,44 @@ export default class CallPreview extends React.Component { }); } + private snap = () => { + const translationX = this.state.translationX; + const translationY = this.state.translationY; + // We subtract the PiP size from the window size in order to calculate + // the position to snap to from the PiP center and not its top-left + // corner + const windowWidth = ( + UIStore.instance.windowWidth - + (this.callViewWrapper.current?.clientWidth || PIP_VIEW_WIDTH) + ); + const windowHeight = ( + UIStore.instance.windowHeight - + (this.callViewWrapper.current?.clientHeight || PIP_VIEW_HEIGHT) + ); + + if (translationX >= windowWidth / 2 && translationY >= windowHeight / 2) { + this.setState({ + translationX: windowWidth - PADDING.right, + translationY: windowHeight - PADDING.bottom, + }); + } else if (translationX >= windowWidth / 2 && translationY <= windowHeight / 2) { + this.setState({ + translationX: windowWidth - PADDING.right, + translationY: PADDING.top, + }); + } else if (translationX <= windowWidth / 2 && translationY >= windowHeight / 2) { + this.setState({ + translationX: PADDING.left, + translationY: windowHeight - PADDING.bottom, + }); + } else { + this.setState({ + translationX: PADDING.left, + translationY: PADDING.top, + }); + } + } + private onRoomViewStoreUpdate = (payload) => { if (RoomViewStore.getRoomId() === this.state.roomId) return; @@ -253,6 +292,7 @@ export default class CallPreview extends React.Component { }; private onEndMoving = () => { + this.snap(); this.moving = false; }; From 39ca2844bde3ecfecd9d4e6956654b047331d552 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 19 Jun 2021 10:10:27 +0200 Subject: [PATCH 051/221] Add AnimationUtils MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/utils/AnimationUtils.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 src/utils/AnimationUtils.ts diff --git a/src/utils/AnimationUtils.ts b/src/utils/AnimationUtils.ts new file mode 100644 index 0000000000..9654bdeb11 --- /dev/null +++ b/src/utils/AnimationUtils.ts @@ -0,0 +1,19 @@ +/* +Copyright 2021 Šimon Brandner + +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 lerp(start: number, end: number, amt: number) { + return (1 - amt) * start + amt * end; +} From c8cf23b87c1c649a4a03b89ea8dd61f26f20c7ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 19 Jun 2021 12:21:24 +0200 Subject: [PATCH 052/221] Implement LERP MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/voip/CallPreview.tsx | 96 +++++++++++++---------- 1 file changed, 54 insertions(+), 42 deletions(-) diff --git a/src/components/views/voip/CallPreview.tsx b/src/components/views/voip/CallPreview.tsx index e5a0487a4a..210569ed05 100644 --- a/src/components/views/voip/CallPreview.tsx +++ b/src/components/views/voip/CallPreview.tsx @@ -28,10 +28,14 @@ import { CallEvent, CallState, MatrixCall } from 'matrix-js-sdk/src/webrtc/call' import { MatrixClientPeg } from '../../../MatrixClientPeg'; import {replaceableComponent} from "../../../utils/replaceableComponent"; import UIStore from '../../../stores/UIStore'; +import { lerp } from '../../../utils/AnimationUtils'; const PIP_VIEW_WIDTH = 336; const PIP_VIEW_HEIGHT = 232; +const MOVING_AMT = 0.2; +const SNAPPING_AMT = 0.05; + const PADDING = { top: 58, bottom: 58, @@ -107,6 +111,12 @@ export default class CallPreview extends React.Component { private roomStoreToken: any; private dispatcherRef: string; private settingsWatcherRef: string; + private callViewWrapper = createRef(); + private initX = 0; + private initY = 0; + private desiredTranslationX = UIStore.instance.windowWidth - PADDING.right - PIP_VIEW_WIDTH; + private desiredTranslationY = UIStore.instance.windowHeight - PADDING.bottom - PIP_VIEW_WIDTH; + private moving = false; constructor(props: IProps) { super(props); @@ -126,12 +136,6 @@ export default class CallPreview extends React.Component { }; } - private callViewWrapper = createRef(); - - private initX = 0; - private initY = 0; - private moving = false; - public componentDidMount() { CallHandler.sharedInstance().addListener(CallHandlerEvent.CallChangeRoom, this.updateCalls); this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate); @@ -155,40 +159,52 @@ export default class CallPreview extends React.Component { SettingsStore.unwatchSetting(this.settingsWatcherRef); } + private animationCallback = () => { + // If the PiP isn't being dragged and there is only a tiny difference in + // the desiredTranslation and translation, quit the animationCallback + // loop. If that is the case, it means the PiP has snapped into its + // position and there is nothing to do. Not doing this would cause an + // infinite loop + if ( + !this.moving && + Math.abs(this.state.translationX - this.desiredTranslationX) <= 1 && + Math.abs(this.state.translationY - this.desiredTranslationY) <= 1 + ) return; + + const amt = this.moving ? MOVING_AMT : SNAPPING_AMT; + this.setState({ + translationX: lerp(this.state.translationX, this.desiredTranslationX, amt), + translationY: lerp(this.state.translationY, this.desiredTranslationY, amt), + }); + requestAnimationFrame(this.animationCallback); + } + private setTranslation(inTranslationX: number, inTranslationY: number) { const width = this.callViewWrapper.current?.clientWidth || PIP_VIEW_WIDTH; const height = this.callViewWrapper.current?.clientHeight || PIP_VIEW_HEIGHT; - let outTranslationX; - let outTranslationY; - // Avoid overflow on the x axis if (inTranslationX + width >= UIStore.instance.windowWidth) { - outTranslationX = UIStore.instance.windowWidth - width; + this.desiredTranslationX = UIStore.instance.windowWidth - width; } else if (inTranslationX <= 0) { - outTranslationX = 0; + this.desiredTranslationX = 0; } else { - outTranslationX = inTranslationX; + this.desiredTranslationX = inTranslationX; } // Avoid overflow on the y axis if (inTranslationY + height >= UIStore.instance.windowHeight) { - outTranslationY = UIStore.instance.windowHeight - height; + this.desiredTranslationY = UIStore.instance.windowHeight - height; } else if (inTranslationY <= 0) { - outTranslationY = 0; + this.desiredTranslationY = 0; } else { - outTranslationY = inTranslationY; + this.desiredTranslationY = inTranslationY; } - - this.setState({ - translationX: outTranslationX, - translationY: outTranslationY, - }); } private snap = () => { - const translationX = this.state.translationX; - const translationY = this.state.translationY; + const translationX = this.desiredTranslationX; + const translationY = this.desiredTranslationY; // We subtract the PiP size from the window size in order to calculate // the position to snap to from the PiP center and not its top-left // corner @@ -202,26 +218,22 @@ export default class CallPreview extends React.Component { ); if (translationX >= windowWidth / 2 && translationY >= windowHeight / 2) { - this.setState({ - translationX: windowWidth - PADDING.right, - translationY: windowHeight - PADDING.bottom, - }); + this.desiredTranslationX = windowWidth - PADDING.right; + this.desiredTranslationY = windowHeight - PADDING.bottom; } else if (translationX >= windowWidth / 2 && translationY <= windowHeight / 2) { - this.setState({ - translationX: windowWidth - PADDING.right, - translationY: PADDING.top, - }); + this.desiredTranslationX = windowWidth - PADDING.right; + this.desiredTranslationY = PADDING.top; } else if (translationX <= windowWidth / 2 && translationY >= windowHeight / 2) { - this.setState({ - translationX: PADDING.left, - translationY: windowHeight - PADDING.bottom, - }); + this.desiredTranslationX = PADDING.left; + this.desiredTranslationY = windowHeight - PADDING.bottom; } else { - this.setState({ - translationX: PADDING.left, - translationY: PADDING.top, - }); + this.desiredTranslationX = PADDING.left; + this.desiredTranslationY = PADDING.top; } + + // We start animating here because we want the PiP to move when we're + // resizing the window + requestAnimationFrame(this.animationCallback); } private onRoomViewStoreUpdate = (payload) => { @@ -277,9 +289,9 @@ export default class CallPreview extends React.Component { event.stopPropagation(); this.moving = true; - - this.initX = event.pageX - this.state.translationX; - this.initY = event.pageY - this.state.translationY; + this.initX = event.pageX - this.desiredTranslationX; + this.initY = event.pageY - this.desiredTranslationY; + requestAnimationFrame(this.animationCallback); }; private onMoving = (event: React.MouseEvent | MouseEvent) => { @@ -292,8 +304,8 @@ export default class CallPreview extends React.Component { }; private onEndMoving = () => { - this.snap(); this.moving = false; + this.snap(); }; public render() { From 50127da227f09aaad64e7f435e8ffe31ad86e1cb Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 21 Jun 2021 15:11:41 -0600 Subject: [PATCH 053/221] Replace MAudioBody with a voice message body as a template --- src/components/views/messages/MAudioBody.js | 112 ------------------- src/components/views/messages/MAudioBody.tsx | 111 ++++++++++++++++++ 2 files changed, 111 insertions(+), 112 deletions(-) delete mode 100644 src/components/views/messages/MAudioBody.js create mode 100644 src/components/views/messages/MAudioBody.tsx diff --git a/src/components/views/messages/MAudioBody.js b/src/components/views/messages/MAudioBody.js deleted file mode 100644 index 0d5e449fc0..0000000000 --- a/src/components/views/messages/MAudioBody.js +++ /dev/null @@ -1,112 +0,0 @@ -/* - Copyright 2016 OpenMarket Ltd - - 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. - */ - -import React from 'react'; -import MFileBody from './MFileBody'; - -import { decryptFile } from '../../../utils/DecryptFile'; -import { _t } from '../../../languageHandler'; -import InlineSpinner from '../elements/InlineSpinner'; -import {replaceableComponent} from "../../../utils/replaceableComponent"; -import {mediaFromContent} from "../../../customisations/Media"; - -@replaceableComponent("views.messages.MAudioBody") -export default class MAudioBody extends React.Component { - constructor(props) { - super(props); - this.state = { - playing: false, - decryptedUrl: null, - decryptedBlob: null, - error: null, - }; - } - onPlayToggle() { - this.setState({ - playing: !this.state.playing, - }); - } - - _getContentUrl() { - const media = mediaFromContent(this.props.mxEvent.getContent()); - if (media.isEncrypted) { - return this.state.decryptedUrl; - } else { - return media.srcHttp; - } - } - - componentDidMount() { - const content = this.props.mxEvent.getContent(); - if (content.file !== undefined && this.state.decryptedUrl === null) { - let decryptedBlob; - decryptFile(content.file).then(function(blob) { - decryptedBlob = blob; - return URL.createObjectURL(decryptedBlob); - }).then((url) => { - this.setState({ - decryptedUrl: url, - decryptedBlob: decryptedBlob, - }); - }, (err) => { - console.warn("Unable to decrypt attachment: ", err); - this.setState({ - error: err, - }); - }); - } - } - - componentWillUnmount() { - if (this.state.decryptedUrl) { - URL.revokeObjectURL(this.state.decryptedUrl); - } - } - - render() { - const content = this.props.mxEvent.getContent(); - - if (this.state.error !== null) { - return ( - - - { _t("Error decrypting audio") } - - ); - } - - if (content.file !== undefined && this.state.decryptedUrl === null) { - // Need to decrypt the attachment - // The attachment is decrypted in componentDidMount. - // For now add an img tag with a 16x16 spinner. - // Not sure how tall the audio player is so not sure how tall it should actually be. - return ( - - - - ); - } - - const contentUrl = this._getContentUrl(); - - return ( - - - ); - } -} diff --git a/src/components/views/messages/MAudioBody.tsx b/src/components/views/messages/MAudioBody.tsx new file mode 100644 index 0000000000..9e77bc0893 --- /dev/null +++ b/src/components/views/messages/MAudioBody.tsx @@ -0,0 +1,111 @@ +/* +Copyright 2021 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. +*/ + +import React from "react"; +import {MatrixEvent} from "matrix-js-sdk/src/models/event"; +import {replaceableComponent} from "../../../utils/replaceableComponent"; +import {Playback} from "../../../voice/Playback"; +import MFileBody from "./MFileBody"; +import InlineSpinner from '../elements/InlineSpinner'; +import {_t} from "../../../languageHandler"; +import {mediaFromContent} from "../../../customisations/Media"; +import {decryptFile} from "../../../utils/DecryptFile"; +import RecordingPlayback from "../voice_messages/RecordingPlayback"; +import {IMediaEventContent} from "../../../customisations/models/IMediaEventContent"; + +interface IProps { + mxEvent: MatrixEvent; +} + +interface IState { + error?: Error; + playback?: Playback; + decryptedBlob?: Blob; +} + +@replaceableComponent("views.messages.MAudioBody") +export default class MAudioBody extends React.PureComponent { + constructor(props: IProps) { + super(props); + + this.state = {}; + } + + public async componentDidMount() { + let buffer: ArrayBuffer; + const content: IMediaEventContent = this.props.mxEvent.getContent(); + const media = mediaFromContent(content); + if (media.isEncrypted) { + try { + const blob = await decryptFile(content.file); + buffer = await blob.arrayBuffer(); + this.setState({decryptedBlob: blob}); + } catch (e) { + this.setState({error: e}); + console.warn("Unable to decrypt voice message", e); + return; // stop processing the audio file + } + } else { + try { + buffer = await media.downloadSource().then(r => r.blob()).then(r => r.arrayBuffer()); + } catch (e) { + this.setState({error: e}); + console.warn("Unable to download voice message", e); + return; // stop processing the audio file + } + } + + const waveform = content?.["org.matrix.msc1767.audio"]?.waveform?.map(p => p / 1024); + + // We should have a buffer to work with now: let's set it up + const playback = new Playback(buffer, waveform); + this.setState({ playback }); + // Note: the RecordingPlayback component will handle preparing the Playback class for us. + } + + public componentWillUnmount() { + this.state.playback?.destroy(); + } + + public render() { + if (this.state.error) { + // TODO: @@TR: Verify error state + return ( + + + { _t("Error processing voice message") } + + ); + } + + if (!this.state.playback) { + // TODO: @@TR: Verify loading/decrypting state + return ( + + + + ); + } + + // At this point we should have a playable state + return ( + + + + + ) + } +} From 9f2eba4351a8ac4977e783c68811fb71a48a0586 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 21 Jun 2021 15:13:12 -0600 Subject: [PATCH 054/221] Fix class identifiers --- res/css/_components.scss | 1 + res/css/views/messages/_MAudioBody.scss | 19 +++++++++++++++++++ src/components/views/messages/MAudioBody.tsx | 16 +++++++--------- 3 files changed, 27 insertions(+), 9 deletions(-) create mode 100644 res/css/views/messages/_MAudioBody.scss diff --git a/res/css/_components.scss b/res/css/_components.scss index ec3af8655e..c997881138 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -156,6 +156,7 @@ @import "./views/messages/_CreateEvent.scss"; @import "./views/messages/_DateSeparator.scss"; @import "./views/messages/_EventTileBubble.scss"; +@import "./views/messages/_MAudioBody.scss"; @import "./views/messages/_MEmoteBody.scss"; @import "./views/messages/_MFileBody.scss"; @import "./views/messages/_MImageBody.scss"; diff --git a/res/css/views/messages/_MAudioBody.scss b/res/css/views/messages/_MAudioBody.scss new file mode 100644 index 0000000000..2c9b6f7030 --- /dev/null +++ b/res/css/views/messages/_MAudioBody.scss @@ -0,0 +1,19 @@ +/* +Copyright 2021 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. +*/ + +.mx_MAudioBody { + display: inline-block; // makes the playback controls magically line up +} diff --git a/src/components/views/messages/MAudioBody.tsx b/src/components/views/messages/MAudioBody.tsx index 9e77bc0893..8568899323 100644 --- a/src/components/views/messages/MAudioBody.tsx +++ b/src/components/views/messages/MAudioBody.tsx @@ -55,7 +55,7 @@ export default class MAudioBody extends React.PureComponent { this.setState({decryptedBlob: blob}); } catch (e) { this.setState({error: e}); - console.warn("Unable to decrypt voice message", e); + console.warn("Unable to decrypt audio message", e); return; // stop processing the audio file } } else { @@ -63,15 +63,13 @@ export default class MAudioBody extends React.PureComponent { buffer = await media.downloadSource().then(r => r.blob()).then(r => r.arrayBuffer()); } catch (e) { this.setState({error: e}); - console.warn("Unable to download voice message", e); + console.warn("Unable to download audio message", e); return; // stop processing the audio file } } - const waveform = content?.["org.matrix.msc1767.audio"]?.waveform?.map(p => p / 1024); - // We should have a buffer to work with now: let's set it up - const playback = new Playback(buffer, waveform); + const playback = new Playback(buffer); this.setState({ playback }); // Note: the RecordingPlayback component will handle preparing the Playback class for us. } @@ -84,9 +82,9 @@ export default class MAudioBody extends React.PureComponent { if (this.state.error) { // TODO: @@TR: Verify error state return ( - + - { _t("Error processing voice message") } + { _t("Error processing audio message") } ); } @@ -94,7 +92,7 @@ export default class MAudioBody extends React.PureComponent { if (!this.state.playback) { // TODO: @@TR: Verify loading/decrypting state return ( - + ); @@ -102,7 +100,7 @@ export default class MAudioBody extends React.PureComponent { // At this point we should have a playable state return ( - + From 470778cbb84f875314643fdbc3e484d2c66e21fc Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 21 Jun 2021 15:18:34 -0600 Subject: [PATCH 055/221] Move voice message components to audio-generic directory --- res/css/_components.scss | 6 +++--- .../_PlayPauseButton.scss | 0 .../_PlaybackContainer.scss | 0 .../{voice_messages => audio_messages}/_Waveform.scss | 0 .../views/{voice_messages => audio_messages}/Clock.tsx | 4 ++-- .../LiveRecordingClock.tsx | 6 +++--- .../LiveRecordingWaveform.tsx | 10 +++++----- .../PlayPauseButton.tsx | 10 +++++----- .../PlaybackClock.tsx | 8 ++++---- .../PlaybackWaveform.tsx | 10 +++++----- .../RecordingPlayback.tsx | 6 ++++-- .../{voice_messages => audio_messages}/Waveform.tsx | 4 ++-- src/components/views/messages/MAudioBody.tsx | 2 +- src/components/views/messages/MVoiceMessageBody.tsx | 2 +- src/components/views/rooms/VoiceRecordComposerTile.tsx | 6 +++--- 15 files changed, 38 insertions(+), 36 deletions(-) rename res/css/views/{voice_messages => audio_messages}/_PlayPauseButton.scss (100%) rename res/css/views/{voice_messages => audio_messages}/_PlaybackContainer.scss (100%) rename res/css/views/{voice_messages => audio_messages}/_Waveform.scss (100%) rename src/components/views/{voice_messages => audio_messages}/Clock.tsx (92%) rename src/components/views/{voice_messages => audio_messages}/LiveRecordingClock.tsx (84%) rename src/components/views/{voice_messages => audio_messages}/LiveRecordingWaveform.tsx (83%) rename src/components/views/{voice_messages => audio_messages}/PlayPauseButton.tsx (86%) rename src/components/views/{voice_messages => audio_messages}/PlaybackClock.tsx (90%) rename src/components/views/{voice_messages => audio_messages}/PlaybackWaveform.tsx (85%) rename src/components/views/{voice_messages => audio_messages}/RecordingPlayback.tsx (88%) rename src/components/views/{voice_messages => audio_messages}/Waveform.tsx (93%) diff --git a/res/css/_components.scss b/res/css/_components.scss index c997881138..cb921e251c 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -37,6 +37,9 @@ @import "./structures/_ViewSource.scss"; @import "./structures/auth/_CompleteSecurity.scss"; @import "./structures/auth/_Login.scss"; +@import "./views/audio_messages/_PlayPauseButton.scss"; +@import "./views/audio_messages/_PlaybackContainer.scss"; +@import "./views/audio_messages/_Waveform.scss"; @import "./views/auth/_AuthBody.scss"; @import "./views/auth/_AuthButtons.scss"; @import "./views/auth/_AuthFooter.scss"; @@ -254,9 +257,6 @@ @import "./views/toasts/_AnalyticsToast.scss"; @import "./views/toasts/_NonUrgentEchoFailureToast.scss"; @import "./views/verification/_VerificationShowSas.scss"; -@import "./views/voice_messages/_PlayPauseButton.scss"; -@import "./views/voice_messages/_PlaybackContainer.scss"; -@import "./views/voice_messages/_Waveform.scss"; @import "./views/voip/_CallContainer.scss"; @import "./views/voip/_CallView.scss"; @import "./views/voip/_CallViewForRoom.scss"; diff --git a/res/css/views/voice_messages/_PlayPauseButton.scss b/res/css/views/audio_messages/_PlayPauseButton.scss similarity index 100% rename from res/css/views/voice_messages/_PlayPauseButton.scss rename to res/css/views/audio_messages/_PlayPauseButton.scss diff --git a/res/css/views/voice_messages/_PlaybackContainer.scss b/res/css/views/audio_messages/_PlaybackContainer.scss similarity index 100% rename from res/css/views/voice_messages/_PlaybackContainer.scss rename to res/css/views/audio_messages/_PlaybackContainer.scss diff --git a/res/css/views/voice_messages/_Waveform.scss b/res/css/views/audio_messages/_Waveform.scss similarity index 100% rename from res/css/views/voice_messages/_Waveform.scss rename to res/css/views/audio_messages/_Waveform.scss diff --git a/src/components/views/voice_messages/Clock.tsx b/src/components/views/audio_messages/Clock.tsx similarity index 92% rename from src/components/views/voice_messages/Clock.tsx rename to src/components/views/audio_messages/Clock.tsx index 23e6762c52..19ca1cb348 100644 --- a/src/components/views/voice_messages/Clock.tsx +++ b/src/components/views/audio_messages/Clock.tsx @@ -15,7 +15,7 @@ limitations under the License. */ import React from "react"; -import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; interface IProps { seconds: number; @@ -28,7 +28,7 @@ interface IState { * Simply converts seconds into minutes and seconds. Note that hours will not be * displayed, making it possible to see "82:29". */ -@replaceableComponent("views.voice_messages.Clock") +@replaceableComponent("views.audio_messages.Clock") export default class Clock extends React.Component { public constructor(props) { super(props); diff --git a/src/components/views/voice_messages/LiveRecordingClock.tsx b/src/components/views/audio_messages/LiveRecordingClock.tsx similarity index 84% rename from src/components/views/voice_messages/LiveRecordingClock.tsx rename to src/components/views/audio_messages/LiveRecordingClock.tsx index b82539eb16..41909dc201 100644 --- a/src/components/views/voice_messages/LiveRecordingClock.tsx +++ b/src/components/views/audio_messages/LiveRecordingClock.tsx @@ -15,8 +15,8 @@ limitations under the License. */ import React from "react"; -import {IRecordingUpdate, VoiceRecording} from "../../../voice/VoiceRecording"; -import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { IRecordingUpdate, VoiceRecording } from "../../../voice/VoiceRecording"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; import Clock from "./Clock"; interface IProps { @@ -30,7 +30,7 @@ interface IState { /** * A clock for a live recording. */ -@replaceableComponent("views.voice_messages.LiveRecordingClock") +@replaceableComponent("views.audio_messages.LiveRecordingClock") export default class LiveRecordingClock extends React.PureComponent { public constructor(props) { super(props); diff --git a/src/components/views/voice_messages/LiveRecordingWaveform.tsx b/src/components/views/audio_messages/LiveRecordingWaveform.tsx similarity index 83% rename from src/components/views/voice_messages/LiveRecordingWaveform.tsx rename to src/components/views/audio_messages/LiveRecordingWaveform.tsx index aab89f6ab1..27d165e613 100644 --- a/src/components/views/voice_messages/LiveRecordingWaveform.tsx +++ b/src/components/views/audio_messages/LiveRecordingWaveform.tsx @@ -15,10 +15,10 @@ limitations under the License. */ import React from "react"; -import {IRecordingUpdate, RECORDING_PLAYBACK_SAMPLES, VoiceRecording} from "../../../voice/VoiceRecording"; -import {replaceableComponent} from "../../../utils/replaceableComponent"; -import {arrayFastResample, arraySeed} from "../../../utils/arrays"; -import {percentageOf} from "../../../utils/numbers"; +import { IRecordingUpdate, RECORDING_PLAYBACK_SAMPLES, VoiceRecording } from "../../../voice/VoiceRecording"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { arrayFastResample, arraySeed } from "../../../utils/arrays"; +import { percentageOf } from "../../../utils/numbers"; import Waveform from "./Waveform"; interface IProps { @@ -32,7 +32,7 @@ interface IState { /** * A waveform which shows the waveform of a live recording */ -@replaceableComponent("views.voice_messages.LiveRecordingWaveform") +@replaceableComponent("views.audio_messages.LiveRecordingWaveform") export default class LiveRecordingWaveform extends React.PureComponent { public constructor(props) { super(props); diff --git a/src/components/views/voice_messages/PlayPauseButton.tsx b/src/components/views/audio_messages/PlayPauseButton.tsx similarity index 86% rename from src/components/views/voice_messages/PlayPauseButton.tsx rename to src/components/views/audio_messages/PlayPauseButton.tsx index 1f87eb012d..399cb169bb 100644 --- a/src/components/views/voice_messages/PlayPauseButton.tsx +++ b/src/components/views/audio_messages/PlayPauseButton.tsx @@ -14,11 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, {ReactNode} from "react"; -import {replaceableComponent} from "../../../utils/replaceableComponent"; +import React, { ReactNode } from "react"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; -import {_t} from "../../../languageHandler"; -import {Playback, PlaybackState} from "../../../voice/Playback"; +import { _t } from "../../../languageHandler"; +import { Playback, PlaybackState } from "../../../voice/Playback"; import classNames from "classnames"; interface IProps { @@ -33,7 +33,7 @@ interface IProps { * Displays a play/pause button (activating the play/pause function of the recorder) * to be displayed in reference to a recording. */ -@replaceableComponent("views.voice_messages.PlayPauseButton") +@replaceableComponent("views.audio_messages.PlayPauseButton") export default class PlayPauseButton extends React.PureComponent { public constructor(props) { super(props); diff --git a/src/components/views/voice_messages/PlaybackClock.tsx b/src/components/views/audio_messages/PlaybackClock.tsx similarity index 90% rename from src/components/views/voice_messages/PlaybackClock.tsx rename to src/components/views/audio_messages/PlaybackClock.tsx index 2e8ec9a3e7..82108ae63c 100644 --- a/src/components/views/voice_messages/PlaybackClock.tsx +++ b/src/components/views/audio_messages/PlaybackClock.tsx @@ -15,10 +15,10 @@ limitations under the License. */ import React from "react"; -import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; import Clock from "./Clock"; -import {Playback, PlaybackState} from "../../../voice/Playback"; -import {UPDATE_EVENT} from "../../../stores/AsyncStore"; +import { Playback, PlaybackState } from "../../../voice/Playback"; +import { UPDATE_EVENT } from "../../../stores/AsyncStore"; interface IProps { playback: Playback; @@ -33,7 +33,7 @@ interface IState { /** * A clock for a playback of a recording. */ -@replaceableComponent("views.voice_messages.PlaybackClock") +@replaceableComponent("views.audio_messages.PlaybackClock") export default class PlaybackClock extends React.PureComponent { public constructor(props) { super(props); diff --git a/src/components/views/voice_messages/PlaybackWaveform.tsx b/src/components/views/audio_messages/PlaybackWaveform.tsx similarity index 85% rename from src/components/views/voice_messages/PlaybackWaveform.tsx rename to src/components/views/audio_messages/PlaybackWaveform.tsx index 2e9f163f5e..bb205e4222 100644 --- a/src/components/views/voice_messages/PlaybackWaveform.tsx +++ b/src/components/views/audio_messages/PlaybackWaveform.tsx @@ -15,11 +15,11 @@ limitations under the License. */ import React from "react"; -import {replaceableComponent} from "../../../utils/replaceableComponent"; -import {arraySeed, arrayTrimFill} from "../../../utils/arrays"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { arraySeed, arrayTrimFill } from "../../../utils/arrays"; import Waveform from "./Waveform"; -import {Playback, PLAYBACK_WAVEFORM_SAMPLES} from "../../../voice/Playback"; -import {percentageOf} from "../../../utils/numbers"; +import { Playback, PLAYBACK_WAVEFORM_SAMPLES } from "../../../voice/Playback"; +import { percentageOf } from "../../../utils/numbers"; interface IProps { playback: Playback; @@ -33,7 +33,7 @@ interface IState { /** * A waveform which shows the waveform of a previously recorded recording */ -@replaceableComponent("views.voice_messages.PlaybackWaveform") +@replaceableComponent("views.audio_messages.PlaybackWaveform") export default class PlaybackWaveform extends React.PureComponent { public constructor(props) { super(props); diff --git a/src/components/views/voice_messages/RecordingPlayback.tsx b/src/components/views/audio_messages/RecordingPlayback.tsx similarity index 88% rename from src/components/views/voice_messages/RecordingPlayback.tsx rename to src/components/views/audio_messages/RecordingPlayback.tsx index 776997cec2..32ce32805d 100644 --- a/src/components/views/voice_messages/RecordingPlayback.tsx +++ b/src/components/views/audio_messages/RecordingPlayback.tsx @@ -14,12 +14,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {Playback, PlaybackState} from "../../../voice/Playback"; +import { Playback, PlaybackState } from "../../../voice/Playback"; import React, {ReactNode} from "react"; -import {UPDATE_EVENT} from "../../../stores/AsyncStore"; +import { UPDATE_EVENT } from "../../../stores/AsyncStore"; import PlaybackWaveform from "./PlaybackWaveform"; import PlayPauseButton from "./PlayPauseButton"; import PlaybackClock from "./PlaybackClock"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; interface IProps { // Playback instance to render. Cannot change during component lifecycle: create @@ -31,6 +32,7 @@ interface IState { playbackPhase: PlaybackState; } +@replaceableComponent("views.audio_messages.RecordingPlayback") export default class RecordingPlayback extends React.PureComponent { constructor(props: IProps) { super(props); diff --git a/src/components/views/voice_messages/Waveform.tsx b/src/components/views/audio_messages/Waveform.tsx similarity index 93% rename from src/components/views/voice_messages/Waveform.tsx rename to src/components/views/audio_messages/Waveform.tsx index 840a5a12b3..dc7551025e 100644 --- a/src/components/views/voice_messages/Waveform.tsx +++ b/src/components/views/audio_messages/Waveform.tsx @@ -15,7 +15,7 @@ limitations under the License. */ import React from "react"; -import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; import classNames from "classnames"; interface IProps { @@ -34,7 +34,7 @@ interface IState { * For CSS purposes, a mx_Waveform_bar_100pct class is added when the bar should be * "filled", as a demonstration of the progress property. */ -@replaceableComponent("views.voice_messages.Waveform") +@replaceableComponent("views.audio_messages.Waveform") export default class Waveform extends React.PureComponent { public static defaultProps = { progress: 1, diff --git a/src/components/views/messages/MAudioBody.tsx b/src/components/views/messages/MAudioBody.tsx index 8568899323..ddcebd7a60 100644 --- a/src/components/views/messages/MAudioBody.tsx +++ b/src/components/views/messages/MAudioBody.tsx @@ -23,7 +23,7 @@ import InlineSpinner from '../elements/InlineSpinner'; import {_t} from "../../../languageHandler"; import {mediaFromContent} from "../../../customisations/Media"; import {decryptFile} from "../../../utils/DecryptFile"; -import RecordingPlayback from "../voice_messages/RecordingPlayback"; +import RecordingPlayback from "../audio_messages/RecordingPlayback"; import {IMediaEventContent} from "../../../customisations/models/IMediaEventContent"; interface IProps { diff --git a/src/components/views/messages/MVoiceMessageBody.tsx b/src/components/views/messages/MVoiceMessageBody.tsx index a7e3b1cd86..84cd6ce012 100644 --- a/src/components/views/messages/MVoiceMessageBody.tsx +++ b/src/components/views/messages/MVoiceMessageBody.tsx @@ -23,7 +23,7 @@ import InlineSpinner from '../elements/InlineSpinner'; import {_t} from "../../../languageHandler"; import {mediaFromContent} from "../../../customisations/Media"; import {decryptFile} from "../../../utils/DecryptFile"; -import RecordingPlayback from "../voice_messages/RecordingPlayback"; +import RecordingPlayback from "../audio_messages/RecordingPlayback"; import {IMediaEventContent} from "../../../customisations/models/IMediaEventContent"; interface IProps { diff --git a/src/components/views/rooms/VoiceRecordComposerTile.tsx b/src/components/views/rooms/VoiceRecordComposerTile.tsx index 122ba0ca0b..d8c481e332 100644 --- a/src/components/views/rooms/VoiceRecordComposerTile.tsx +++ b/src/components/views/rooms/VoiceRecordComposerTile.tsx @@ -21,12 +21,12 @@ import {RecordingState, VoiceRecording} from "../../../voice/VoiceRecording"; import {Room} from "matrix-js-sdk/src/models/room"; import {MatrixClientPeg} from "../../../MatrixClientPeg"; import classNames from "classnames"; -import LiveRecordingWaveform from "../voice_messages/LiveRecordingWaveform"; +import LiveRecordingWaveform from "../audio_messages/LiveRecordingWaveform"; import {replaceableComponent} from "../../../utils/replaceableComponent"; -import LiveRecordingClock from "../voice_messages/LiveRecordingClock"; +import LiveRecordingClock from "../audio_messages/LiveRecordingClock"; import {VoiceRecordingStore} from "../../../stores/VoiceRecordingStore"; import {UPDATE_EVENT} from "../../../stores/AsyncStore"; -import RecordingPlayback from "../voice_messages/RecordingPlayback"; +import RecordingPlayback from "../audio_messages/RecordingPlayback"; import {MsgType} from "matrix-js-sdk/src/@types/event"; import Modal from "../../../Modal"; import ErrorDialog from "../dialogs/ErrorDialog"; From dda60949c33d6924e9f9053ae04c87df2a5759d0 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 21 Jun 2021 16:18:39 -0600 Subject: [PATCH 056/221] Introduce basic audio playback control --- .../views/audio_messages/AudioPlayer.tsx | 96 +++++++++++++++++++ .../views/audio_messages/DurationClock.tsx | 55 +++++++++++ .../views/audio_messages/PlayPauseButton.tsx | 18 +++- src/components/views/messages/MAudioBody.tsx | 25 ++--- src/voice/Playback.ts | 11 +++ src/voice/PlaybackClock.ts | 18 +++- 6 files changed, 203 insertions(+), 20 deletions(-) create mode 100644 src/components/views/audio_messages/AudioPlayer.tsx create mode 100644 src/components/views/audio_messages/DurationClock.tsx diff --git a/src/components/views/audio_messages/AudioPlayer.tsx b/src/components/views/audio_messages/AudioPlayer.tsx new file mode 100644 index 0000000000..82372aca74 --- /dev/null +++ b/src/components/views/audio_messages/AudioPlayer.tsx @@ -0,0 +1,96 @@ +/* +Copyright 2021 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. +*/ + +import { Playback, PlaybackState } from "../../../voice/Playback"; +import React, { createRef, ReactNode, RefObject } from "react"; +import { UPDATE_EVENT } from "../../../stores/AsyncStore"; +import PlaybackWaveform from "./PlaybackWaveform"; +import PlayPauseButton from "./PlayPauseButton"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { formatBytes } from "../../../utils/FormattingUtils"; +import DurationClock from "./DurationClock"; +import { Key } from "../../../Keyboard"; + +interface IProps { + // Playback instance to render. Cannot change during component lifecycle: create + // an all-new component instead. + playback: Playback; +} + +interface IState { + playbackPhase: PlaybackState; +} + +@replaceableComponent("views.audio_messages.AudioPlayer") +export default class AudioPlayer extends React.PureComponent { + private playPauseRef: RefObject = createRef(); + + constructor(props: IProps) { + super(props); + + this.state = { + playbackPhase: PlaybackState.Decoding, // default assumption + }; + + // We don't need to de-register: the class handles this for us internally + this.props.playback.on(UPDATE_EVENT, this.onPlaybackUpdate); + + // Don't wait for the promise to complete - it will emit a progress update when it + // is done, and it's not meant to take long anyhow. + // noinspection JSIgnoredPromiseFromCall + this.props.playback.prepare(); + } + + private onPlaybackUpdate = (ev: PlaybackState) => { + this.setState({ playbackPhase: ev }); + }; + + private onKeyPress = (ev: React.KeyboardEvent) => { + if (ev.key === Key.SPACE) { + ev.stopPropagation(); + this.playPauseRef.current?.toggle(); + } + }; + + protected renderFileSize(): string { + const bytes = this.props.playback.sizeBytes; + if (!bytes) return null; + + // Not translated as these are units, and therefore universal + return `(${formatBytes(bytes)})`; + } + + public render(): ReactNode { + // tabIndex=0 to ensure that the whole component becomes a tab stop, where we handle keyboard + // events for accessibility + return
+
+ +
+ +   {/* easiest way to introduce a gap between the components */} + { this.renderFileSize() } +
+
+ +
+ } +} diff --git a/src/components/views/audio_messages/DurationClock.tsx b/src/components/views/audio_messages/DurationClock.tsx new file mode 100644 index 0000000000..f6271d1cf4 --- /dev/null +++ b/src/components/views/audio_messages/DurationClock.tsx @@ -0,0 +1,55 @@ +/* +Copyright 2021 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. +*/ + +import React from "react"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; +import Clock from "./Clock"; +import { Playback } from "../../../voice/Playback"; + +interface IProps { + playback: Playback; +} + +interface IState { + durationSeconds: number; +} + +/** + * A clock which shows a clip's maximum duration. + */ +@replaceableComponent("views.audio_messages.DurationClock") +export default class DurationClock extends React.PureComponent { + public constructor(props) { + super(props); + + this.state = { + // we track the duration on state because we won't really know what the clip duration + // is until the first time update, and as a PureComponent we are trying to dedupe state + // updates as much as possible. This is just the easiest way to avoid a forceUpdate() or + // member property to track "did we get a duration". + durationSeconds: this.props.playback.clockInfo.durationSeconds, + }; + this.props.playback.clockInfo.liveData.onUpdate(this.onTimeUpdate); + } + + private onTimeUpdate = (time: number[]) => { + this.setState({durationSeconds: time[1]}); + }; + + public render() { + return ; + } +} diff --git a/src/components/views/audio_messages/PlayPauseButton.tsx b/src/components/views/audio_messages/PlayPauseButton.tsx index 399cb169bb..7d881a10e5 100644 --- a/src/components/views/audio_messages/PlayPauseButton.tsx +++ b/src/components/views/audio_messages/PlayPauseButton.tsx @@ -21,7 +21,8 @@ import { _t } from "../../../languageHandler"; import { Playback, PlaybackState } from "../../../voice/Playback"; import classNames from "classnames"; -interface IProps { +// omitted props are handled by render function +interface IProps extends Omit, "title" | "onClick" | "disabled"> { // Playback instance to manipulate. Cannot change during the component lifecycle. playback: Playback; @@ -39,13 +40,19 @@ export default class PlayPauseButton extends React.PureComponent { super(props); } - private onClick = async () => { - await this.props.playback.toggle(); + private onClick = () => { + // noinspection JSIgnoredPromiseFromCall + this.toggle(); }; + public async toggle() { + await this.props.playback.toggle(); + } + public render(): ReactNode { - const isPlaying = this.props.playback.isPlaying; - const isDisabled = this.props.playbackPhase === PlaybackState.Decoding; + const { playback, playbackPhase, ...restProps } = this.props; + const isPlaying = playback.isPlaying; + const isDisabled = playbackPhase === PlaybackState.Decoding; const classes = classNames('mx_PlayPauseButton', { 'mx_PlayPauseButton_play': !isPlaying, 'mx_PlayPauseButton_pause': isPlaying, @@ -56,6 +63,7 @@ export default class PlayPauseButton extends React.PureComponent { title={isPlaying ? _t("Pause") : _t("Play")} onClick={this.onClick} disabled={isDisabled} + {...restProps} />; } } diff --git a/src/components/views/messages/MAudioBody.tsx b/src/components/views/messages/MAudioBody.tsx index ddcebd7a60..a8f2304fe9 100644 --- a/src/components/views/messages/MAudioBody.tsx +++ b/src/components/views/messages/MAudioBody.tsx @@ -15,16 +15,16 @@ limitations under the License. */ import React from "react"; -import {MatrixEvent} from "matrix-js-sdk/src/models/event"; -import {replaceableComponent} from "../../../utils/replaceableComponent"; -import {Playback} from "../../../voice/Playback"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { Playback } from "../../../voice/Playback"; import MFileBody from "./MFileBody"; import InlineSpinner from '../elements/InlineSpinner'; -import {_t} from "../../../languageHandler"; -import {mediaFromContent} from "../../../customisations/Media"; -import {decryptFile} from "../../../utils/DecryptFile"; -import RecordingPlayback from "../audio_messages/RecordingPlayback"; -import {IMediaEventContent} from "../../../customisations/models/IMediaEventContent"; +import { _t } from "../../../languageHandler"; +import { mediaFromContent } from "../../../customisations/Media"; +import { decryptFile } from "../../../utils/DecryptFile"; +import { IMediaEventContent } from "../../../customisations/models/IMediaEventContent"; +import AudioPlayer from "../audio_messages/AudioPlayer"; interface IProps { mxEvent: MatrixEvent; @@ -52,9 +52,9 @@ export default class MAudioBody extends React.PureComponent { try { const blob = await decryptFile(content.file); buffer = await blob.arrayBuffer(); - this.setState({decryptedBlob: blob}); + this.setState({ decryptedBlob: blob }); } catch (e) { - this.setState({error: e}); + this.setState({ error: e }); console.warn("Unable to decrypt audio message", e); return; // stop processing the audio file } @@ -62,7 +62,7 @@ export default class MAudioBody extends React.PureComponent { try { buffer = await media.downloadSource().then(r => r.blob()).then(r => r.arrayBuffer()); } catch (e) { - this.setState({error: e}); + this.setState({ error: e }); console.warn("Unable to download audio message", e); return; // stop processing the audio file } @@ -70,6 +70,7 @@ export default class MAudioBody extends React.PureComponent { // We should have a buffer to work with now: let's set it up const playback = new Playback(buffer); + playback.clockInfo.populatePlaceholdersFrom(this.props.mxEvent); this.setState({ playback }); // Note: the RecordingPlayback component will handle preparing the Playback class for us. } @@ -101,7 +102,7 @@ export default class MAudioBody extends React.PureComponent { // At this point we should have a playable state return ( - + ) diff --git a/src/voice/Playback.ts b/src/voice/Playback.ts index 61da435151..f1f91310b2 100644 --- a/src/voice/Playback.ts +++ b/src/voice/Playback.ts @@ -58,6 +58,7 @@ export class Playback extends EventEmitter implements IDestroyable { private resampledWaveform: number[]; private waveformObservable = new SimpleObservable(); private readonly clock: PlaybackClock; + private readonly fileSize: number; /** * Creates a new playback instance from a buffer. @@ -67,12 +68,22 @@ export class Playback extends EventEmitter implements IDestroyable { */ constructor(private buf: ArrayBuffer, seedWaveform = DEFAULT_WAVEFORM) { super(); + // Capture the file size early as reading the buffer will result in a 0-length buffer left behind + this.fileSize = this.buf.byteLength; this.context = createAudioContext(); this.resampledWaveform = arrayFastResample(seedWaveform ?? DEFAULT_WAVEFORM, PLAYBACK_WAVEFORM_SAMPLES); this.waveformObservable.update(this.resampledWaveform); this.clock = new PlaybackClock(this.context); } + /** + * Size of the audio clip in bytes. May be zero if unknown. This is updated + * when the playback goes through phase changes. + */ + public get sizeBytes(): number { + return this.fileSize; + } + /** * Stable waveform for the playback. Values are guaranteed to be between * zero and one, inclusive. diff --git a/src/voice/PlaybackClock.ts b/src/voice/PlaybackClock.ts index d6d36e861f..9c2d36923f 100644 --- a/src/voice/PlaybackClock.ts +++ b/src/voice/PlaybackClock.ts @@ -14,8 +14,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {SimpleObservable} from "matrix-widget-api"; -import {IDestroyable} from "../utils/IDestroyable"; +import { SimpleObservable } from "matrix-widget-api"; +import { IDestroyable } from "../utils/IDestroyable"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; // Because keeping track of time is sufficiently complicated... export class PlaybackClock implements IDestroyable { @@ -25,12 +26,13 @@ export class PlaybackClock implements IDestroyable { private observable = new SimpleObservable(); private timerId: number; private clipDuration = 0; + private placeholderDuration = 0; public constructor(private context: AudioContext) { } public get durationSeconds(): number { - return this.clipDuration; + return this.clipDuration || this.placeholderDuration; } public set durationSeconds(val: number) { @@ -54,6 +56,16 @@ export class PlaybackClock implements IDestroyable { } }; + /** + * Populates default information about the audio clip from the event body. + * The placeholders will be overridden once known. + * @param {MatrixEvent} event The event to use for placeholders. + */ + public populatePlaceholdersFrom(event: MatrixEvent) { + const durationSeconds = Number(event.getContent()['info']?.['duration']); + if (Number.isFinite(durationSeconds)) this.placeholderDuration = durationSeconds; + } + /** * Mark the time in the audio context where the clip starts/has been loaded. * This is to ensure the clock isn't skewed into thinking it is ~0.5s into From 8ce77e618f4dbeaa007bc1e3c513717c5998275a Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 23 Jun 2021 13:25:07 -0600 Subject: [PATCH 057/221] Reduce pointless CSS vars by 2 --- res/css/views/audio_messages/_PlaybackContainer.scss | 6 +++--- res/themes/dark/css/_dark.scss | 2 -- res/themes/legacy-dark/css/_legacy-dark.scss | 2 -- res/themes/legacy-light/css/_legacy-light.scss | 2 -- res/themes/light/css/_light.scss | 2 -- 5 files changed, 3 insertions(+), 11 deletions(-) diff --git a/res/css/views/audio_messages/_PlaybackContainer.scss b/res/css/views/audio_messages/_PlaybackContainer.scss index 20def16d6a..e992dd10cc 100644 --- a/res/css/views/audio_messages/_PlaybackContainer.scss +++ b/res/css/views/audio_messages/_PlaybackContainer.scss @@ -22,14 +22,14 @@ limitations under the License. // 7px top and bottom for visual design. 12px left & right, but the waveform (right) // has a 1px padding on it that we want to account for. padding: 7px 12px 7px 11px; - background-color: $voice-record-waveform-bg-color; + background-color: $message-body-panel-bg-color; border-radius: 12px; // Cheat at alignment a bit display: flex; align-items: center; - color: $voice-record-waveform-fg-color; + color: $message-body-panel-fg-color; font-size: $font-14px; line-height: $font-24px; @@ -40,7 +40,7 @@ limitations under the License. &.mx_Waveform_bar_100pct { // Small animation to remove the mechanical feel of progress transition: background-color 250ms ease; - background-color: $voice-record-waveform-fg-color; + background-color: $message-body-panel-fg-color; } } } diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index 8b5fde3bd1..81fd3c892a 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -215,8 +215,6 @@ $message-body-panel-icon-fg-color: #21262C; // "Separator" $message-body-panel-icon-bg-color: $tertiary-fg-color; $voice-record-stop-border-color: $quaternary-fg-color; -$voice-record-waveform-bg-color: $message-body-panel-bg-color; -$voice-record-waveform-fg-color: $message-body-panel-fg-color; $voice-record-waveform-incomplete-fg-color: $quaternary-fg-color; $voice-record-icon-color: $quaternary-fg-color; $voice-playback-button-bg-color: $message-body-panel-icon-bg-color; diff --git a/res/themes/legacy-dark/css/_legacy-dark.scss b/res/themes/legacy-dark/css/_legacy-dark.scss index eb6dc40599..a6a038f290 100644 --- a/res/themes/legacy-dark/css/_legacy-dark.scss +++ b/res/themes/legacy-dark/css/_legacy-dark.scss @@ -209,8 +209,6 @@ $message-body-panel-icon-bg-color: $secondary-fg-color; // See non-legacy dark for variable information $voice-record-stop-border-color: #6F7882; -$voice-record-waveform-bg-color: $message-body-panel-bg-color; -$voice-record-waveform-fg-color: $message-body-panel-fg-color; $voice-record-waveform-incomplete-fg-color: #6F7882; $voice-record-icon-color: #6F7882; $voice-playback-button-bg-color: $tertiary-fg-color; diff --git a/res/themes/legacy-light/css/_legacy-light.scss b/res/themes/legacy-light/css/_legacy-light.scss index a6b180bab4..7c1495d935 100644 --- a/res/themes/legacy-light/css/_legacy-light.scss +++ b/res/themes/legacy-light/css/_legacy-light.scss @@ -334,8 +334,6 @@ $message-body-panel-icon-bg-color: $primary-bg-color; $voice-record-stop-symbol-color: #ff4b55; $voice-record-live-circle-color: #ff4b55; $voice-record-stop-border-color: #E3E8F0; -$voice-record-waveform-bg-color: $message-body-panel-bg-color; -$voice-record-waveform-fg-color: $message-body-panel-fg-color; $voice-record-waveform-incomplete-fg-color: #C1C6CD; $voice-record-icon-color: $tertiary-fg-color; $voice-playback-button-bg-color: $message-body-panel-icon-bg-color; diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index d8dab9c9c4..7e958c2af6 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -335,8 +335,6 @@ $voice-record-stop-symbol-color: #ff4b55; $voice-record-live-circle-color: #ff4b55; $voice-record-stop-border-color: #E3E8F0; // "Separator" -$voice-record-waveform-bg-color: $message-body-panel-bg-color; -$voice-record-waveform-fg-color: $message-body-panel-fg-color; $voice-record-waveform-incomplete-fg-color: $quaternary-fg-color; $voice-record-icon-color: $tertiary-fg-color; $voice-playback-button-bg-color: $message-body-panel-icon-bg-color; From ffef2e58cb72fba6551c8f2611007920cb0f62ab Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 23 Jun 2021 13:34:57 -0600 Subject: [PATCH 058/221] Unify audio message types to new media body --- res/css/_components.scss | 1 + .../audio_messages/_PlaybackContainer.scss | 6 ---- res/css/views/messages/_MediaBody.scss | 28 +++++++++++++++++++ .../views/audio_messages/AudioPlayer.tsx | 5 ++-- .../audio_messages/RecordingPlayback.tsx | 2 +- .../views/rooms/VoiceRecordComposerTile.tsx | 2 +- 6 files changed, 33 insertions(+), 11 deletions(-) create mode 100644 res/css/views/messages/_MediaBody.scss diff --git a/res/css/_components.scss b/res/css/_components.scss index cb921e251c..d86ce8d017 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -169,6 +169,7 @@ @import "./views/messages/_MTextBody.scss"; @import "./views/messages/_MVideoBody.scss"; @import "./views/messages/_MVoiceMessageBody.scss"; +@import "./views/messages/_MediaBody.scss"; @import "./views/messages/_MessageActionBar.scss"; @import "./views/messages/_MessageTimestamp.scss"; @import "./views/messages/_MjolnirBody.scss"; diff --git a/res/css/views/audio_messages/_PlaybackContainer.scss b/res/css/views/audio_messages/_PlaybackContainer.scss index e992dd10cc..c1192f188b 100644 --- a/res/css/views/audio_messages/_PlaybackContainer.scss +++ b/res/css/views/audio_messages/_PlaybackContainer.scss @@ -22,17 +22,11 @@ limitations under the License. // 7px top and bottom for visual design. 12px left & right, but the waveform (right) // has a 1px padding on it that we want to account for. padding: 7px 12px 7px 11px; - background-color: $message-body-panel-bg-color; - border-radius: 12px; // Cheat at alignment a bit display: flex; align-items: center; - color: $message-body-panel-fg-color; - font-size: $font-14px; - line-height: $font-24px; - .mx_Waveform { .mx_Waveform_bar { background-color: $voice-record-waveform-incomplete-fg-color; diff --git a/res/css/views/messages/_MediaBody.scss b/res/css/views/messages/_MediaBody.scss new file mode 100644 index 0000000000..12e441750c --- /dev/null +++ b/res/css/views/messages/_MediaBody.scss @@ -0,0 +1,28 @@ +/* +Copyright 2021 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. +*/ + +// A "media body" is any file upload looking thing, apart from images and videos (they +// have unique styles). + +.mx_MediaBody { + background-color: $message-body-panel-bg-color; + border-radius: 12px; + + color: $message-body-panel-fg-color; + font-size: $font-14px; + line-height: $font-24px; +} + diff --git a/src/components/views/audio_messages/AudioPlayer.tsx b/src/components/views/audio_messages/AudioPlayer.tsx index 82372aca74..93bfab3dcd 100644 --- a/src/components/views/audio_messages/AudioPlayer.tsx +++ b/src/components/views/audio_messages/AudioPlayer.tsx @@ -17,7 +17,6 @@ limitations under the License. import { Playback, PlaybackState } from "../../../voice/Playback"; import React, { createRef, ReactNode, RefObject } from "react"; import { UPDATE_EVENT } from "../../../stores/AsyncStore"; -import PlaybackWaveform from "./PlaybackWaveform"; import PlayPauseButton from "./PlayPauseButton"; import { replaceableComponent } from "../../../utils/replaceableComponent"; import { formatBytes } from "../../../utils/FormattingUtils"; @@ -76,7 +75,7 @@ export default class AudioPlayer extends React.PureComponent { public render(): ReactNode { // tabIndex=0 to ensure that the whole component becomes a tab stop, where we handle keyboard // events for accessibility - return
+ return
{ { this.renderFileSize() }
- + TODO: Seek bar
} } diff --git a/src/components/views/audio_messages/RecordingPlayback.tsx b/src/components/views/audio_messages/RecordingPlayback.tsx index 32ce32805d..7c7a0a87c1 100644 --- a/src/components/views/audio_messages/RecordingPlayback.tsx +++ b/src/components/views/audio_messages/RecordingPlayback.tsx @@ -55,7 +55,7 @@ export default class RecordingPlayback extends React.PureComponent + return
diff --git a/src/components/views/rooms/VoiceRecordComposerTile.tsx b/src/components/views/rooms/VoiceRecordComposerTile.tsx index d8c481e332..d956a3febf 100644 --- a/src/components/views/rooms/VoiceRecordComposerTile.tsx +++ b/src/components/views/rooms/VoiceRecordComposerTile.tsx @@ -177,7 +177,7 @@ export default class VoiceRecordComposerTile extends React.PureComponent + return
; From aaec9857fd2762901bf0356fdef3f48f42cf1064 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 23 Jun 2021 19:34:25 -0600 Subject: [PATCH 059/221] Add optional mark function callback --- src/utils/MarkedExecution.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/utils/MarkedExecution.ts b/src/utils/MarkedExecution.ts index b0b8fdf63d..01cc91adce 100644 --- a/src/utils/MarkedExecution.ts +++ b/src/utils/MarkedExecution.ts @@ -26,9 +26,11 @@ export class MarkedExecution { /** * Creates a MarkedExecution for the provided function. - * @param fn The function to be called upon trigger if marked. + * @param {Function} fn The function to be called upon trigger if marked. + * @param {Function} onMarkCallback A function that is called when a new mark is made. Not + * called if a mark is already flagged. */ - constructor(private fn: () => void) { + constructor(private fn: () => void, private onMarkCallback?: () => void) { } /** @@ -42,6 +44,7 @@ export class MarkedExecution { * Marks the function to be called upon trigger(). */ public mark() { + if (!this.marked) this.onMarkCallback?.(); this.marked = true; } From 9c752680ba9689f35df94d8efbdfae688a382849 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 23 Jun 2021 19:34:34 -0600 Subject: [PATCH 060/221] Tile styling and early behaviour --- res/css/_components.scss | 3 +- .../views/audio_messages/_AudioPlayer.scss | 68 +++++++++++ .../audio_messages/_PlayPauseButton.scss | 2 + res/css/views/audio_messages/_SeekBar.scss | 89 ++++++++++++++ res/css/views/messages/_MAudioBody.scss | 19 --- res/themes/legacy-dark/css/_legacy-dark.scss | 3 + .../legacy-light/css/_legacy-light.scss | 3 + .../views/audio_messages/AudioPlayer.tsx | 23 +++- .../views/audio_messages/PlaybackClock.tsx | 11 +- .../views/audio_messages/SeekBar.tsx | 110 ++++++++++++++++++ src/components/views/messages/MAudioBody.tsx | 2 +- src/i18n/strings/en_EN.json | 7 +- 12 files changed, 310 insertions(+), 30 deletions(-) create mode 100644 res/css/views/audio_messages/_AudioPlayer.scss create mode 100644 res/css/views/audio_messages/_SeekBar.scss delete mode 100644 res/css/views/messages/_MAudioBody.scss create mode 100644 src/components/views/audio_messages/SeekBar.tsx diff --git a/res/css/_components.scss b/res/css/_components.scss index d86ce8d017..1517527034 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -37,8 +37,10 @@ @import "./structures/_ViewSource.scss"; @import "./structures/auth/_CompleteSecurity.scss"; @import "./structures/auth/_Login.scss"; +@import "./views/audio_messages/_AudioPlayer.scss"; @import "./views/audio_messages/_PlayPauseButton.scss"; @import "./views/audio_messages/_PlaybackContainer.scss"; +@import "./views/audio_messages/_SeekBar.scss"; @import "./views/audio_messages/_Waveform.scss"; @import "./views/auth/_AuthBody.scss"; @import "./views/auth/_AuthButtons.scss"; @@ -159,7 +161,6 @@ @import "./views/messages/_CreateEvent.scss"; @import "./views/messages/_DateSeparator.scss"; @import "./views/messages/_EventTileBubble.scss"; -@import "./views/messages/_MAudioBody.scss"; @import "./views/messages/_MEmoteBody.scss"; @import "./views/messages/_MFileBody.scss"; @import "./views/messages/_MImageBody.scss"; diff --git a/res/css/views/audio_messages/_AudioPlayer.scss b/res/css/views/audio_messages/_AudioPlayer.scss new file mode 100644 index 0000000000..2d3777aeca --- /dev/null +++ b/res/css/views/audio_messages/_AudioPlayer.scss @@ -0,0 +1,68 @@ +/* +Copyright 2021 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. +*/ + +.mx_AudioPlayer_container { + padding: 16px 12px 11px 12px; + width: 267px; + + .mx_AudioPlayer_primaryContainer { + display: flex; + + .mx_PlayPauseButton { + margin-right: 8px; + } + + .mx_AudioPlayer_mediaInfo { + flex: 1; + overflow: hidden; // makes the ellipsis on the file name work + + & > * { + display: block; + } + + .mx_AudioPlayer_mediaName { + color: $primary-fg-color; + font-size: $font-15px; + line-height: $font-15px; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + padding-bottom: 4px; // mimics the line-height differences in the Figma + } + + .mx_AudioPlayer_byline { + font-size: $font-12px; + line-height: $font-12px; + } + } + } + + .mx_AudioPlayer_seek { + display: flex; + align-items: center; + + .mx_SeekBar { + flex: 1; + } + + .mx_Clock { + width: $font-42px; // we're not using a monospace font, so fake it + min-width: $font-42px; // for flexbox + padding-left: 4px; // isolate from seek bar + text-align: right; + } + } +} diff --git a/res/css/views/audio_messages/_PlayPauseButton.scss b/res/css/views/audio_messages/_PlayPauseButton.scss index 6caedafa29..714da3e605 100644 --- a/res/css/views/audio_messages/_PlayPauseButton.scss +++ b/res/css/views/audio_messages/_PlayPauseButton.scss @@ -18,6 +18,8 @@ limitations under the License. position: relative; width: 32px; height: 32px; + min-width: 32px; // for when the button is used in a flexbox + min-height: 32px; // for when the button is used in a flexbox border-radius: 32px; background-color: $voice-playback-button-bg-color; diff --git a/res/css/views/audio_messages/_SeekBar.scss b/res/css/views/audio_messages/_SeekBar.scss new file mode 100644 index 0000000000..23fc0bf5db --- /dev/null +++ b/res/css/views/audio_messages/_SeekBar.scss @@ -0,0 +1,89 @@ +/* +Copyright 2021 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. +*/ + +// CSS inspiration from: +// * https://www.w3schools.com/howto/howto_js_rangeslider.asp +// * https://stackoverflow.com/a/28283806 +// * https://css-tricks.com/styling-cross-browser-compatible-range-inputs-css/ + +.mx_SeekBar { + // Dev note: we deliberately do not have the -ms-track (and friends) selectors because we don't + // need to support IE. + + appearance: none; // default style override + + width: 100%; + height: 1px; + background: $quaternary-fg-color; + outline: none; // remove blue selection border + position: relative; // for progress bar support later on + + &::-webkit-slider-thumb { + appearance: none; // default style override + + // Dev note: This needs to be duplicated with the -moz-range-thumb selector + // because otherwise Edge (webkit) will fail to see the styles and just refuse + // to apply them. + width: 8px; + height: 8px; + border-radius: 8px; + background-color: $tertiary-fg-color; + cursor: pointer; + } + + &::-moz-range-thumb { + width: 8px; + height: 8px; + border-radius: 8px; + background-color: $tertiary-fg-color; + cursor: pointer; + + // Firefox adds a border on the thumb + border: none; + } + + // This is for webkit support, but we can't limit the functionality of it to just webkit + // browsers. Firefox responds to webkit-prefixed values now, which means we can't use media + // or support queries to selectively apply the rule. An upside is that this CSS doesn't work + // in firefox, so it's just wasted CPU/GPU time. + &::before { // ::before to ensure it ends up under the thumb + content: ''; + background-color: $tertiary-fg-color; + + // Absolute positioning to ensure it overlaps with the existing bar + position: absolute; + top: 0; + left: 0; + + // Sizing to match the bar + width: 100%; + height: 1px; + + // And finally dynamic width without overly hurting the rendering engine. + transform-origin: 0 100%; + transform: scaleX(var(--fillTo)); + } + + // This is firefox's built-in support for the above, with 100% less hacks. + &::-moz-range-progress { + background-color: $tertiary-fg-color; + height: 1px; + } + + &:disabled { + opacity: 0.5; + } +} diff --git a/res/css/views/messages/_MAudioBody.scss b/res/css/views/messages/_MAudioBody.scss deleted file mode 100644 index 2c9b6f7030..0000000000 --- a/res/css/views/messages/_MAudioBody.scss +++ /dev/null @@ -1,19 +0,0 @@ -/* -Copyright 2021 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. -*/ - -.mx_MAudioBody { - display: inline-block; // makes the playback controls magically line up -} diff --git a/res/themes/legacy-dark/css/_legacy-dark.scss b/res/themes/legacy-dark/css/_legacy-dark.scss index a6a038f290..df01efbe1e 100644 --- a/res/themes/legacy-dark/css/_legacy-dark.scss +++ b/res/themes/legacy-dark/css/_legacy-dark.scss @@ -20,6 +20,9 @@ $tertiary-fg-color: $primary-fg-color; $primary-bg-color: $bg-color; $muted-fg-color: $header-panel-text-primary-color; +// Legacy theme backports +$quaternary-fg-color: #6F7882; + // used for dialog box text $light-fg-color: $header-panel-text-secondary-color; diff --git a/res/themes/legacy-light/css/_legacy-light.scss b/res/themes/legacy-light/css/_legacy-light.scss index 7c1495d935..c7debcdabe 100644 --- a/res/themes/legacy-light/css/_legacy-light.scss +++ b/res/themes/legacy-light/css/_legacy-light.scss @@ -28,6 +28,9 @@ $tertiary-fg-color: $primary-fg-color; $primary-bg-color: #ffffff; $muted-fg-color: #61708b; // Commonly used in headings and relevant alt text +// Legacy theme backports +$quaternary-fg-color: #C1C6CD; + // used for dialog box text $light-fg-color: #747474; diff --git a/src/components/views/audio_messages/AudioPlayer.tsx b/src/components/views/audio_messages/AudioPlayer.tsx index 93bfab3dcd..336139e467 100644 --- a/src/components/views/audio_messages/AudioPlayer.tsx +++ b/src/components/views/audio_messages/AudioPlayer.tsx @@ -22,11 +22,16 @@ import { replaceableComponent } from "../../../utils/replaceableComponent"; import { formatBytes } from "../../../utils/FormattingUtils"; import DurationClock from "./DurationClock"; import { Key } from "../../../Keyboard"; +import { _t } from "../../../languageHandler"; +import SeekBar from "./SeekBar"; +import PlaybackClock from "./PlaybackClock"; interface IProps { // Playback instance to render. Cannot change during component lifecycle: create // an all-new component instead. playback: Playback; + + mediaName: string; } interface IState { @@ -83,13 +88,21 @@ export default class AudioPlayer extends React.PureComponent { tabIndex={-1} // prevent tabbing into the button ref={this.playPauseRef} /> -
- -   {/* easiest way to introduce a gap between the components */} - { this.renderFileSize() } +
+ + {this.props.mediaName || _t("Unnamed audio")} + +
+ +   {/* easiest way to introduce a gap between the components */} + { this.renderFileSize() } +
- TODO: Seek bar +
+ + +
} } diff --git a/src/components/views/audio_messages/PlaybackClock.tsx b/src/components/views/audio_messages/PlaybackClock.tsx index 82108ae63c..d94e0bed82 100644 --- a/src/components/views/audio_messages/PlaybackClock.tsx +++ b/src/components/views/audio_messages/PlaybackClock.tsx @@ -22,6 +22,11 @@ import { UPDATE_EVENT } from "../../../stores/AsyncStore"; interface IProps { playback: Playback; + + // The default number of seconds to show when the playback has completed or + // has not started. Not used during playback, even when paused. Defaults to + // clip duration length. + defaultDisplaySeconds?: number; } interface IState { @@ -64,7 +69,11 @@ export default class PlaybackClock extends React.PureComponent { public render() { let seconds = this.state.seconds; if (this.state.playbackPhase === PlaybackState.Stopped) { - seconds = this.state.durationSeconds; + if (Number.isFinite(this.props.defaultDisplaySeconds)) { + seconds = this.props.defaultDisplaySeconds; + } else { + seconds = this.state.durationSeconds; + } } return ; } diff --git a/src/components/views/audio_messages/SeekBar.tsx b/src/components/views/audio_messages/SeekBar.tsx new file mode 100644 index 0000000000..2561321df3 --- /dev/null +++ b/src/components/views/audio_messages/SeekBar.tsx @@ -0,0 +1,110 @@ +/* +Copyright 2021 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. +*/ + +import { Playback, PlaybackState } from "../../../voice/Playback"; +import React, { ChangeEvent, createRef, CSSProperties, ReactNode, RefObject } from "react"; +import { UPDATE_EVENT } from "../../../stores/AsyncStore"; +import PlayPauseButton from "./PlayPauseButton"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { formatBytes } from "../../../utils/FormattingUtils"; +import DurationClock from "./DurationClock"; +import { Key } from "../../../Keyboard"; +import { _t } from "../../../languageHandler"; +import { MarkedExecution } from "../../../utils/MarkedExecution"; +import { percentageOf } from "../../../utils/numbers"; + +interface IProps { + // Playback instance to render. Cannot change during component lifecycle: create + // an all-new component instead. + playback: Playback; + + // Tab index for the underlying component. Useful if the seek bar is in a managed state. + // Defaults to zero. + tabIndex?: number; + + playbackPhase: PlaybackState; +} + +interface IState { + percentage: number; +} + +interface ISeekCSS extends CSSProperties { + '--fillTo': number; +} + +@replaceableComponent("views.audio_messages.SeekBar") +export default class SeekBar extends React.PureComponent { + // We use an animation frame request to avoid overly spamming prop updates, even if we aren't + // really using anything demanding on the CSS front. + + private animationFrameFn = new MarkedExecution( + () => this.doUpdate(), + () => requestAnimationFrame(() => this.animationFrameFn.trigger())); + + public static defaultProps = { + tabIndex: 0, + }; + + constructor(props: IProps) { + super(props); + + this.state = { + percentage: 0, + }; + + // We don't need to de-register: the class handles this for us internally + this.props.playback.clockInfo.liveData.onUpdate(() => this.animationFrameFn.mark()); + } + + private doUpdate() { + this.setState({ + percentage: percentageOf( + this.props.playback.clockInfo.timeSeconds, + 0, + this.props.playback.clockInfo.durationSeconds), + }); + } + + public left() { + console.log("@@ LEFT"); + } + + public right() { + console.log("@@ RIGHT"); + } + + private onChange = (ev: ChangeEvent) => { + console.log('@@ CHANGE', ev.target.value); + }; + + public render(): ReactNode { + // We use a range input to avoid having to re-invent accessibility handling on + // a custom set of divs. + return ; + } +} diff --git a/src/components/views/messages/MAudioBody.tsx b/src/components/views/messages/MAudioBody.tsx index a8f2304fe9..8791828828 100644 --- a/src/components/views/messages/MAudioBody.tsx +++ b/src/components/views/messages/MAudioBody.tsx @@ -102,7 +102,7 @@ export default class MAudioBody extends React.PureComponent { // At this point we should have a playable state return ( - + ) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index bc62868a0f..9da5a41327 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -918,8 +918,6 @@ "Silence call": "Silence call", "Decline": "Decline", "Accept": "Accept", - "Pause": "Pause", - "Play": "Play", "The other party cancelled the verification.": "The other party cancelled the verification.", "Verified!": "Verified!", "You've successfully verified this user.": "You've successfully verified this user.", @@ -1870,7 +1868,7 @@ "Ignored attempt to disable encryption": "Ignored attempt to disable encryption", "Encryption not enabled": "Encryption not enabled", "The encryption used by this room isn't supported.": "The encryption used by this room isn't supported.", - "Error decrypting audio": "Error decrypting audio", + "Error processing audio message": "Error processing audio message", "React": "React", "Edit": "Edit", "Retry": "Retry", @@ -2602,6 +2600,9 @@ "Use email or phone to optionally be discoverable by existing contacts.": "Use email or phone to optionally be discoverable by existing contacts.", "Use email to optionally be discoverable by existing contacts.": "Use email to optionally be discoverable by existing contacts.", "Sign in with SSO": "Sign in with SSO", + "Unnamed audio": "Unnamed audio", + "Pause": "Pause", + "Play": "Play", "Couldn't load page": "Couldn't load page", "You must register to use this functionality": "You must register to use this functionality", "You must join the room to see its files": "You must join the room to see its files", From ebb6f1b602a0f5fb0d2c799db551d4a213c11602 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 24 Jun 2021 20:18:50 -0600 Subject: [PATCH 061/221] Add seeking and notes about clock desync --- res/css/views/audio_messages/_SeekBar.scss | 2 + .../views/audio_messages/AudioPlayer.tsx | 7 ++ .../views/audio_messages/SeekBar.tsx | 14 +++- src/voice/Playback.ts | 81 ++++++++++++++++--- src/voice/PlaybackClock.ts | 56 ++++++++++++- 5 files changed, 143 insertions(+), 17 deletions(-) diff --git a/res/css/views/audio_messages/_SeekBar.scss b/res/css/views/audio_messages/_SeekBar.scss index 23fc0bf5db..ca3f7aa562 100644 --- a/res/css/views/audio_messages/_SeekBar.scss +++ b/res/css/views/audio_messages/_SeekBar.scss @@ -31,6 +31,8 @@ limitations under the License. outline: none; // remove blue selection border position: relative; // for progress bar support later on + cursor: pointer; + &::-webkit-slider-thumb { appearance: none; // default style override diff --git a/src/components/views/audio_messages/AudioPlayer.tsx b/src/components/views/audio_messages/AudioPlayer.tsx index 336139e467..0767c814d8 100644 --- a/src/components/views/audio_messages/AudioPlayer.tsx +++ b/src/components/views/audio_messages/AudioPlayer.tsx @@ -41,6 +41,7 @@ interface IState { @replaceableComponent("views.audio_messages.AudioPlayer") export default class AudioPlayer extends React.PureComponent { private playPauseRef: RefObject = createRef(); + private seekRef: RefObject = createRef(); constructor(props: IProps) { super(props); @@ -66,6 +67,12 @@ export default class AudioPlayer extends React.PureComponent { if (ev.key === Key.SPACE) { ev.stopPropagation(); this.playPauseRef.current?.toggle(); + } else if (ev.key === Key.ARROW_LEFT) { + ev.stopPropagation(); + this.seekRef.current?.left(); + } else if (ev.key === Key.ARROW_RIGHT) { + ev.stopPropagation(); + this.seekRef.current?.right(); } }; diff --git a/src/components/views/audio_messages/SeekBar.tsx b/src/components/views/audio_messages/SeekBar.tsx index 2561321df3..09beed70b6 100644 --- a/src/components/views/audio_messages/SeekBar.tsx +++ b/src/components/views/audio_messages/SeekBar.tsx @@ -46,6 +46,8 @@ interface ISeekCSS extends CSSProperties { '--fillTo': number; } +const ARROW_SKIP_SECONDS = 5; // arbitrary + @replaceableComponent("views.audio_messages.SeekBar") export default class SeekBar extends React.PureComponent { // We use an animation frame request to avoid overly spamming prop updates, even if we aren't @@ -80,15 +82,21 @@ export default class SeekBar extends React.PureComponent { } public left() { - console.log("@@ LEFT"); + // noinspection JSIgnoredPromiseFromCall + this.props.playback.skipTo(this.props.playback.clockInfo.timeSeconds - ARROW_SKIP_SECONDS); } public right() { - console.log("@@ RIGHT"); + // noinspection JSIgnoredPromiseFromCall + this.props.playback.skipTo(this.props.playback.clockInfo.timeSeconds + ARROW_SKIP_SECONDS); } private onChange = (ev: ChangeEvent) => { - console.log('@@ CHANGE', ev.target.value); + // Thankfully, onChange is only called when the user changes the value, not when we + // change the value on the component. We can use this as a reliable "skip to X" function. + // + // noinspection JSIgnoredPromiseFromCall + this.props.playback.skipTo(Number(ev.target.value) * this.props.playback.clockInfo.durationSeconds); }; public render(): ReactNode { diff --git a/src/voice/Playback.ts b/src/voice/Playback.ts index f1f91310b2..6a120bf924 100644 --- a/src/voice/Playback.ts +++ b/src/voice/Playback.ts @@ -161,16 +161,9 @@ export class Playback extends EventEmitter implements IDestroyable { public async play() { // We can't restart a buffer source, so we need to create a new one if we hit the end if (this.state === PlaybackState.Stopped) { - if (this.source) { - this.source.disconnect(); - this.source.removeEventListener("ended", this.onPlaybackEnd); - } - - this.source = this.context.createBufferSource(); - this.source.connect(this.context.destination); - this.source.buffer = this.audioBuf; - this.source.start(); // start immediately - this.source.addEventListener("ended", this.onPlaybackEnd); + this.disconnectSource(); + this.makeNewSourceBuffer(); + this.source.start(); } // We use the context suspend/resume functions because it allows us to pause a source @@ -180,6 +173,18 @@ export class Playback extends EventEmitter implements IDestroyable { this.emit(PlaybackState.Playing); } + private disconnectSource() { + this.source?.disconnect(); + this.source?.removeEventListener("ended", this.onPlaybackEnd); + } + + private makeNewSourceBuffer() { + this.source = this.context.createBufferSource(); + this.source.buffer = this.audioBuf; + this.source.addEventListener("ended", this.onPlaybackEnd); + this.source.connect(this.context.destination); + } + public async pause() { await this.context.suspend(); this.emit(PlaybackState.Paused); @@ -194,4 +199,60 @@ export class Playback extends EventEmitter implements IDestroyable { if (this.isPlaying) await this.pause(); else await this.play(); } + + public async skipTo(timeSeconds: number) { + // Dev note: this function talks a lot about clock desyncs. There is a clock running + // independently to the audio context and buffer so that accurate human-perceptible + // time can be exposed. The PlaybackClock class has more information, but the short + // version is that we need to line up the useful time (clip position) with the context + // time, and avoid as many deviations as possible as otherwise the user could see the + // wrong time, and we stop playback at the wrong time, etc. + + timeSeconds = clamp(timeSeconds, 0, this.clock.durationSeconds); + + // Track playing state so we don't cause seeking to start playing the track. + const isPlaying = this.isPlaying; + + if (isPlaying) { + // Pause first so we can get an accurate measurement of time + await this.context.suspend(); + } + + // We can't simply tell the context/buffer to jump to a time, so we have to + // start a whole new buffer and start it from the new time offset. + const now = this.context.currentTime; + this.disconnectSource(); + this.makeNewSourceBuffer(); + + // We have to resync the clock because it can get confused about where we're + // at in the audio clip. + this.clock.syncTo(now, timeSeconds); + + // Always start the source to queue it up. We have to do this now (and pause + // quickly if we're not supposed to be playing) as otherwise the clock can desync + // when it comes time to the user hitting play. After a couple jumps, the user + // will have desynced the clock enough to be about 10-15 seconds off, while this + // keeps it as close to perfect as humans can perceive. + this.source.start(now, timeSeconds); + + // Dev note: it's critical that the code gap between `this.source.start()` and + // `this.pause()` is as small as possible: we do not want to delay *anything* + // as that could cause a clock desync, or a buggy feeling as a single note plays + // during seeking. + + if (isPlaying) { + // If we were playing before, continue the context so the clock doesn't desync. + await this.context.resume(); + } else { + // As mentioned above, we'll have to pause the clip if we weren't supposed to + // be playing it just yet. If we didn't have this, the audio clip plays but all + // the states will be wrong: clock won't advance, pause state doesn't match the + // blaring noise leaving the user's speakers, etc. + // + // Also as mentioned, if the code gap is small enough then this should be + // executed immediately after the start time, leaving no feasible time for the + // user's speakers to play any sound. + await this.pause(); + } + } } diff --git a/src/voice/PlaybackClock.ts b/src/voice/PlaybackClock.ts index 9c2d36923f..e3f41930de 100644 --- a/src/voice/PlaybackClock.ts +++ b/src/voice/PlaybackClock.ts @@ -18,7 +18,42 @@ import { SimpleObservable } from "matrix-widget-api"; import { IDestroyable } from "../utils/IDestroyable"; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; -// Because keeping track of time is sufficiently complicated... +/** + * Tracks accurate human-perceptible time for an audio clip, as informed + * by managed playback. This clock is tightly coupled with the operation + * of the Playback class, making assumptions about how the provided + * AudioContext will be used (suspended/resumed to preserve time, etc). + * + * But why do we need a clock? The AudioContext exposes time information, + * and so does the audio buffer, but not in a way that is useful for humans + * to perceive. The audio buffer time is often lagged behind the context + * time due to internal processing delays of the audio API. Additionally, + * the context's time is tracked from when it was first initialized/started, + * not related to positioning within the clip. However, the context time + * is the most accurate time we can use to determine position within the + * clip if we're fast enough to track the pauses and stops. + * + * As a result, we track every play, pause, stop, and seek event from the + * Playback class (kinda: it calls us, which is close enough to the same + * thing). These events are then tracked on the AudioContext time scale, + * with assumptions that code execution will result in negligible desync + * of the clock, or at least no perceptible difference in time. It's + * extremely important that the calling code, and the clock's own code, + * is extremely fast between the event happening and the clock time being + * tracked - anything more than a dozen milliseconds is likely to stack up + * poorly, leading to clock desync. + * + * Clock desync can be dangerous for the stability of the playback controls: + * if the clock thinks the user is somewhere else in the clip, it could + * inform the playback of the wrong place in time, leading to dead air in + * the output or, if severe enough, a clock that won't stop running while + * the audio is paused/stopped. Other examples include the clip stopping at + * 90% time due to playback ending, the clip playing from the wrong spot + * relative to the time, and negative clock time. + * + * Note that the clip duration is fed to the clock: this is to ensure that + * we have the most accurate time possible to present. + */ export class PlaybackClock implements IDestroyable { private clipStart = 0; private stopped = true; @@ -41,6 +76,12 @@ export class PlaybackClock implements IDestroyable { } public get timeSeconds(): number { + // The modulo is to ensure that we're only looking at the most recent clip + // time, as the context is long-running and multiple plays might not be + // informed to us (if the control is looping, for example). By taking the + // remainder of the division operation, we're assuming that playback is + // incomplete or stopped, thus giving an accurate position within the active + // clip segment. return (this.context.currentTime - this.clipStart) % this.clipDuration; } @@ -49,7 +90,7 @@ export class PlaybackClock implements IDestroyable { } private checkTime = () => { - const now = this.timeSeconds; + const now = this.timeSeconds; // calculated dynamically if (this.lastCheck !== now) { this.observable.update([now, this.durationSeconds]); this.lastCheck = now; @@ -82,8 +123,9 @@ export class PlaybackClock implements IDestroyable { } if (!this.timerId) { - // case to number because the types are wrong - // 100ms interval to make sure the time is as accurate as possible + // cast to number because the types are wrong + // 100ms interval to make sure the time is as accurate as possible without + // being overly insane this.timerId = setInterval(this.checkTime, 100); } } @@ -92,6 +134,12 @@ export class PlaybackClock implements IDestroyable { this.stopped = true; } + public syncTo(contextTime: number, clipTime: number) { + this.clipStart = contextTime - clipTime; + this.stopped = false; // count as a mid-stream pause (if we were stopped) + this.checkTime(); + } + public destroy() { this.observable.close(); if (this.timerId) clearInterval(this.timerId); From c5a72ee6ad1c75307132ffe063450c77f77d51f7 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 24 Jun 2021 20:26:40 -0600 Subject: [PATCH 062/221] Fix arrow seeking --- .../views/audio_messages/AudioPlayer.tsx | 16 ++++++++++++---- .../views/audio_messages/PlayPauseButton.tsx | 4 ++-- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/components/views/audio_messages/AudioPlayer.tsx b/src/components/views/audio_messages/AudioPlayer.tsx index 0767c814d8..2f74c7b069 100644 --- a/src/components/views/audio_messages/AudioPlayer.tsx +++ b/src/components/views/audio_messages/AudioPlayer.tsx @@ -63,10 +63,13 @@ export default class AudioPlayer extends React.PureComponent { this.setState({ playbackPhase: ev }); }; - private onKeyPress = (ev: React.KeyboardEvent) => { + private onKeyDown = (ev: React.KeyboardEvent) => { + // stopPropagation() prevents the FocusComposer catch-all from triggering, + // but we need to do it on key down instead of press (even though the user + // interaction is typically on press). if (ev.key === Key.SPACE) { ev.stopPropagation(); - this.playPauseRef.current?.toggle(); + this.playPauseRef.current?.toggleState(); } else if (ev.key === Key.ARROW_LEFT) { ev.stopPropagation(); this.seekRef.current?.left(); @@ -87,7 +90,7 @@ export default class AudioPlayer extends React.PureComponent { public render(): ReactNode { // tabIndex=0 to ensure that the whole component becomes a tab stop, where we handle keyboard // events for accessibility - return
+ return
{
- +
diff --git a/src/components/views/audio_messages/PlayPauseButton.tsx b/src/components/views/audio_messages/PlayPauseButton.tsx index 7d881a10e5..a4f1e770f2 100644 --- a/src/components/views/audio_messages/PlayPauseButton.tsx +++ b/src/components/views/audio_messages/PlayPauseButton.tsx @@ -42,10 +42,10 @@ export default class PlayPauseButton extends React.PureComponent { private onClick = () => { // noinspection JSIgnoredPromiseFromCall - this.toggle(); + this.toggleState(); }; - public async toggle() { + public async toggleState() { await this.props.playback.toggle(); } From dd53c25706757e5d373bc7bcedf19f9a2470ddee Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 24 Jun 2021 20:37:34 -0600 Subject: [PATCH 063/221] Fix right panel sizing --- res/css/views/audio_messages/_AudioPlayer.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/views/audio_messages/_AudioPlayer.scss b/res/css/views/audio_messages/_AudioPlayer.scss index 2d3777aeca..3f02e54aab 100644 --- a/res/css/views/audio_messages/_AudioPlayer.scss +++ b/res/css/views/audio_messages/_AudioPlayer.scss @@ -16,7 +16,7 @@ limitations under the License. .mx_AudioPlayer_container { padding: 16px 12px 11px 12px; - width: 267px; + max-width: 267px; // use max to make the control fit in the files/pinned panels .mx_AudioPlayer_primaryContainer { display: flex; From d724af600ff47555b1e108056b9bb383b5aab9dc Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 24 Jun 2021 20:41:00 -0600 Subject: [PATCH 064/221] Remove copy/paste fails --- src/components/views/audio_messages/SeekBar.tsx | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/components/views/audio_messages/SeekBar.tsx b/src/components/views/audio_messages/SeekBar.tsx index 09beed70b6..ed07c44482 100644 --- a/src/components/views/audio_messages/SeekBar.tsx +++ b/src/components/views/audio_messages/SeekBar.tsx @@ -15,14 +15,8 @@ limitations under the License. */ import { Playback, PlaybackState } from "../../../voice/Playback"; -import React, { ChangeEvent, createRef, CSSProperties, ReactNode, RefObject } from "react"; -import { UPDATE_EVENT } from "../../../stores/AsyncStore"; -import PlayPauseButton from "./PlayPauseButton"; +import React, { ChangeEvent, CSSProperties, ReactNode } from "react"; import { replaceableComponent } from "../../../utils/replaceableComponent"; -import { formatBytes } from "../../../utils/FormattingUtils"; -import DurationClock from "./DurationClock"; -import { Key } from "../../../Keyboard"; -import { _t } from "../../../languageHandler"; import { MarkedExecution } from "../../../utils/MarkedExecution"; import { percentageOf } from "../../../utils/numbers"; From 76caba0385ee5b333552a7457fd66823e937b668 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 25 Jun 2021 00:10:32 -0600 Subject: [PATCH 065/221] Restore mx_VoiceMessagePrimaryContainer class --- src/components/views/rooms/VoiceRecordComposerTile.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/VoiceRecordComposerTile.tsx b/src/components/views/rooms/VoiceRecordComposerTile.tsx index d956a3febf..036a345af0 100644 --- a/src/components/views/rooms/VoiceRecordComposerTile.tsx +++ b/src/components/views/rooms/VoiceRecordComposerTile.tsx @@ -177,7 +177,7 @@ export default class VoiceRecordComposerTile extends React.PureComponent + return
; From e0a73a583e5f11ed3b42382c7f90b8554873e5dd Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 25 Jun 2021 00:19:57 -0600 Subject: [PATCH 066/221] Increase clickable area of seek bar --- res/css/views/audio_messages/_SeekBar.scss | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/res/css/views/audio_messages/_SeekBar.scss b/res/css/views/audio_messages/_SeekBar.scss index ca3f7aa562..602ce94916 100644 --- a/res/css/views/audio_messages/_SeekBar.scss +++ b/res/css/views/audio_messages/_SeekBar.scss @@ -29,7 +29,7 @@ limitations under the License. height: 1px; background: $quaternary-fg-color; outline: none; // remove blue selection border - position: relative; // for progress bar support later on + position: relative; // for before+after pseudo elements later on cursor: pointer; @@ -88,4 +88,16 @@ limitations under the License. &:disabled { opacity: 0.5; } + + // Increase clickable area for the slider (approximately same size as browser default) + // We do it this way to keep the same padding and margins of the element, avoiding margin math. + // Source: https://front-back.com/expand-clickable-areas-for-a-better-touch-experience/ + &:after { + content: ''; + position: absolute; + top: -6px; + bottom: -6px; + left: 0; + right: 0; + } } From 7a6ee7d91896f234a54a4def20a3bdba57acb04f Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 25 Jun 2021 00:22:05 -0600 Subject: [PATCH 067/221] I know how CSS works. --- res/css/views/audio_messages/_SeekBar.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/views/audio_messages/_SeekBar.scss b/res/css/views/audio_messages/_SeekBar.scss index 602ce94916..d13fe4ac6a 100644 --- a/res/css/views/audio_messages/_SeekBar.scss +++ b/res/css/views/audio_messages/_SeekBar.scss @@ -92,7 +92,7 @@ limitations under the License. // Increase clickable area for the slider (approximately same size as browser default) // We do it this way to keep the same padding and margins of the element, avoiding margin math. // Source: https://front-back.com/expand-clickable-areas-for-a-better-touch-experience/ - &:after { + &::after { content: ''; position: absolute; top: -6px; From d6cf2346fe3da5881eac8ab5a0b282f7da71848b Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 28 Jun 2021 21:00:36 -0600 Subject: [PATCH 068/221] Manually conflict resolve https://github.com/matrix-org/matrix-react-sdk/pull/6240 --- .../audio_messages/_PlaybackContainer.scss | 5 ++ .../audio_messages/LiveRecordingClock.tsx | 30 ++++++++--- .../audio_messages/LiveRecordingWaveform.tsx | 49 ++++++++++------- .../views/audio_messages/Waveform.tsx | 13 +++-- .../views/rooms/VoiceRecordComposerTile.tsx | 52 ++----------------- 5 files changed, 68 insertions(+), 81 deletions(-) diff --git a/res/css/views/audio_messages/_PlaybackContainer.scss b/res/css/views/audio_messages/_PlaybackContainer.scss index c1192f188b..fd01864bba 100644 --- a/res/css/views/audio_messages/_PlaybackContainer.scss +++ b/res/css/views/audio_messages/_PlaybackContainer.scss @@ -27,9 +27,14 @@ limitations under the License. display: flex; align-items: center; + contain: content; + .mx_Waveform { .mx_Waveform_bar { background-color: $voice-record-waveform-incomplete-fg-color; + height: 100%; + /* Variable set by a JS component */ + transform: scaleY(max(0.05, var(--barHeight))); &.mx_Waveform_bar_100pct { // Small animation to remove the mechanical feel of progress diff --git a/src/components/views/audio_messages/LiveRecordingClock.tsx b/src/components/views/audio_messages/LiveRecordingClock.tsx index 41909dc201..a9dbd3c52f 100644 --- a/src/components/views/audio_messages/LiveRecordingClock.tsx +++ b/src/components/views/audio_messages/LiveRecordingClock.tsx @@ -18,6 +18,7 @@ import React from "react"; import { IRecordingUpdate, VoiceRecording } from "../../../voice/VoiceRecording"; import { replaceableComponent } from "../../../utils/replaceableComponent"; import Clock from "./Clock"; +import { MarkedExecution } from "../../../utils/MarkedExecution"; interface IProps { recorder: VoiceRecording; @@ -32,16 +33,31 @@ interface IState { */ @replaceableComponent("views.audio_messages.LiveRecordingClock") export default class LiveRecordingClock extends React.PureComponent { - public constructor(props) { - super(props); + private seconds = 0; + private scheduledUpdate = new MarkedExecution( + () => this.updateClock(), + () => requestAnimationFrame(() => this.scheduledUpdate.trigger()), + ); - this.state = {seconds: 0}; - this.props.recorder.liveData.onUpdate(this.onRecordingUpdate); + constructor(props) { + super(props); + this.state = { + seconds: 0, + }; } - private onRecordingUpdate = (update: IRecordingUpdate) => { - this.setState({seconds: update.timeSeconds}); - }; + componentDidMount() { + this.props.recorder.liveData.onUpdate((update: IRecordingUpdate) => { + this.seconds = update.timeSeconds; + this.scheduledUpdate.mark(); + }); + } + + private updateClock() { + this.setState({ + seconds: this.seconds, + }); + } public render() { return ; diff --git a/src/components/views/audio_messages/LiveRecordingWaveform.tsx b/src/components/views/audio_messages/LiveRecordingWaveform.tsx index 27d165e613..6e88346cae 100644 --- a/src/components/views/audio_messages/LiveRecordingWaveform.tsx +++ b/src/components/views/audio_messages/LiveRecordingWaveform.tsx @@ -20,13 +20,14 @@ import { replaceableComponent } from "../../../utils/replaceableComponent"; import { arrayFastResample, arraySeed } from "../../../utils/arrays"; import { percentageOf } from "../../../utils/numbers"; import Waveform from "./Waveform"; +import { MarkedExecution } from "../../../utils/MarkedExecution"; interface IProps { recorder: VoiceRecording; } interface IState { - heights: number[]; + waveform: number[]; } /** @@ -34,27 +35,35 @@ interface IState { */ @replaceableComponent("views.audio_messages.LiveRecordingWaveform") export default class LiveRecordingWaveform extends React.PureComponent { - public constructor(props) { - super(props); - - this.state = {heights: arraySeed(0, RECORDING_PLAYBACK_SAMPLES)}; - this.props.recorder.liveData.onUpdate(this.onRecordingUpdate); - } - - private onRecordingUpdate = (update: IRecordingUpdate) => { - // The waveform and the downsample target are pretty close, so we should be fine to - // do this, despite the docs on arrayFastResample. - const bars = arrayFastResample(Array.from(update.waveform), RECORDING_PLAYBACK_SAMPLES); - this.setState({ - // The incoming data is between zero and one, but typically even screaming into a - // microphone won't send you over 0.6, so we artificially adjust the gain for the - // waveform. This results in a slightly more cinematic/animated waveform for the - // user. - heights: bars.map(b => percentageOf(b, 0, 0.50)), - }); + public static defaultProps = { + progress: 1, }; + private waveform: number[] = []; + private scheduledUpdate = new MarkedExecution( + () => this.updateWaveform(), + () => requestAnimationFrame(() => this.scheduledUpdate.trigger()), + ); + + constructor(props) { + super(props); + this.state = { + waveform: [], + }; + } + + componentDidMount() { + this.props.recorder.liveData.onUpdate((update: IRecordingUpdate) => { + this.waveform = update.waveform; + this.scheduledUpdate.mark(); + }); + } + + private updateWaveform() { + this.setState({ waveform: this.waveform }); + } + public render() { - return ; + return ; } } diff --git a/src/components/views/audio_messages/Waveform.tsx b/src/components/views/audio_messages/Waveform.tsx index dc7551025e..3b7a881754 100644 --- a/src/components/views/audio_messages/Waveform.tsx +++ b/src/components/views/audio_messages/Waveform.tsx @@ -17,6 +17,11 @@ limitations under the License. import React from "react"; import { replaceableComponent } from "../../../utils/replaceableComponent"; import classNames from "classnames"; +import { CSSProperties } from "react"; + +interface WaveformCSSProperties extends CSSProperties { + '--barHeight': number; +} interface IProps { relHeights: number[]; // relative heights (0-1) @@ -40,10 +45,6 @@ export default class Waveform extends React.PureComponent { progress: 1, }; - public constructor(props) { - super(props); - } - public render() { return
{this.props.relHeights.map((h, i) => { @@ -53,7 +54,9 @@ export default class Waveform extends React.PureComponent { 'mx_Waveform_bar': true, 'mx_Waveform_bar_100pct': isCompleteBar, }); - return ; + return ; })}
; } diff --git a/src/components/views/rooms/VoiceRecordComposerTile.tsx b/src/components/views/rooms/VoiceRecordComposerTile.tsx index c5a196c377..4e8f258ee8 100644 --- a/src/components/views/rooms/VoiceRecordComposerTile.tsx +++ b/src/components/views/rooms/VoiceRecordComposerTile.tsx @@ -18,22 +18,18 @@ import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import { _t } from "../../../languageHandler"; import React, {ReactNode} from "react"; import { - IRecordingUpdate, - RECORDING_PLAYBACK_SAMPLES, RecordingState, VoiceRecording, } from "../../../voice/VoiceRecording"; import {Room} from "matrix-js-sdk/src/models/room"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import classNames from "classnames"; -import LiveRecordingWaveform from "../voice_messages/LiveRecordingWaveform"; +import LiveRecordingWaveform from "../audio_messages/LiveRecordingWaveform"; import { replaceableComponent } from "../../../utils/replaceableComponent"; -import { arrayFastResample, arraySeed } from "../../../utils/arrays"; -import { percentageOf } from "../../../utils/numbers"; -import LiveRecordingClock from "../voice_messages/LiveRecordingClock"; +import LiveRecordingClock from "../audio_messages/LiveRecordingClock"; import { VoiceRecordingStore } from "../../../stores/VoiceRecordingStore"; import {UPDATE_EVENT} from "../../../stores/AsyncStore"; -import RecordingPlayback from "../voice_messages/RecordingPlayback"; +import RecordingPlayback from "../audio_messages/RecordingPlayback"; import { MsgType } from "matrix-js-sdk/src/@types/event"; import Modal from "../../../Modal"; import ErrorDialog from "../dialogs/ErrorDialog"; @@ -46,8 +42,6 @@ interface IProps { interface IState { recorder?: VoiceRecording; recordingPhase?: RecordingState; - relHeights: number[]; - seconds: number; } /** @@ -55,58 +49,18 @@ interface IState { */ @replaceableComponent("views.rooms.VoiceRecordComposerTile") export default class VoiceRecordComposerTile extends React.PureComponent { - private waveform: number[] = []; - private seconds = 0; - private scheduledAnimationFrame = false; - public constructor(props) { super(props); this.state = { recorder: null, // no recording started by default - seconds: 0, - relHeights: arraySeed(0, RECORDING_PLAYBACK_SAMPLES), }; } - public componentDidUpdate(prevProps, prevState) { - if (!prevState.recorder && this.state.recorder) { - this.state.recorder.liveData.onUpdate(this.onRecordingUpdate); - } - } - public async componentWillUnmount() { await VoiceRecordingStore.instance.disposeRecording(); } - private onRecordingUpdate = (update: IRecordingUpdate): void => { - this.waveform = update.waveform; - this.seconds = update.timeSeconds; - - if (this.scheduledAnimationFrame) { - return; - } - - this.scheduledAnimationFrame = true; - // The audio recorder flushes data faster than the screen refresh rate - // Using requestAnimationFrame makes sure that we only flush the data - // to react once per tick to avoid unneeded work. - requestAnimationFrame(() => { - // The waveform and the downsample target are pretty close, so we should be fine to - // do this, despite the docs on arrayFastResample. - const bars = arrayFastResample(Array.from(this.waveform), RECORDING_PLAYBACK_SAMPLES); - this.setState({ - // The incoming data is between zero and one, but typically even screaming into a - // microphone won't send you over 0.6, so we artificially adjust the gain for the - // waveform. This results in a slightly more cinematic/animated waveform for the - // user. - relHeights: bars.map(b => percentageOf(b, 0, 0.50)), - seconds: this.seconds, - }); - this.scheduledAnimationFrame = false; - }); - } - // called by composer public async send() { if (!this.state.recorder) { From 7a4ceec9858c38c3ff90912553df60ec5686f808 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 28 Jun 2021 21:02:42 -0600 Subject: [PATCH 069/221] Restore clamped dimensions on voice recorder waveform --- .../views/audio_messages/LiveRecordingWaveform.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/components/views/audio_messages/LiveRecordingWaveform.tsx b/src/components/views/audio_messages/LiveRecordingWaveform.tsx index 6e88346cae..b9c5f80f05 100644 --- a/src/components/views/audio_messages/LiveRecordingWaveform.tsx +++ b/src/components/views/audio_messages/LiveRecordingWaveform.tsx @@ -17,7 +17,7 @@ limitations under the License. import React from "react"; import { IRecordingUpdate, RECORDING_PLAYBACK_SAMPLES, VoiceRecording } from "../../../voice/VoiceRecording"; import { replaceableComponent } from "../../../utils/replaceableComponent"; -import { arrayFastResample, arraySeed } from "../../../utils/arrays"; +import { arrayFastResample } from "../../../utils/arrays"; import { percentageOf } from "../../../utils/numbers"; import Waveform from "./Waveform"; import { MarkedExecution } from "../../../utils/MarkedExecution"; @@ -54,7 +54,12 @@ export default class LiveRecordingWaveform extends React.PureComponent { - this.waveform = update.waveform; + const bars = arrayFastResample(Array.from(update.waveform), RECORDING_PLAYBACK_SAMPLES); + // The incoming data is between zero and one, but typically even screaming into a + // microphone won't send you over 0.6, so we artificially adjust the gain for the + // waveform. This results in a slightly more cinematic/animated waveform for the + // user. + this.waveform = bars.map(b => percentageOf(b, 0, 0.50)); this.scheduledUpdate.mark(); }); } From 18407fbce9dff7434a134091cc9f5c30daf7b91b Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 29 Jun 2021 14:36:24 +0100 Subject: [PATCH 070/221] Upgrade matrix-js-sdk to 12.0.1-rc.1 --- package.json | 2 +- yarn.lock | 28 +++++++++++++++++++++++----- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index bae7b1e4cf..c30f0ea394 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,7 @@ "katex": "^0.12.0", "linkifyjs": "^2.1.9", "lodash": "^4.17.20", - "matrix-js-sdk": "12.0.0", + "matrix-js-sdk": "12.0.1-rc.1", "matrix-widget-api": "^0.1.0-beta.15", "minimist": "^1.2.5", "opus-recorder": "^8.0.3", diff --git a/yarn.lock b/yarn.lock index 3bcb8de404..8bef69428a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1334,7 +1334,6 @@ "@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz": version "3.2.3" - uid cc332fdd25c08ef0e40f4d33fc3f822a0f98b6f4 resolved "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz#cc332fdd25c08ef0e40f4d33fc3f822a0f98b6f4" "@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents": @@ -1681,6 +1680,11 @@ "@types/scheduler" "*" csstype "^3.0.2" +"@types/retry@^0.12.0": + version "0.12.0" + resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d" + integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA== + "@types/sanitize-html@^2.3.1": version "2.3.1" resolved "https://registry.yarnpkg.com/@types/sanitize-html/-/sanitize-html-2.3.1.tgz#094d696b83b7394b016e96342bbffa6a028795ce" @@ -5737,10 +5741,10 @@ mathml-tag-names@^2.1.3: resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz#4ddadd67308e780cf16a47685878ee27b736a0a3" integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg== -matrix-js-sdk@12.0.0: - version "12.0.0" - resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-12.0.0.tgz#8ee7cc37661476341d0c792a1a12bc78b19f9fdd" - integrity sha512-DHeq87Sx9Dv37FYyvZkmA1VYsQUNaVgc3QzMUkFwoHt1T4EZzgyYpdsp3uYruJzUW0ACvVJcwFdrU4e1VS97dQ== +matrix-js-sdk@12.0.1-rc.1: + version "12.0.1-rc.1" + resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-12.0.1-rc.1.tgz#da8a1c38e65ccfe9cb53a6aaf681488df0667ad5" + integrity sha512-ksz/oFh4hc/+gbNQ3OIGKhuVd9Vy9lUYhoaSF2efxO4NnPTwbjLNFKzgmQ+CV5ixUgmUcn972Tv0vyNZH9DTGg== dependencies: "@babel/runtime" "^7.12.5" another-json "^0.2.0" @@ -5748,6 +5752,7 @@ matrix-js-sdk@12.0.0: bs58 "^4.0.1" content-type "^1.0.4" loglevel "^1.7.1" + p-retry "^4.5.0" qs "^6.9.6" request "^2.88.2" unhomoglyph "^1.0.6" @@ -6330,6 +6335,14 @@ p-locate@^4.1.0: dependencies: p-limit "^2.2.0" +p-retry@^4.5.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-4.6.0.tgz#9de15ae696278cffe86fce2d8f73b7f894f8bc9e" + integrity sha512-SAHbQEwg3X5DRNaLmWjT+DlGc93ba5i+aP3QLfVNDncQEQO4xjbYW4N/lcVTSuP0aJietGfx2t94dJLzfBMpXw== + dependencies: + "@types/retry" "^0.12.0" + retry "^0.13.1" + p-try@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" @@ -7200,6 +7213,11 @@ ret@~0.1.10: resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== +retry@^0.13.1: + version "0.13.1" + resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658" + integrity sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg== + reusify@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" From 70fc51866765e305e64c8ed086759b6e9c84eac6 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 29 Jun 2021 14:43:00 +0100 Subject: [PATCH 071/221] Prepare changelog for v3.25.0-rc.1 --- CHANGELOG.md | 163 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 163 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f979b4802..efd55925a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,166 @@ +Changes in [3.25.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.25.0-rc.1) (2021-06-29) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.24.0...v3.25.0-rc.1) + + * Update to js-sdk v12.0.1-rc.1 + * Translations update from Weblate + [\#6286](https://github.com/matrix-org/matrix-react-sdk/pull/6286) + * Fix back button on user info card after clicking a permalink + [\#6277](https://github.com/matrix-org/matrix-react-sdk/pull/6277) + * Group ACLs with MELS + [\#6280](https://github.com/matrix-org/matrix-react-sdk/pull/6280) + * Fix editState not getting passed through + [\#6282](https://github.com/matrix-org/matrix-react-sdk/pull/6282) + * Migrate message context menu to IconizedContextMenu + [\#5671](https://github.com/matrix-org/matrix-react-sdk/pull/5671) + * Improve audio recording performance + [\#6240](https://github.com/matrix-org/matrix-react-sdk/pull/6240) + * Fix multiple timeline panels handling composer and edit events + [\#6278](https://github.com/matrix-org/matrix-react-sdk/pull/6278) + * Let m.notice messages mark a room as unread + [\#6281](https://github.com/matrix-org/matrix-react-sdk/pull/6281) + * Removes the override on the Bubble Container + [\#5953](https://github.com/matrix-org/matrix-react-sdk/pull/5953) + * Fix IRC layout regressions + [\#6193](https://github.com/matrix-org/matrix-react-sdk/pull/6193) + * Fix trashcan.svg by exporting it with its viewbox + [\#6248](https://github.com/matrix-org/matrix-react-sdk/pull/6248) + * Fix tiny scrollbar dot on chrome/electron in Forward Dialog + [\#6276](https://github.com/matrix-org/matrix-react-sdk/pull/6276) + * Upgrade puppeteer to use newer version of Chrome + [\#6268](https://github.com/matrix-org/matrix-react-sdk/pull/6268) + * Make toast dismiss button less prominent + [\#6275](https://github.com/matrix-org/matrix-react-sdk/pull/6275) + * Encrypt the voice message file if needed + [\#6269](https://github.com/matrix-org/matrix-react-sdk/pull/6269) + * Fix hyper-precise presence + [\#6270](https://github.com/matrix-org/matrix-react-sdk/pull/6270) + * Fix issues around private spaces, including previewable + [\#6265](https://github.com/matrix-org/matrix-react-sdk/pull/6265) + * Make _pinned messages_ in `m.room.pinned_events` event clickable + [\#6257](https://github.com/matrix-org/matrix-react-sdk/pull/6257) + * Fix space avatar management layout being broken + [\#6266](https://github.com/matrix-org/matrix-react-sdk/pull/6266) + * Convert EntityTile, MemberTile and PresenceLabel to TS + [\#6251](https://github.com/matrix-org/matrix-react-sdk/pull/6251) + * Fix UserInfo not working when rendered without a room + [\#6260](https://github.com/matrix-org/matrix-react-sdk/pull/6260) + * Update membership reason handling, including leave reason displaying + [\#6253](https://github.com/matrix-org/matrix-react-sdk/pull/6253) + * Consolidate types with js-sdk changes + [\#6220](https://github.com/matrix-org/matrix-react-sdk/pull/6220) + * Fix edit history modal + [\#6258](https://github.com/matrix-org/matrix-react-sdk/pull/6258) + * Convert MemberList to TS + [\#6249](https://github.com/matrix-org/matrix-react-sdk/pull/6249) + * Fix two PRs duplicating the css attribute + [\#6259](https://github.com/matrix-org/matrix-react-sdk/pull/6259) + * Improve invite error messages in InviteDialog for room invites + [\#6201](https://github.com/matrix-org/matrix-react-sdk/pull/6201) + * Fix invite dialog being cut off when it has limited results + [\#6256](https://github.com/matrix-org/matrix-react-sdk/pull/6256) + * Fix pinning event in a room which hasn't had events pinned in before + [\#6255](https://github.com/matrix-org/matrix-react-sdk/pull/6255) + * Allow modal widget buttons to be disabled when the modal opens + [\#6178](https://github.com/matrix-org/matrix-react-sdk/pull/6178) + * Decrease e2e shield fill mask size so that it doesn't overlap + [\#6250](https://github.com/matrix-org/matrix-react-sdk/pull/6250) + * Dial Pad UI bug fixes + [\#5786](https://github.com/matrix-org/matrix-react-sdk/pull/5786) + * Simple handling of mid-call output changes + [\#6247](https://github.com/matrix-org/matrix-react-sdk/pull/6247) + * Improve ForwardDialog performance by using TruncatedList + [\#6228](https://github.com/matrix-org/matrix-react-sdk/pull/6228) + * Fix dependency and lockfile mismatch + [\#6246](https://github.com/matrix-org/matrix-react-sdk/pull/6246) + * Improve room directory click behaviour + [\#6234](https://github.com/matrix-org/matrix-react-sdk/pull/6234) + * Fix keyboard accessibility of the space panel + [\#6239](https://github.com/matrix-org/matrix-react-sdk/pull/6239) + * Add ways to manage addresses for Spaces + [\#6151](https://github.com/matrix-org/matrix-react-sdk/pull/6151) + * Hide communities invites and the community autocompleter when Spaces on + [\#6244](https://github.com/matrix-org/matrix-react-sdk/pull/6244) + * Convert bunch of files to TS + [\#6241](https://github.com/matrix-org/matrix-react-sdk/pull/6241) + * Open local addresses section by default when there are no existing local + addresses + [\#6179](https://github.com/matrix-org/matrix-react-sdk/pull/6179) + * Allow reordering of the space panel via Drag and Drop + [\#6137](https://github.com/matrix-org/matrix-react-sdk/pull/6137) + * Replace drag and drop mechanism in communities with something simpler + [\#6134](https://github.com/matrix-org/matrix-react-sdk/pull/6134) + * EventTilePreview fixes + [\#6000](https://github.com/matrix-org/matrix-react-sdk/pull/6000) + * Upgrade @types/react and @types/react-dom + [\#6233](https://github.com/matrix-org/matrix-react-sdk/pull/6233) + * Fix type error in the SpaceStore + [\#6242](https://github.com/matrix-org/matrix-react-sdk/pull/6242) + * Add experimental options to the Spaces beta + [\#6199](https://github.com/matrix-org/matrix-react-sdk/pull/6199) + * Consolidate types with js-sdk changes + [\#6215](https://github.com/matrix-org/matrix-react-sdk/pull/6215) + * Fix branch matching for Buildkite + [\#6236](https://github.com/matrix-org/matrix-react-sdk/pull/6236) + * Migrate SearchBar to TypeScript + [\#6230](https://github.com/matrix-org/matrix-react-sdk/pull/6230) + * Add support to keyboard shortcuts dialog for [digits] + [\#6088](https://github.com/matrix-org/matrix-react-sdk/pull/6088) + * Fix modal opening race condition + [\#6238](https://github.com/matrix-org/matrix-react-sdk/pull/6238) + * Deprecate FormButton in favour of AccessibleButton + [\#6229](https://github.com/matrix-org/matrix-react-sdk/pull/6229) + * Add PR template + [\#6216](https://github.com/matrix-org/matrix-react-sdk/pull/6216) + * Prefer canonical aliases while autocompleting rooms + [\#6222](https://github.com/matrix-org/matrix-react-sdk/pull/6222) + * Fix quote button + [\#6232](https://github.com/matrix-org/matrix-react-sdk/pull/6232) + * Restore branch matching support for GitHub Actions e2e tests + [\#6224](https://github.com/matrix-org/matrix-react-sdk/pull/6224) + * Fix View Source accessing renamed private field on MatrixEvent + [\#6225](https://github.com/matrix-org/matrix-react-sdk/pull/6225) + * Fix ConfirmUserActionDialog returning an input field rather than text + [\#6219](https://github.com/matrix-org/matrix-react-sdk/pull/6219) + * Revert "Partially restore immutable event objects at the rendering layer" + [\#6221](https://github.com/matrix-org/matrix-react-sdk/pull/6221) + * Add jq to e2e tests Dockerfile + [\#6218](https://github.com/matrix-org/matrix-react-sdk/pull/6218) + * Partially restore immutable event objects at the rendering layer + [\#6196](https://github.com/matrix-org/matrix-react-sdk/pull/6196) + * Update MSC number references for voice messages + [\#6197](https://github.com/matrix-org/matrix-react-sdk/pull/6197) + * Fix phase enum usage in JS modules as well + [\#6214](https://github.com/matrix-org/matrix-react-sdk/pull/6214) + * Migrate some dialogs to TypeScript + [\#6185](https://github.com/matrix-org/matrix-react-sdk/pull/6185) + * Typescript fixes due to MatrixEvent being TSified + [\#6208](https://github.com/matrix-org/matrix-react-sdk/pull/6208) + * Allow click-to-ping, quote & emoji picker for edit composer too + [\#5858](https://github.com/matrix-org/matrix-react-sdk/pull/5858) + * Add call silencing + [\#6082](https://github.com/matrix-org/matrix-react-sdk/pull/6082) + * Fix types in SlashCommands + [\#6207](https://github.com/matrix-org/matrix-react-sdk/pull/6207) + * Benchmark multiple common user scenario + [\#6190](https://github.com/matrix-org/matrix-react-sdk/pull/6190) + * Fix forward dialog message preview display names + [\#6204](https://github.com/matrix-org/matrix-react-sdk/pull/6204) + * Remove stray bullet point in reply preview + [\#6206](https://github.com/matrix-org/matrix-react-sdk/pull/6206) + * Stop requesting null next replies from the server + [\#6203](https://github.com/matrix-org/matrix-react-sdk/pull/6203) + * Fix soft crash caused by a broken shouldComponentUpdate + [\#6202](https://github.com/matrix-org/matrix-react-sdk/pull/6202) + * Keep composer reply when scrolling away from a highlighted event + [\#6200](https://github.com/matrix-org/matrix-react-sdk/pull/6200) + * Cache virtual/native room mappings when they're created + [\#6194](https://github.com/matrix-org/matrix-react-sdk/pull/6194) + * Disable comment-on-alert + [\#6191](https://github.com/matrix-org/matrix-react-sdk/pull/6191) + * Bump postcss from 7.0.35 to 7.0.36 + [\#6195](https://github.com/matrix-org/matrix-react-sdk/pull/6195) + Changes in [3.24.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.24.0) (2021-06-21) ===================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.24.0-rc.1...v3.24.0) From a48d453be12f85ec5afea4e2c1bdf75c35d7aa64 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 29 Jun 2021 14:43:01 +0100 Subject: [PATCH 072/221] v3.25.0-rc.1 --- package.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index c30f0ea394..1994de4453 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "3.24.0", + "version": "3.25.0-rc.1", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { @@ -25,7 +25,7 @@ "bin": { "reskindex": "scripts/reskindex.js" }, - "main": "./src/index.js", + "main": "./lib/index.js", "matrix_src_main": "./src/index.js", "matrix_lib_main": "./lib/index.js", "matrix_lib_typings": "./lib/index.d.ts", @@ -196,5 +196,6 @@ "coverageReporters": [ "text" ] - } + }, + "typings": "./lib/index.d.ts" } From fdced3da1b156fefef6011e1f1395052b9fbddbe Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Wed, 30 Jun 2021 08:09:55 +0100 Subject: [PATCH 073/221] Remove reminescent references to the tinter --- res/css/structures/_GroupView.scss | 2 +- res/css/views/messages/_MImageBody.scss | 2 +- src/components/structures/RoomView.tsx | 9 --------- src/components/views/rooms/SimpleRoomHeader.js | 2 +- 4 files changed, 3 insertions(+), 12 deletions(-) diff --git a/res/css/structures/_GroupView.scss b/res/css/structures/_GroupView.scss index 2350d9f28a..60f9ebdd08 100644 --- a/res/css/structures/_GroupView.scss +++ b/res/css/structures/_GroupView.scss @@ -323,7 +323,7 @@ limitations under the License. } .mx_GroupView_featuredThing .mx_BaseAvatar { - /* To prevent misalignment with mx_TintableSvg (in addButton) */ + /* To prevent misalignment with img (in addButton) */ vertical-align: initial; } diff --git a/res/css/views/messages/_MImageBody.scss b/res/css/views/messages/_MImageBody.scss index 1c773c2f06..515d867da5 100644 --- a/res/css/views/messages/_MImageBody.scss +++ b/res/css/views/messages/_MImageBody.scss @@ -43,7 +43,7 @@ limitations under the License. top: 50%; } -// Inner img and TintableSvg should be centered around 0, 0 +// Inner img should be centered around 0, 0 .mx_MImageBody_thumbnail_spinner > * { transform: translate(-50%, -50%); } diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 61685e7ba8..81000a87a6 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -1054,11 +1054,6 @@ export default class RoomView extends React.Component { }); } - private updateTint() { - const room = this.state.room; - if (!room) return; - } - private onAccountData = (event: MatrixEvent) => { const type = event.getType(); if ((type === "org.matrix.preview_urls" || type === "im.vector.web.settings") && this.state.room) { @@ -1705,10 +1700,6 @@ export default class RoomView extends React.Component { // otherwise react calls it with null on each update. private gatherTimelinePanelRef = r => { this.messagePanel = r; - if (r) { - console.log("updateTint from RoomView.gatherTimelinePanelRef"); - this.updateTint(); - } }; private getOldRoom() { diff --git a/src/components/views/rooms/SimpleRoomHeader.js b/src/components/views/rooms/SimpleRoomHeader.js index 2133ccabcd..768a456b35 100644 --- a/src/components/views/rooms/SimpleRoomHeader.js +++ b/src/components/views/rooms/SimpleRoomHeader.js @@ -27,7 +27,7 @@ export default class SimpleRoomHeader extends React.Component { static propTypes = { title: PropTypes.string, - // `src` to a TintableSvg. Optional. + // `src` to an image. Optional. icon: PropTypes.string, }; From f3b4a2181572407cdb345123509572b972bc9494 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Wed, 30 Jun 2021 09:02:00 +0100 Subject: [PATCH 074/221] Remove Tinter reference --- src/components/views/messages/MStickerBody.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/views/messages/MStickerBody.js b/src/components/views/messages/MStickerBody.js index aca3dba37c..eb3635b0c0 100644 --- a/src/components/views/messages/MStickerBody.js +++ b/src/components/views/messages/MStickerBody.js @@ -42,8 +42,7 @@ export default class MStickerBody extends MImageBody { // Placeholder to show in place of the sticker image if // img onLoad hasn't fired yet. getPlaceholder() { - const TintableSVG = sdk.getComponent('elements.TintableSvg'); - return ; + return ; } // Tooltip to show on mouse over From e768ecb3d0806e5cc1e247ae24b666e512b0fb6d Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 30 Jun 2021 13:01:26 +0100 Subject: [PATCH 075/221] Typescript conversion of Composer components and more --- src/CountlyAnalytics.ts | 3 +- src/HtmlUtils.tsx | 4 +- src/SlashCommands.tsx | 15 +- .../{TextualBody.js => TextualBody.tsx} | 196 +++++++-------- .../{TextualEvent.js => TextualEvent.tsx} | 19 +- .../views/rooms/BasicMessageComposer.tsx | 96 ++++---- ...ageComposer.js => EditMessageComposer.tsx} | 223 +++++++++--------- .../views/rooms/MessageComposer.tsx | 10 +- ...matBar.js => MessageComposerFormatBar.tsx} | 84 ++++--- ...ageComposer.js => SendMessageComposer.tsx} | 186 ++++++++------- src/editor/model.ts | 54 ++--- src/editor/parts.ts | 8 +- src/utils/EditorStateTransfer.ts | 14 +- src/utils/{pillify.js => pillify.tsx} | 20 +- .../views/rooms/SendMessageComposer-test.js | 4 +- 15 files changed, 492 insertions(+), 444 deletions(-) rename src/components/views/messages/{TextualBody.js => TextualBody.tsx} (80%) rename src/components/views/messages/{TextualEvent.js => TextualEvent.tsx} (72%) rename src/components/views/rooms/{EditMessageComposer.js => EditMessageComposer.tsx} (70%) rename src/components/views/rooms/{MessageComposerFormatBar.js => MessageComposerFormatBar.tsx} (55%) rename src/components/views/rooms/{SendMessageComposer.js => SendMessageComposer.tsx} (78%) rename src/utils/{pillify.js => pillify.tsx} (91%) diff --git a/src/CountlyAnalytics.ts b/src/CountlyAnalytics.ts index 39dcac4048..3ad56fe3bf 100644 --- a/src/CountlyAnalytics.ts +++ b/src/CountlyAnalytics.ts @@ -15,6 +15,7 @@ limitations under the License. */ import { randomString } from "matrix-js-sdk/src/randomstring"; +import { IContent } from "matrix-js-sdk/src/models/event"; import { getCurrentLanguage } from './languageHandler'; import PlatformPeg from './PlatformPeg'; @@ -868,7 +869,7 @@ export default class CountlyAnalytics { roomId: string, isEdit: boolean, isReply: boolean, - content: {format?: string, msgtype: string}, + content: IContent, ) { if (this.disabled) return; const cli = MatrixClientPeg.get(); diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index c80b50c566..90ee8067bb 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -358,11 +358,11 @@ interface IOpts { stripReplyFallback?: boolean; returnString?: boolean; forComposerQuote?: boolean; - ref?: React.Ref; + ref?: React.Ref; } export interface IOptsReturnNode extends IOpts { - returnString: false; + returnString: false | undefined; } export interface IOptsReturnString extends IOpts { diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx index 0f38c5fffc..128ca9e5e2 100644 --- a/src/SlashCommands.tsx +++ b/src/SlashCommands.tsx @@ -1181,7 +1181,7 @@ export const Commands = [ ]; // build a map from names and aliases to the Command objects. -export const CommandMap = new Map(); +export const CommandMap = new Map(); Commands.forEach(cmd => { CommandMap.set(cmd.command, cmd); cmd.aliases.forEach(alias => { @@ -1189,15 +1189,15 @@ Commands.forEach(cmd => { }); }); -export function parseCommandString(input: string) { +export function parseCommandString(input: string): { cmd?: string, args?: string } { // trim any trailing whitespace, as it can confuse the parser for // IRC-style commands input = input.replace(/\s+$/, ''); if (input[0] !== '/') return {}; // not a command const bits = input.match(/^(\S+?)(?:[ \n]+((.|\n)*))?$/); - let cmd; - let args; + let cmd: string; + let args: string; if (bits) { cmd = bits[1].substring(1).toLowerCase(); args = bits[2]; @@ -1208,6 +1208,11 @@ export function parseCommandString(input: string) { return { cmd, args }; } +interface ICmd { + cmd?: Command; + args?: string; +} + /** * Process the given text for /commands and return a bound method to perform them. * @param {string} roomId The room in which the command was performed. @@ -1216,7 +1221,7 @@ export function parseCommandString(input: string) { * processing the command, or 'promise' if a request was sent out. * Returns null if the input didn't match a command. */ -export function getCommand(input: string) { +export function getCommand(input: string): ICmd { const { cmd, args } = parseCommandString(input); if (CommandMap.has(cmd) && CommandMap.get(cmd).isEnabled()) { diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.tsx similarity index 80% rename from src/components/views/messages/TextualBody.js rename to src/components/views/messages/TextualBody.tsx index ffaaaada4d..cb9de013bf 100644 --- a/src/components/views/messages/TextualBody.js +++ b/src/components/views/messages/TextualBody.tsx @@ -1,7 +1,5 @@ /* -Copyright 2015, 2016 OpenMarket Ltd -Copyright 2017 New Vector Ltd -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2015 - 2021 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. @@ -16,134 +14,151 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { createRef } from 'react'; +import React, { createRef, SyntheticEvent } from 'react'; import ReactDOM from 'react-dom'; -import PropTypes from 'prop-types'; import highlight from 'highlight.js'; +import { MatrixEvent } from 'matrix-js-sdk/src/models/event'; +import { MsgType } from "matrix-js-sdk/lib/@types/event"; + import * as HtmlUtils from '../../../HtmlUtils'; import { formatDate } from '../../../DateUtils'; -import * as sdk from '../../../index'; import Modal from '../../../Modal'; import dis from '../../../dispatcher/dispatcher'; import { _t } from '../../../languageHandler'; import * as ContextMenu from '../../structures/ContextMenu'; +import { toRightOf } from '../../structures/ContextMenu'; import SettingsStore from "../../../settings/SettingsStore"; import ReplyThread from "../elements/ReplyThread"; 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"; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import { replaceableComponent } from "../../../utils/replaceableComponent"; import UIStore from "../../../stores/UIStore"; import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload"; import { Action } from "../../../dispatcher/actions"; +import { TileShape } from '../rooms/EventTile'; +import EditorStateTransfer from "../../../utils/EditorStateTransfer"; +import GenericTextContextMenu from "../context_menus/GenericTextContextMenu"; +import Spoiler from "../elements/Spoiler"; +import QuestionDialog from "../dialogs/QuestionDialog"; +import MessageEditHistoryDialog from "../dialogs/MessageEditHistoryDialog"; +import EditMessageComposer from '../rooms/EditMessageComposer'; +import LinkPreviewWidget from '../rooms/LinkPreviewWidget'; + +interface IProps { + /* the MatrixEvent to show */ + mxEvent: MatrixEvent; + + /* a list of words to highlight */ + highlights?: string[]; + + /* link URL for the highlights */ + highlightLink?: string; + + /* should show URL previews for this event */ + showUrlPreview?: boolean; + + /* the shape of the tile, used */ + tileShape?: TileShape; + + editState?: EditorStateTransfer; + replacingEventId?: string; + + /* callback for when our widget has loaded */ + onHeightChanged(): void, +} + +interface IState { + // the URLs (if any) to be previewed with a LinkPreviewWidget inside this TextualBody. + links: string[]; + + // track whether the preview widget is hidden + widgetHidden: boolean; +} @replaceableComponent("views.messages.TextualBody") -export default class TextualBody extends React.Component { - static propTypes = { - /* the MatrixEvent to show */ - mxEvent: PropTypes.object.isRequired, +export default class TextualBody extends React.Component { + private readonly contentRef = createRef(); - /* a list of words to highlight */ - highlights: PropTypes.array, - - /* link URL for the highlights */ - highlightLink: PropTypes.string, - - /* should show URL previews for this event */ - showUrlPreview: PropTypes.bool, - - /* callback for when our widget has loaded */ - onHeightChanged: PropTypes.func, - - /* the shape of the tile, used */ - tileShape: PropTypes.string, - }; + private unmounted = false; + private pills: Element[] = []; constructor(props) { super(props); - this._content = createRef(); - this.state = { - // the URLs (if any) to be previewed with a LinkPreviewWidget - // inside this TextualBody. links: [], - - // track whether the preview widget is hidden widgetHidden: false, }; } componentDidMount() { - this._unmounted = false; - this._pills = []; if (!this.props.editState) { - this._applyFormatting(); + this.applyFormatting(); } } - _applyFormatting() { + private applyFormatting(): void { const showLineNumbers = SettingsStore.getValue("showCodeLineNumbers"); - this.activateSpoilers([this._content.current]); + this.activateSpoilers([this.contentRef.current]); // pillifyLinks BEFORE linkifyElement because plain room/user URLs in the composer // are still sent as plaintext URLs. If these are ever pillified in the composer, // we should be pillify them here by doing the linkifying BEFORE the pillifying. - pillifyLinks([this._content.current], this.props.mxEvent, this._pills); - HtmlUtils.linkifyElement(this._content.current); + pillifyLinks([this.contentRef.current], this.props.mxEvent, this.pills); + HtmlUtils.linkifyElement(this.contentRef.current); this.calculateUrlPreview(); if (this.props.mxEvent.getContent().format === "org.matrix.custom.html") { // Handle expansion and add buttons - const pres = ReactDOM.findDOMNode(this).getElementsByTagName("pre"); + const pres = (ReactDOM.findDOMNode(this) as Element).getElementsByTagName("pre"); if (pres.length > 0) { for (let i = 0; i < pres.length; i++) { // If there already is a div wrapping the codeblock we want to skip this. // This happens after the codeblock was edited. - if (pres[i].parentNode.className == "mx_EventTile_pre_container") continue; + if (pres[i].parentElement.className == "mx_EventTile_pre_container") continue; // Add code element if it's missing since we depend on it if (pres[i].getElementsByTagName("code").length == 0) { - this._addCodeElement(pres[i]); + this.addCodeElement(pres[i]); } // Wrap a div around
 so that the copy button can be correctly positioned
                     // when the 
 overflows and is scrolled horizontally.
-                    const div = this._wrapInDiv(pres[i]);
-                    this._handleCodeBlockExpansion(pres[i]);
-                    this._addCodeExpansionButton(div, pres[i]);
-                    this._addCodeCopyButton(div);
+                    const div = this.wrapInDiv(pres[i]);
+                    this.handleCodeBlockExpansion(pres[i]);
+                    this.addCodeExpansionButton(div, pres[i]);
+                    this.addCodeCopyButton(div);
                     if (showLineNumbers) {
-                        this._addLineNumbers(pres[i]);
+                        this.addLineNumbers(pres[i]);
                     }
                 }
             }
             // Highlight code
-            const codes = ReactDOM.findDOMNode(this).getElementsByTagName("code");
+            const codes = (ReactDOM.findDOMNode(this) as Element).getElementsByTagName("code");
             if (codes.length > 0) {
                 // Do this asynchronously: parsing code takes time and we don't
                 // need to block the DOM update on it.
                 setTimeout(() => {
-                    if (this._unmounted) return;
+                    if (this.unmounted) return;
                     for (let i = 0; i < codes.length; i++) {
                         // If the code already has the hljs class we want to skip this.
                         // This happens after the codeblock was edited.
                         if (codes[i].className.includes("hljs")) continue;
-                        this._highlightCode(codes[i]);
+                        this.highlightCode(codes[i]);
                     }
                 }, 10);
             }
         }
     }
 
-    _addCodeElement(pre) {
+    private addCodeElement(pre: HTMLPreElement): void {
         const code = document.createElement("code");
         code.append(...pre.childNodes);
         pre.appendChild(code);
     }
 
-    _addCodeExpansionButton(div, pre) {
+    private addCodeExpansionButton(div: HTMLDivElement, pre: HTMLPreElement): void {
         // Calculate how many percent does the pre element take up.
         // If it's less than 30% we don't add the expansion button.
         const percentageOfViewport = pre.offsetHeight / UIStore.instance.windowHeight * 100;
@@ -175,7 +190,7 @@ export default class TextualBody extends React.Component {
         div.appendChild(button);
     }
 
-    _addCodeCopyButton(div) {
+    private addCodeCopyButton(div: HTMLDivElement): void {
         const button = document.createElement("span");
         button.className = "mx_EventTile_button mx_EventTile_copyButton ";
 
@@ -185,11 +200,10 @@ export default class TextualBody extends React.Component {
         if (expansionButtonExists.length > 0) button.className += "mx_EventTile_buttonBottom";
 
         button.onclick = async () => {
-            const copyCode = button.parentNode.getElementsByTagName("code")[0];
+            const copyCode = button.parentElement.getElementsByTagName("code")[0];
             const successful = await copyPlaintext(copyCode.textContent);
 
             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'),
@@ -200,7 +214,7 @@ export default class TextualBody extends React.Component {
         div.appendChild(button);
     }
 
-    _wrapInDiv(pre) {
+    private wrapInDiv(pre: HTMLPreElement): HTMLDivElement {
         const div = document.createElement("div");
         div.className = "mx_EventTile_pre_container";
 
@@ -212,13 +226,13 @@ export default class TextualBody extends React.Component {
         return div;
     }
 
-    _handleCodeBlockExpansion(pre) {
+    private handleCodeBlockExpansion(pre: HTMLPreElement): void {
         if (!SettingsStore.getValue("expandCodeByDefault")) {
             pre.className = "mx_EventTile_collapsedCodeBlock";
         }
     }
 
-    _addLineNumbers(pre) {
+    private addLineNumbers(pre: HTMLPreElement): void {
         // Calculate number of lines in pre
         const number = pre.innerHTML.replace(/\n(<\/code>)?$/, "").split(/\n/).length;
         pre.innerHTML = '' + pre.innerHTML + '';
@@ -229,7 +243,7 @@ export default class TextualBody extends React.Component {
         }
     }
 
-    _highlightCode(code) {
+    private highlightCode(code: HTMLElement): void {
         if (SettingsStore.getValue("enableSyntaxHighlightLanguageDetection")) {
             highlight.highlightBlock(code);
         } else {
@@ -249,14 +263,14 @@ export default class TextualBody extends React.Component {
             const stoppedEditing = prevProps.editState && !this.props.editState;
             const messageWasEdited = prevProps.replacingEventId !== this.props.replacingEventId;
             if (messageWasEdited || stoppedEditing) {
-                this._applyFormatting();
+                this.applyFormatting();
             }
         }
     }
 
     componentWillUnmount() {
-        this._unmounted = true;
-        unmountPills(this._pills);
+        this.unmounted = true;
+        unmountPills(this.pills);
     }
 
     shouldComponentUpdate(nextProps, nextState) {
@@ -273,12 +287,12 @@ export default class TextualBody extends React.Component {
                 nextState.widgetHidden !== this.state.widgetHidden);
     }
 
-    calculateUrlPreview() {
+    private calculateUrlPreview(): void {
         //console.info("calculateUrlPreview: ShowUrlPreview for %s is %s", this.props.mxEvent.getId(), this.props.showUrlPreview);
 
         if (this.props.showUrlPreview) {
             // pass only the first child which is the event tile otherwise this recurses on edited events
-            let links = this.findLinks([this._content.current]);
+            let links = this.findLinks([this.contentRef.current]);
             if (links.length) {
                 // de-duplicate the links after stripping hashes as they don't affect the preview
                 // using a set here maintains the order
@@ -291,8 +305,8 @@ export default class TextualBody extends React.Component {
                 this.setState({ links });
 
                 // lazy-load the hidden state of the preview widget from localstorage
-                if (global.localStorage) {
-                    const hidden = global.localStorage.getItem("hide_preview_" + this.props.mxEvent.getId());
+                if (window.localStorage) {
+                    const hidden = !!window.localStorage.getItem("hide_preview_" + this.props.mxEvent.getId());
                     this.setState({ widgetHidden: hidden });
                 }
             } else if (this.state.links.length) {
@@ -301,19 +315,15 @@ export default class TextualBody extends React.Component {
         }
     }
 
-    activateSpoilers(nodes) {
+    private activateSpoilers(nodes: ArrayLike): void {
         let node = nodes[0];
         while (node) {
             if (node.tagName === "SPAN" && typeof node.getAttribute("data-mx-spoiler") === "string") {
                 const spoilerContainer = document.createElement('span');
 
                 const reason = node.getAttribute("data-mx-spoiler");
-                const Spoiler = sdk.getComponent('elements.Spoiler');
                 node.removeAttribute("data-mx-spoiler"); // we don't want to recurse
-                const spoiler = ;
+                const spoiler = ;
 
                 ReactDOM.render(spoiler, spoilerContainer);
                 node.parentNode.replaceChild(spoilerContainer, node);
@@ -322,15 +332,15 @@ export default class TextualBody extends React.Component {
             }
 
             if (node.childNodes && node.childNodes.length) {
-                this.activateSpoilers(node.childNodes);
+                this.activateSpoilers(node.childNodes as NodeListOf);
             }
 
-            node = node.nextSibling;
+            node = node.nextSibling as Element;
         }
     }
 
-    findLinks(nodes) {
-        let links = [];
+    private findLinks(nodes: ArrayLike): string[] {
+        let links: string[] = [];
 
         for (let i = 0; i < nodes.length; i++) {
             const node = nodes[i];
@@ -348,7 +358,7 @@ export default class TextualBody extends React.Component {
         return links;
     }
 
-    isLinkPreviewable(node) {
+    private isLinkPreviewable(node: Element): boolean {
         // don't try to preview relative links
         if (!node.getAttribute("href").startsWith("http://") &&
             !node.getAttribute("href").startsWith("https://")) {
@@ -381,7 +391,7 @@ export default class TextualBody extends React.Component {
         }
     }
 
-    onCancelClick = event => {
+    private onCancelClick = (): void => {
         this.setState({ widgetHidden: true });
         // FIXME: persist this somewhere smarter than local storage
         if (global.localStorage) {
@@ -390,7 +400,7 @@ export default class TextualBody extends React.Component {
         this.forceUpdate();
     };
 
-    onEmoteSenderClick = event => {
+    private onEmoteSenderClick = (): void => {
         const mxEvent = this.props.mxEvent;
         dis.dispatch({
             action: Action.ComposerInsert,
@@ -398,7 +408,7 @@ export default class TextualBody extends React.Component {
         });
     };
 
-    getEventTileOps = () => ({
+    public getEventTileOps = () => ({
         isWidgetHidden: () => {
             return this.state.widgetHidden;
         },
@@ -411,7 +421,7 @@ export default class TextualBody extends React.Component {
         },
     });
 
-    onStarterLinkClick = (starterLink, ev) => {
+    private onStarterLinkClick = (starterLink: string, ev: SyntheticEvent): void => {
         ev.preventDefault();
         // We need to add on our scalar token to the starter link, but we may not have one!
         // In addition, we can't fetch one on click and then go to it immediately as that
@@ -431,7 +441,6 @@ export default class TextualBody extends React.Component {
         const scalarClient = integrationManager.getScalarClient();
         scalarClient.connect().then(() => {
             const completeUrl = scalarClient.getStarterLink(starterLink);
-            const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
             const integrationsUrl = integrationManager.uiUrl;
             Modal.createTrackedDialog('Add an integration', '', QuestionDialog, {
                 title: _t("Add an Integration"),
@@ -458,12 +467,11 @@ export default class TextualBody extends React.Component {
         });
     };
 
-    _openHistoryDialog = async () => {
-        const MessageEditHistoryDialog = sdk.getComponent("views.dialogs.MessageEditHistoryDialog");
+    private openHistoryDialog = async (): Promise => {
         Modal.createDialog(MessageEditHistoryDialog, { mxEvent: this.props.mxEvent });
     };
 
-    _renderEditedMarker() {
+    private renderEditedMarker() {
         const date = this.props.mxEvent.replacingEventDate();
         const dateString = date && formatDate(date);
 
@@ -479,7 +487,7 @@ export default class TextualBody extends React.Component {
         return (
             
@@ -490,24 +498,25 @@ export default class TextualBody extends React.Component {
 
     render() {
         if (this.props.editState) {
-            const EditMessageComposer = sdk.getComponent('rooms.EditMessageComposer');
             return ;
         }
         const mxEvent = this.props.mxEvent;
         const content = mxEvent.getContent();
 
         // only strip reply if this is the original replying event, edits thereafter do not have the fallback
-        const stripReply = !mxEvent.replacingEvent() && ReplyThread.getParentEventId(mxEvent);
+        const stripReply = !mxEvent.replacingEvent() && !!ReplyThread.getParentEventId(mxEvent);
         let body = HtmlUtils.bodyToHtml(content, this.props.highlights, {
-            disableBigEmoji: content.msgtype === "m.emote" || !SettingsStore.getValue('TextualBody.enableBigEmoji'),
+            disableBigEmoji: content.msgtype === MsgType.Emote
+                || !SettingsStore.getValue('TextualBody.enableBigEmoji'),
             // Part of Replies fallback support
             stripReplyFallback: stripReply,
-            ref: this._content,
+            ref: this.contentRef,
+            returnString: false,
         });
         if (this.props.replacingEventId) {
             body = <>
                 {body}
-                {this._renderEditedMarker()}
+                {this.renderEditedMarker()}
             ;
         }
 
@@ -521,7 +530,6 @@ export default class TextualBody extends React.Component {
 
         let widgets;
         if (this.state.links.length && !this.state.widgetHidden && this.props.showUrlPreview) {
-            const LinkPreviewWidget = sdk.getComponent('rooms.LinkPreviewWidget');
             widgets = this.state.links.map((link)=>{
                 return 
                         * 
@@ -549,7 +557,7 @@ export default class TextualBody extends React.Component {
                         { widgets }
                     
                 );
-            case "m.notice":
+            case MsgType.Notice:
                 return (
                     
                         { body }
diff --git a/src/components/views/messages/TextualEvent.js b/src/components/views/messages/TextualEvent.tsx
similarity index 72%
rename from src/components/views/messages/TextualEvent.js
rename to src/components/views/messages/TextualEvent.tsx
index 663f47dd2a..70f90a33e4 100644
--- a/src/components/views/messages/TextualEvent.js
+++ b/src/components/views/messages/TextualEvent.tsx
@@ -1,6 +1,5 @@
 /*
-Copyright 2015, 2016 OpenMarket Ltd
-Copyright 2019 The Matrix.org Foundation C.I.C.
+Copyright 2015 - 2021 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.
@@ -16,20 +15,20 @@ limitations under the License.
 */
 
 import React from 'react';
-import PropTypes from 'prop-types';
+import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
+
 import * as TextForEvent from "../../../TextForEvent";
 import { replaceableComponent } from "../../../utils/replaceableComponent";
 
-@replaceableComponent("views.messages.TextualEvent")
-export default class TextualEvent extends React.Component {
-    static propTypes = {
-        /* the MatrixEvent to show */
-        mxEvent: PropTypes.object.isRequired,
-    };
+interface IProps {
+    mxEvent: MatrixEvent;
+}
 
+@replaceableComponent("views.messages.TextualEvent")
+export default class TextualEvent extends React.Component {
     render() {
         const text = TextForEvent.textForEvent(this.props.mxEvent, true);
-        if (text == null || text.length === 0) return null;
+        if (!text || (text as string).length === 0) return null;
         return (
             
{ text }
); diff --git a/src/components/views/rooms/BasicMessageComposer.tsx b/src/components/views/rooms/BasicMessageComposer.tsx index 94a292afe7..d317aa409b 100644 --- a/src/components/views/rooms/BasicMessageComposer.tsx +++ b/src/components/views/rooms/BasicMessageComposer.tsx @@ -41,7 +41,7 @@ import { Key } from "../../../Keyboard"; import { EMOTICON_TO_EMOJI } from "../../../emoji"; import { CommandCategories, CommandMap, parseCommandString } from "../../../SlashCommands"; import Range from "../../../editor/range"; -import MessageComposerFormatBar from "./MessageComposerFormatBar"; +import MessageComposerFormatBar, { Formatting } from "./MessageComposerFormatBar"; import DocumentOffset from "../../../editor/offset"; import { IDiff } from "../../../editor/diff"; import AutocompleteWrapperModel from "../../../editor/autocomplete"; @@ -55,7 +55,7 @@ const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.sourc const IS_MAC = navigator.platform.indexOf("Mac") !== -1; -function ctrlShortcutLabel(key) { +function ctrlShortcutLabel(key: string): string { return (IS_MAC ? "⌘" : "Ctrl") + "+" + key; } @@ -81,14 +81,6 @@ function selectionEquals(a: Partial, b: Selection): boolean { a.type === b.type; } -enum Formatting { - Bold = "bold", - Italics = "italics", - Strikethrough = "strikethrough", - Code = "code", - Quote = "quote", -} - interface IProps { model: EditorModel; room: Room; @@ -111,9 +103,9 @@ interface IState { @replaceableComponent("views.rooms.BasicMessageEditor") export default class BasicMessageEditor extends React.Component { - private editorRef = createRef(); + public readonly editorRef = createRef(); private autocompleteRef = createRef(); - private formatBarRef = createRef(); + private formatBarRef = createRef(); private modifiedFlag = false; private isIMEComposing = false; @@ -156,7 +148,7 @@ export default class BasicMessageEditor extends React.Component } } - private replaceEmoticon = (caretPosition: DocumentPosition) => { + private replaceEmoticon = (caretPosition: DocumentPosition): number => { const { model } = this.props; const range = model.startRange(caretPosition); // expand range max 8 characters backwards from caretPosition, @@ -188,7 +180,7 @@ export default class BasicMessageEditor extends React.Component } }; - private updateEditorState = (selection: Caret, inputType?: string, diff?: IDiff) => { + private updateEditorState = (selection: Caret, inputType?: string, diff?: IDiff): void => { renderModel(this.editorRef.current, this.props.model); if (selection) { // set the caret/selection try { @@ -230,25 +222,25 @@ export default class BasicMessageEditor extends React.Component } }; - private showPlaceholder() { + private showPlaceholder(): void { // escape single quotes const placeholder = this.props.placeholder.replace(/'/g, '\\\''); this.editorRef.current.style.setProperty("--placeholder", `'${placeholder}'`); this.editorRef.current.classList.add("mx_BasicMessageComposer_inputEmpty"); } - private hidePlaceholder() { + private hidePlaceholder(): void { this.editorRef.current.classList.remove("mx_BasicMessageComposer_inputEmpty"); this.editorRef.current.style.removeProperty("--placeholder"); } - private onCompositionStart = () => { + private onCompositionStart = (): void => { this.isIMEComposing = true; // even if the model is empty, the composition text shouldn't be mixed with the placeholder this.hidePlaceholder(); }; - private onCompositionEnd = () => { + private onCompositionEnd = (): void => { this.isIMEComposing = false; // some browsers (Chrome) don't fire an input event after ending a composition, // so trigger a model update after the composition is done by calling the input handler. @@ -271,14 +263,14 @@ export default class BasicMessageEditor extends React.Component } }; - isComposing(event: React.KeyboardEvent) { + public isComposing(event: React.KeyboardEvent): boolean { // checking the event.isComposing flag just in case any browser out there // emits events related to the composition after compositionend // has been fired return !!(this.isIMEComposing || (event.nativeEvent && event.nativeEvent.isComposing)); } - private onCutCopy = (event: ClipboardEvent, type: string) => { + private onCutCopy = (event: ClipboardEvent, type: string): void => { const selection = document.getSelection(); const text = selection.toString(); if (text) { @@ -296,15 +288,15 @@ export default class BasicMessageEditor extends React.Component } }; - private onCopy = (event: ClipboardEvent) => { + private onCopy = (event: ClipboardEvent): void => { this.onCutCopy(event, "copy"); }; - private onCut = (event: ClipboardEvent) => { + private onCut = (event: ClipboardEvent): void => { this.onCutCopy(event, "cut"); }; - private onPaste = (event: ClipboardEvent) => { + private onPaste = (event: ClipboardEvent): boolean => { event.preventDefault(); // we always handle the paste ourselves if (this.props.onPaste && this.props.onPaste(event, this.props.model)) { // to prevent double handling, allow props.onPaste to skip internal onPaste @@ -328,7 +320,7 @@ export default class BasicMessageEditor extends React.Component replaceRangeAndMoveCaret(range, parts); }; - private onInput = (event: Partial) => { + private onInput = (event: Partial): void => { // ignore any input while doing IME compositions if (this.isIMEComposing) { return; @@ -339,7 +331,7 @@ export default class BasicMessageEditor extends React.Component this.props.model.update(text, event.inputType, caret); }; - private insertText(textToInsert: string, inputType = "insertText") { + private insertText(textToInsert: string, inputType = "insertText"): void { const sel = document.getSelection(); const { caret, text } = getCaretOffsetAndText(this.editorRef.current, sel); const newText = text.substr(0, caret.offset) + textToInsert + text.substr(caret.offset); @@ -353,14 +345,14 @@ export default class BasicMessageEditor extends React.Component // we don't need to. But if the user is navigating the caret without input // we need to recalculate it, to be able to know where to insert content after // losing focus - private setLastCaretFromPosition(position: DocumentPosition) { + private setLastCaretFromPosition(position: DocumentPosition): void { const { model } = this.props; this._isCaretAtEnd = position.isAtEnd(model); this.lastCaret = position.asOffset(model); this.lastSelection = cloneSelection(document.getSelection()); } - private refreshLastCaretIfNeeded() { + private refreshLastCaretIfNeeded(): DocumentOffset { // XXX: needed when going up and down in editing messages ... not sure why yet // because the editors should stop doing this when when blurred ... // maybe it's on focus and the _editorRef isn't available yet or something. @@ -377,38 +369,38 @@ export default class BasicMessageEditor extends React.Component return this.lastCaret; } - clearUndoHistory() { + public clearUndoHistory(): void { this.historyManager.clear(); } - getCaret() { + public getCaret(): DocumentOffset { return this.lastCaret; } - isSelectionCollapsed() { + public isSelectionCollapsed(): boolean { return !this.lastSelection || this.lastSelection.isCollapsed; } - isCaretAtStart() { + public isCaretAtStart(): boolean { return this.getCaret().offset === 0; } - isCaretAtEnd() { + public isCaretAtEnd(): boolean { return this._isCaretAtEnd; } - private onBlur = () => { + private onBlur = (): void => { document.removeEventListener("selectionchange", this.onSelectionChange); }; - private onFocus = () => { + private onFocus = (): void => { document.addEventListener("selectionchange", this.onSelectionChange); // force to recalculate this.lastSelection = null; this.refreshLastCaretIfNeeded(); }; - private onSelectionChange = () => { + private onSelectionChange = (): void => { const { isEmpty } = this.props.model; this.refreshLastCaretIfNeeded(); @@ -427,7 +419,7 @@ export default class BasicMessageEditor extends React.Component } }; - private onKeyDown = (event: React.KeyboardEvent) => { + private onKeyDown = (event: React.KeyboardEvent): void => { const model = this.props.model; let handled = false; const action = getKeyBindingsManager().getMessageComposerAction(event); @@ -523,7 +515,7 @@ export default class BasicMessageEditor extends React.Component } }; - private async tabCompleteName() { + private async tabCompleteName(): Promise { try { await new Promise(resolve => this.setState({ showVisualBell: false }, resolve)); const { model } = this.props; @@ -557,27 +549,27 @@ export default class BasicMessageEditor extends React.Component } } - isModified() { + public isModified(): boolean { return this.modifiedFlag; } - private onAutoCompleteConfirm = (completion: ICompletion) => { + private onAutoCompleteConfirm = (completion: ICompletion): void => { this.modifiedFlag = true; this.props.model.autoComplete.onComponentConfirm(completion); }; - private onAutoCompleteSelectionChange = (completion: ICompletion, completionIndex: number) => { + private onAutoCompleteSelectionChange = (completion: ICompletion, completionIndex: number): void => { this.modifiedFlag = true; this.props.model.autoComplete.onComponentSelectionChange(completion); this.setState({ completionIndex }); }; - private configureEmoticonAutoReplace = () => { + private configureEmoticonAutoReplace = (): void => { const shouldReplace = SettingsStore.getValue('MessageComposerInput.autoReplaceEmoji'); this.props.model.setTransformCallback(shouldReplace ? this.replaceEmoticon : null); }; - private configureShouldShowPillAvatar = () => { + private configureShouldShowPillAvatar = (): void => { const showPillAvatar = SettingsStore.getValue("Pill.shouldShowPillAvatar"); this.setState({ showPillAvatar }); }; @@ -611,8 +603,8 @@ export default class BasicMessageEditor extends React.Component this.editorRef.current.focus(); } - private getInitialCaretPosition() { - let caretPosition; + private getInitialCaretPosition(): DocumentPosition { + let caretPosition: DocumentPosition; if (this.props.initialCaret) { // if restoring state from a previous editor, // restore caret position from the state @@ -625,7 +617,7 @@ export default class BasicMessageEditor extends React.Component return caretPosition; } - private onFormatAction = (action: Formatting) => { + private onFormatAction = (action: Formatting): void => { const range = getRangeForSelection(this.editorRef.current, this.props.model, document.getSelection()); // trim the range as we want it to exclude leading/trailing spaces range.trim(); @@ -680,9 +672,9 @@ export default class BasicMessageEditor extends React.Component }); const shortcuts = { - bold: ctrlShortcutLabel("B"), - italics: ctrlShortcutLabel("I"), - quote: ctrlShortcutLabel(">"), + [Formatting.Bold]: ctrlShortcutLabel("B"), + [Formatting.Italics]: ctrlShortcutLabel("I"), + [Formatting.Quote]: ctrlShortcutLabel(">"), }; const { completionIndex } = this.state; @@ -714,11 +706,11 @@ export default class BasicMessageEditor extends React.Component
); } - focus() { + public focus(): void { this.editorRef.current.focus(); } - public insertMention(userId: string) { + public insertMention(userId: string): void { const { model } = this.props; const { partCreator } = model; const member = this.props.room.getMember(userId); @@ -736,7 +728,7 @@ export default class BasicMessageEditor extends React.Component this.focus(); } - public insertQuotedMessage(event: MatrixEvent) { + public insertQuotedMessage(event: MatrixEvent): void { const { model } = this.props; const { partCreator } = model; const quoteParts = parseEvent(event, partCreator, { isQuotedMessage: true }); @@ -751,7 +743,7 @@ export default class BasicMessageEditor extends React.Component this.focus(); } - public insertPlaintext(text: string) { + public insertPlaintext(text: string): void { const { model } = this.props; const { partCreator } = model; const caret = this.getCaret(); diff --git a/src/components/views/rooms/EditMessageComposer.js b/src/components/views/rooms/EditMessageComposer.tsx similarity index 70% rename from src/components/views/rooms/EditMessageComposer.js rename to src/components/views/rooms/EditMessageComposer.tsx index 0ab972b5f1..b06b57dced 100644 --- a/src/components/views/rooms/EditMessageComposer.js +++ b/src/components/views/rooms/EditMessageComposer.tsx @@ -1,6 +1,5 @@ /* -Copyright 2019 New Vector Ltd -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2019 - 2021 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,37 +13,42 @@ 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. */ -import React from 'react'; -import * as sdk from '../../../index'; + +import React, { createRef, KeyboardEvent } from 'react'; +import classNames from 'classnames'; +import { EventStatus, IContent, MatrixEvent } from 'matrix-js-sdk/src/models/event'; + import { _t, _td } from '../../../languageHandler'; -import PropTypes from 'prop-types'; import dis from '../../../dispatcher/dispatcher'; import EditorModel from '../../../editor/model'; import { getCaretOffsetAndText } from '../../../editor/dom'; import { htmlSerializeIfNeeded, textSerialize, containsEmote, stripEmoteCommand } from '../../../editor/serialize'; import { findEditableEvent } from '../../../utils/EventUtils'; import { parseEvent } from '../../../editor/deserialize'; -import { CommandPartCreator } from '../../../editor/parts'; +import { CommandPartCreator, Part, PartCreator } from '../../../editor/parts'; import EditorStateTransfer from '../../../utils/EditorStateTransfer'; -import classNames from 'classnames'; -import { EventStatus } from 'matrix-js-sdk/src/models/event'; import BasicMessageComposer from "./BasicMessageComposer"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; -import { CommandCategories, getCommand } from '../../../SlashCommands'; +import { Command, CommandCategories, getCommand } from '../../../SlashCommands'; import { Action } from "../../../dispatcher/actions"; import CountlyAnalytics from "../../../CountlyAnalytics"; import { getKeyBindingsManager, MessageComposerAction } from '../../../KeyBindingsManager'; import { replaceableComponent } from "../../../utils/replaceableComponent"; import SendHistoryManager from '../../../SendHistoryManager'; import Modal from '../../../Modal'; +import { MsgType } from 'matrix-js-sdk/src/@types/event'; +import { Room } from 'matrix-js-sdk/src/models/room'; +import ErrorDialog from "../dialogs/ErrorDialog"; +import QuestionDialog from "../dialogs/QuestionDialog"; +import { ActionPayload } from "../../../dispatcher/payloads"; +import AccessibleButton from '../elements/AccessibleButton'; -function _isReply(mxEvent) { +function eventIsReply(mxEvent: MatrixEvent): boolean { const relatesTo = mxEvent.getContent()["m.relates_to"]; - const isReply = !!(relatesTo && relatesTo["m.in_reply_to"]); - return isReply; + return !!(relatesTo && relatesTo["m.in_reply_to"]); } -function getHtmlReplyFallback(mxEvent) { +function getHtmlReplyFallback(mxEvent: MatrixEvent): string { const html = mxEvent.getContent().formatted_body; if (!html) { return ""; @@ -54,7 +58,7 @@ function getHtmlReplyFallback(mxEvent) { return (mxReply && mxReply.outerHTML) || ""; } -function getTextReplyFallback(mxEvent) { +function getTextReplyFallback(mxEvent: MatrixEvent): string { const body = mxEvent.getContent().body; const lines = body.split("\n").map(l => l.trim()); if (lines.length > 2 && lines[0].startsWith("> ") && lines[1].length === 0) { @@ -63,12 +67,12 @@ function getTextReplyFallback(mxEvent) { return ""; } -function createEditContent(model, editedEvent) { +function createEditContent(model: EditorModel, editedEvent: MatrixEvent): IContent { const isEmote = containsEmote(model); if (isEmote) { model = stripEmoteCommand(model); } - const isReply = _isReply(editedEvent); + const isReply = eventIsReply(editedEvent); let plainPrefix = ""; let htmlPrefix = ""; @@ -79,11 +83,11 @@ function createEditContent(model, editedEvent) { const body = textSerialize(model); - const newContent = { - "msgtype": isEmote ? "m.emote" : "m.text", + const newContent: IContent = { + "msgtype": isEmote ? MsgType.Emote : MsgType.Text, "body": body, }; - const contentBody = { + const contentBody: IContent = { msgtype: newContent.msgtype, body: `${plainPrefix} * ${body}`, }; @@ -105,55 +109,59 @@ function createEditContent(model, editedEvent) { }, contentBody); } +interface IProps { + editState: EditorStateTransfer; + className?: string; +} + +interface IState { + saveDisabled: boolean; +} + @replaceableComponent("views.rooms.EditMessageComposer") -export default class EditMessageComposer extends React.Component { - static propTypes = { - // the message event being edited - editState: PropTypes.instanceOf(EditorStateTransfer).isRequired, - }; - +export default class EditMessageComposer extends React.Component { static contextType = MatrixClientContext; + context: React.ContextType; - constructor(props, context) { + private readonly editorRef = createRef(); + private readonly dispatcherRef: string; + private model: EditorModel = null; + + constructor(props: IProps, context: React.ContextType) { super(props, context); - this.model = null; - this._editorRef = null; this.state = { saveDisabled: true, }; - this._createEditorModel(); - window.addEventListener("beforeunload", this._saveStoredEditorState); + + this.createEditorModel(); + window.addEventListener("beforeunload", this.saveStoredEditorState); this.dispatcherRef = dis.register(this.onAction); } - _setEditorRef = ref => { - this._editorRef = ref; - }; - - _getRoom() { + private getRoom(): Room { return this.context.getRoom(this.props.editState.getEvent().getRoomId()); } - _onKeyDown = (event) => { + private onKeyDown = (event: KeyboardEvent): void => { // ignore any keypress while doing IME compositions - if (this._editorRef.isComposing(event)) { + if (this.editorRef.current?.isComposing(event)) { return; } const action = getKeyBindingsManager().getMessageComposerAction(event); switch (action) { case MessageComposerAction.Send: - this._sendEdit(); + this.sendEdit(); event.preventDefault(); break; case MessageComposerAction.CancelEditing: - this._cancelEdit(); + this.cancelEdit(); break; case MessageComposerAction.EditPrevMessage: { - if (this._editorRef.isModified() || !this._editorRef.isCaretAtStart()) { + if (this.editorRef.current?.isModified() || !this.editorRef.current?.isCaretAtStart()) { return; } - const previousEvent = findEditableEvent(this._getRoom(), false, + const previousEvent = findEditableEvent(this.getRoom(), false, this.props.editState.getEvent().getId()); if (previousEvent) { dis.dispatch({ action: 'edit_event', event: previousEvent }); @@ -162,14 +170,14 @@ export default class EditMessageComposer extends React.Component { break; } case MessageComposerAction.EditNextMessage: { - if (this._editorRef.isModified() || !this._editorRef.isCaretAtEnd()) { + if (this.editorRef.current?.isModified() || !this.editorRef.current?.isCaretAtEnd()) { return; } - const nextEvent = findEditableEvent(this._getRoom(), true, this.props.editState.getEvent().getId()); + const nextEvent = findEditableEvent(this.getRoom(), true, this.props.editState.getEvent().getId()); if (nextEvent) { dis.dispatch({ action: 'edit_event', event: nextEvent }); } else { - this._clearStoredEditorState(); + this.clearStoredEditorState(); dis.dispatch({ action: 'edit_event', event: null }); dis.fire(Action.FocusComposer); } @@ -177,32 +185,32 @@ export default class EditMessageComposer extends React.Component { break; } } + }; + + private get editorRoomKey(): string { + return `mx_edit_room_${this.getRoom().roomId}`; } - get _editorRoomKey() { - return `mx_edit_room_${this._getRoom().roomId}`; - } - - get _editorStateKey() { + private get editorStateKey(): string { return `mx_edit_state_${this.props.editState.getEvent().getId()}`; } - _cancelEdit = () => { - this._clearStoredEditorState(); + private cancelEdit = (): void => { + this.clearStoredEditorState(); dis.dispatch({ action: "edit_event", event: null }); dis.fire(Action.FocusComposer); + }; + + private get shouldSaveStoredEditorState(): boolean { + return localStorage.getItem(this.editorRoomKey) !== null; } - get _shouldSaveStoredEditorState() { - return localStorage.getItem(this._editorRoomKey) !== null; - } - - _restoreStoredEditorState(partCreator) { - const json = localStorage.getItem(this._editorStateKey); + private restoreStoredEditorState(partCreator: PartCreator): Part[] { + const json = localStorage.getItem(this.editorStateKey); if (json) { try { const { parts: serializedParts } = JSON.parse(json); - const parts = serializedParts.map(p => partCreator.deserializePart(p)); + const parts: Part[] = serializedParts.map(p => partCreator.deserializePart(p)); return parts; } catch (e) { console.error("Error parsing editing state: ", e); @@ -210,25 +218,25 @@ export default class EditMessageComposer extends React.Component { } } - _clearStoredEditorState() { - localStorage.removeItem(this._editorRoomKey); - localStorage.removeItem(this._editorStateKey); + private clearStoredEditorState(): void { + localStorage.removeItem(this.editorRoomKey); + localStorage.removeItem(this.editorStateKey); } - _clearPreviousEdit() { - if (localStorage.getItem(this._editorRoomKey)) { - localStorage.removeItem(`mx_edit_state_${localStorage.getItem(this._editorRoomKey)}`); + private clearPreviousEdit(): void { + if (localStorage.getItem(this.editorRoomKey)) { + localStorage.removeItem(`mx_edit_state_${localStorage.getItem(this.editorRoomKey)}`); } } - _saveStoredEditorState() { + private saveStoredEditorState(): void { const item = SendHistoryManager.createItem(this.model); - this._clearPreviousEdit(); - localStorage.setItem(this._editorRoomKey, this.props.editState.getEvent().getId()); - localStorage.setItem(this._editorStateKey, JSON.stringify(item)); + this.clearPreviousEdit(); + localStorage.setItem(this.editorRoomKey, this.props.editState.getEvent().getId()); + localStorage.setItem(this.editorStateKey, JSON.stringify(item)); } - _isSlashCommand() { + private isSlashCommand(): boolean { const parts = this.model.parts; const firstPart = parts[0]; if (firstPart) { @@ -244,10 +252,10 @@ export default class EditMessageComposer extends React.Component { return false; } - _isContentModified(newContent) { + private isContentModified(newContent: IContent): boolean { // if nothing has changed then bail const oldContent = this.props.editState.getEvent().getContent(); - if (!this._editorRef.isModified() || + if (!this.editorRef.current?.isModified() || (oldContent["msgtype"] === newContent["msgtype"] && oldContent["body"] === newContent["body"] && oldContent["format"] === newContent["format"] && oldContent["formatted_body"] === newContent["formatted_body"])) { @@ -256,7 +264,7 @@ export default class EditMessageComposer extends React.Component { return true; } - _getSlashCommand() { + private getSlashCommand(): [Command, string, string] { const commandText = this.model.parts.reduce((text, part) => { // use mxid to textify user pills in a command if (part.type === "user-pill") { @@ -268,7 +276,7 @@ export default class EditMessageComposer extends React.Component { return [cmd, args, commandText]; } - async _runSlashCommand(cmd, args, roomId) { + private async runSlashCommand(cmd: Command, args: string, roomId: string): Promise { const result = cmd.run(roomId, args); let messageContent; let error = result.error; @@ -285,7 +293,6 @@ export default class EditMessageComposer extends React.Component { } if (error) { console.error("Command failure: %s", error); - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); // assume the error is a server error when the command is async const isServerError = !!result.promise; const title = isServerError ? _td("Server error") : _td("Command error"); @@ -309,7 +316,7 @@ export default class EditMessageComposer extends React.Component { } } - _sendEdit = async () => { + private sendEdit = async (): Promise => { const startTime = CountlyAnalytics.getTimestamp(); const editedEvent = this.props.editState.getEvent(); const editContent = createEditContent(this.model, editedEvent); @@ -318,20 +325,19 @@ export default class EditMessageComposer extends React.Component { let shouldSend = true; // If content is modified then send an updated event into the room - if (this._isContentModified(newContent)) { + if (this.isContentModified(newContent)) { const roomId = editedEvent.getRoomId(); - if (!containsEmote(this.model) && this._isSlashCommand()) { - const [cmd, args, commandText] = this._getSlashCommand(); + if (!containsEmote(this.model) && this.isSlashCommand()) { + const [cmd, args, commandText] = this.getSlashCommand(); if (cmd) { if (cmd.category === CommandCategories.messages) { - editContent["m.new_content"] = await this._runSlashCommand(cmd, args, roomId); + editContent["m.new_content"] = await this.runSlashCommand(cmd, args, roomId); } else { - this._runSlashCommand(cmd, args, roomId); + this.runSlashCommand(cmd, args, roomId); shouldSend = false; } } else { // ask the user if their unknown command should be sent as a message - const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); const { finished } = Modal.createTrackedDialog("Unknown command", "", QuestionDialog, { title: _t("Unknown Command"), description:
@@ -358,9 +364,9 @@ export default class EditMessageComposer extends React.Component { } } if (shouldSend) { - this._cancelPreviousPendingEdit(); + this.cancelPreviousPendingEdit(); const prom = this.context.sendMessage(roomId, editContent); - this._clearStoredEditorState(); + this.clearStoredEditorState(); dis.dispatch({ action: "message_sent" }); CountlyAnalytics.instance.trackSendMessage(startTime, prom, roomId, true, false, editContent); } @@ -371,7 +377,7 @@ export default class EditMessageComposer extends React.Component { dis.fire(Action.FocusComposer); }; - _cancelPreviousPendingEdit() { + private cancelPreviousPendingEdit(): void { const originalEvent = this.props.editState.getEvent(); const previousEdit = originalEvent.replacingEvent(); if (previousEdit && ( @@ -389,23 +395,23 @@ export default class EditMessageComposer extends React.Component { const sel = document.getSelection(); let caret; if (sel.focusNode) { - caret = getCaretOffsetAndText(this._editorRef, sel).caret; + caret = getCaretOffsetAndText(this.editorRef.current, sel).caret; } const parts = this.model.serializeParts(); // if caret is undefined because for some reason there isn't a valid selection, // then when mounting the editor again with the same editor state, // it will set the cursor at the end. this.props.editState.setEditorState(caret, parts); - window.removeEventListener("beforeunload", this._saveStoredEditorState); - if (this._shouldSaveStoredEditorState) { - this._saveStoredEditorState(); + window.removeEventListener("beforeunload", this.saveStoredEditorState); + if (this.shouldSaveStoredEditorState) { + this.saveStoredEditorState(); } dis.unregister(this.dispatcherRef); } - _createEditorModel() { + private createEditorModel(): void { const { editState } = this.props; - const room = this._getRoom(); + const room = this.getRoom(); const partCreator = new CommandPartCreator(room, this.context); let parts; if (editState.hasEditorState()) { @@ -414,13 +420,13 @@ export default class EditMessageComposer extends React.Component { parts = editState.getSerializedParts().map(p => partCreator.deserializePart(p)); } else { //otherwise, either restore serialized parts from localStorage or parse the body of the event - parts = this._restoreStoredEditorState(partCreator) || parseEvent(editState.getEvent(), partCreator); + parts = this.restoreStoredEditorState(partCreator) || parseEvent(editState.getEvent(), partCreator); } this.model = new EditorModel(parts, partCreator); - this._saveStoredEditorState(); + this.saveStoredEditorState(); } - _getInitialCaretPosition() { + private getInitialCaretPosition(): CaretPosition { const { editState } = this.props; let caretPosition; if (editState.hasEditorState() && editState.getCaret()) { @@ -435,8 +441,8 @@ export default class EditMessageComposer extends React.Component { return caretPosition; } - _onChange = () => { - if (!this.state.saveDisabled || !this._editorRef || !this._editorRef.isModified()) { + private onChange = (): void => { + if (!this.state.saveDisabled || !this.editorRef.current?.isModified()) { return; } @@ -445,33 +451,34 @@ export default class EditMessageComposer extends React.Component { }); }; - onAction = payload => { - if (payload.action === "edit_composer_insert" && this._editorRef) { + private onAction = (payload: ActionPayload) => { + if (payload.action === "edit_composer_insert" && this.editorRef.current) { if (payload.userId) { - this._editorRef.insertMention(payload.userId); + this.editorRef.current?.insertMention(payload.userId); } else if (payload.event) { - this._editorRef.insertQuotedMessage(payload.event); + this.editorRef.current?.insertQuotedMessage(payload.event); } else if (payload.text) { - this._editorRef.insertPlaintext(payload.text); + this.editorRef.current?.insertPlaintext(payload.text); } } }; render() { - const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); - return (
+ return (
- {_t("Cancel")} - - {_t("Save")} + + { _t("Cancel") } + + + { _t("Save") }
); diff --git a/src/components/views/rooms/MessageComposer.tsx b/src/components/views/rooms/MessageComposer.tsx index db57f98025..7d61ba5ec6 100644 --- a/src/components/views/rooms/MessageComposer.tsx +++ b/src/components/views/rooms/MessageComposer.tsx @@ -43,6 +43,7 @@ import { E2EStatus } from '../../../utils/ShieldUtils'; import SendMessageComposer from "./SendMessageComposer"; import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload"; import { Action } from "../../../dispatcher/actions"; +import EditorModel from "../../../editor/model"; interface IComposerAvatarProps { me: object; @@ -318,14 +319,14 @@ export default class MessageComposer extends React.Component { } }; - addEmoji(emoji: string) { + private addEmoji(emoji: string) { dis.dispatch({ action: Action.ComposerInsert, text: emoji, }); } - sendMessage = async () => { + private sendMessage = async () => { if (this.state.haveRecording && this.voiceRecordingButton) { // There shouldn't be any text message to send when a voice recording is active, so // just send out the voice recording. @@ -333,11 +334,10 @@ export default class MessageComposer extends React.Component { return; } - // XXX: Private function access - this.messageComposerInput._sendMessage(); + this.messageComposerInput.sendMessage(); }; - onChange = (model) => { + private onChange = (model: EditorModel) => { this.setState({ isComposerEmpty: model.isEmpty, }); diff --git a/src/components/views/rooms/MessageComposerFormatBar.js b/src/components/views/rooms/MessageComposerFormatBar.tsx similarity index 55% rename from src/components/views/rooms/MessageComposerFormatBar.js rename to src/components/views/rooms/MessageComposerFormatBar.tsx index c31538c6cd..75bca8aac7 100644 --- a/src/components/views/rooms/MessageComposerFormatBar.js +++ b/src/components/views/rooms/MessageComposerFormatBar.tsx @@ -1,5 +1,5 @@ /* -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2019 - 2021 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,21 +14,35 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; -import PropTypes from 'prop-types'; -import { _t } from '../../../languageHandler'; +import React, { createRef } from 'react'; import classNames from 'classnames'; + +import { _t } from '../../../languageHandler'; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import { replaceableComponent } from "../../../utils/replaceableComponent"; -@replaceableComponent("views.rooms.MessageComposerFormatBar") -export default class MessageComposerFormatBar extends React.PureComponent { - static propTypes = { - onAction: PropTypes.func.isRequired, - shortcuts: PropTypes.object.isRequired, - }; +export enum Formatting { + Bold = "bold", + Italics = "italics", + Strikethrough = "strikethrough", + Code = "code", + Quote = "quote", +} - constructor(props) { +interface IProps { + shortcuts: Partial>; + onAction(action: Formatting): void; +} + +interface IState { + visible: boolean; +} + +@replaceableComponent("views.rooms.MessageComposerFormatBar") +export default class MessageComposerFormatBar extends React.PureComponent { + private readonly formatBarRef = createRef(); + + constructor(props: IProps) { super(props); this.state = { visible: false }; } @@ -37,49 +51,53 @@ export default class MessageComposerFormatBar extends React.PureComponent { const classes = classNames("mx_MessageComposerFormatBar", { "mx_MessageComposerFormatBar_shown": this.state.visible, }); - return (
this._formatBarRef = ref}> - this.props.onAction("bold")} icon="Bold" shortcut={this.props.shortcuts.bold} visible={this.state.visible} /> - this.props.onAction("italics")} icon="Italic" shortcut={this.props.shortcuts.italics} visible={this.state.visible} /> - this.props.onAction("strikethrough")} icon="Strikethrough" visible={this.state.visible} /> - this.props.onAction("code")} icon="Code" visible={this.state.visible} /> - this.props.onAction("quote")} icon="Quote" shortcut={this.props.shortcuts.quote} visible={this.state.visible} /> + return (
+ this.props.onAction(Formatting.Bold)} icon="Bold" shortcut={this.props.shortcuts.bold} visible={this.state.visible} /> + this.props.onAction(Formatting.Italics)} icon="Italic" shortcut={this.props.shortcuts.italics} visible={this.state.visible} /> + this.props.onAction(Formatting.Strikethrough)} icon="Strikethrough" visible={this.state.visible} /> + this.props.onAction(Formatting.Code)} icon="Code" visible={this.state.visible} /> + this.props.onAction(Formatting.Quote)} icon="Quote" shortcut={this.props.shortcuts.quote} visible={this.state.visible} />
); } - showAt(selectionRect) { + public showAt(selectionRect: DOMRect): void { + if (!this.formatBarRef.current) return; + this.setState({ visible: true }); - const parentRect = this._formatBarRef.parentElement.getBoundingClientRect(); - this._formatBarRef.style.left = `${selectionRect.left - parentRect.left}px`; + const parentRect = this.formatBarRef.current.parentElement.getBoundingClientRect(); + this.formatBarRef.current.style.left = `${selectionRect.left - parentRect.left}px`; // 12 is half the height of the bar (e.g. to center it) and 16 is an offset that felt ok. - this._formatBarRef.style.top = `${selectionRect.top - parentRect.top - 16 - 12}px`; + this.formatBarRef.current.style.top = `${selectionRect.top - parentRect.top - 16 - 12}px`; } - hide() { + public hide(): void { this.setState({ visible: false }); } } -class FormatButton extends React.PureComponent { - static propTypes = { - label: PropTypes.string.isRequired, - onClick: PropTypes.func.isRequired, - icon: PropTypes.string.isRequired, - shortcut: PropTypes.string, - visible: PropTypes.bool, - }; +interface IFormatButtonProps { + label: string; + icon: string; + shortcut?: string; + visible?: boolean; + onClick(): void; +} +class FormatButton extends React.PureComponent { render() { const className = `mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIcon${this.props.icon}`; let shortcut; if (this.props.shortcut) { - shortcut =
{this.props.shortcut}
; + shortcut =
+ { this.props.shortcut } +
; } const tooltip =
- {this.props.label} + { this.props.label }
- {shortcut} + { shortcut }
; diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.tsx similarity index 78% rename from src/components/views/rooms/SendMessageComposer.js rename to src/components/views/rooms/SendMessageComposer.tsx index cc819e05e1..2c670f6f64 100644 --- a/src/components/views/rooms/SendMessageComposer.js +++ b/src/components/views/rooms/SendMessageComposer.tsx @@ -1,6 +1,5 @@ /* -Copyright 2019 New Vector Ltd -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2019 - 2021 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,8 +13,11 @@ 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. */ -import React from 'react'; -import PropTypes from 'prop-types'; + +import React, { ClipboardEvent, createRef, KeyboardEvent } from 'react'; +import EMOJI_REGEX from 'emojibase-regex'; +import { IContent, MatrixEvent } from 'matrix-js-sdk/src/models/event'; + import dis from '../../../dispatcher/dispatcher'; import EditorModel from '../../../editor/model'; import { @@ -27,13 +29,12 @@ import { startsWith, stripPrefix, } from '../../../editor/serialize'; -import { CommandPartCreator } from '../../../editor/parts'; +import { CommandPartCreator, Part, PartCreator, SerializedPart } from '../../../editor/parts'; import BasicMessageComposer from "./BasicMessageComposer"; import ReplyThread from "../elements/ReplyThread"; import { findEditableEvent } from '../../../utils/EventUtils'; import SendHistoryManager from "../../../SendHistoryManager"; -import { CommandCategories, getCommand } from '../../../SlashCommands'; -import * as sdk from '../../../index'; +import { Command, CommandCategories, getCommand } from '../../../SlashCommands'; import Modal from '../../../Modal'; import { _t, _td } from '../../../languageHandler'; import ContentMessages from '../../../ContentMessages'; @@ -44,12 +45,20 @@ import { containsEmoji } from "../../../effects/utils"; import { CHAT_EFFECTS } from '../../../effects'; import CountlyAnalytics from "../../../CountlyAnalytics"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; -import EMOJI_REGEX from 'emojibase-regex'; import { getKeyBindingsManager, MessageComposerAction } from '../../../KeyBindingsManager'; import { replaceableComponent } from "../../../utils/replaceableComponent"; import SettingsStore from '../../../settings/SettingsStore'; +import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks"; +import { Room } from 'matrix-js-sdk/src/models/room'; +import ErrorDialog from "../dialogs/ErrorDialog"; +import QuestionDialog from "../dialogs/QuestionDialog"; +import { ActionPayload } from "../../../dispatcher/payloads"; -function addReplyToMessageContent(content, repliedToEvent, permalinkCreator) { +function addReplyToMessageContent( + content: IContent, + repliedToEvent: MatrixEvent, + permalinkCreator: RoomPermalinkCreator, +): void { const replyContent = ReplyThread.makeReplyMixIn(repliedToEvent); Object.assign(content, replyContent); @@ -65,7 +74,11 @@ function addReplyToMessageContent(content, repliedToEvent, permalinkCreator) { } // exported for tests -export function createMessageContent(model, permalinkCreator, replyToEvent) { +export function createMessageContent( + model: EditorModel, + permalinkCreator: RoomPermalinkCreator, + replyToEvent: MatrixEvent, +): IContent { const isEmote = containsEmote(model); if (isEmote) { model = stripEmoteCommand(model); @@ -76,7 +89,7 @@ export function createMessageContent(model, permalinkCreator, replyToEvent) { model = unescapeMessage(model); const body = textSerialize(model); - const content = { + const content: IContent = { msgtype: isEmote ? "m.emote" : "m.text", body: body, }; @@ -94,7 +107,7 @@ export function createMessageContent(model, permalinkCreator, replyToEvent) { } // exported for tests -export function isQuickReaction(model) { +export function isQuickReaction(model: EditorModel): boolean { const parts = model.parts; if (parts.length == 0) return false; const text = textSerialize(model); @@ -111,46 +124,47 @@ export function isQuickReaction(model) { return false; } +interface IProps { + room: Room; + placeholder?: string; + permalinkCreator: RoomPermalinkCreator; + replyToEvent?: MatrixEvent; + disabled?: boolean; + onChange?(model: EditorModel): void; +} + @replaceableComponent("views.rooms.SendMessageComposer") -export default class SendMessageComposer extends React.Component { - static propTypes = { - room: PropTypes.object.isRequired, - placeholder: PropTypes.string, - permalinkCreator: PropTypes.object.isRequired, - replyToEvent: PropTypes.object, - onChange: PropTypes.func, - disabled: PropTypes.bool, - }; - +export default class SendMessageComposer extends React.Component { static contextType = MatrixClientContext; + context: React.ContextType; - constructor(props, context) { + private readonly prepareToEncrypt?: RateLimitedFunc; + private readonly editorRef = createRef(); + private model: EditorModel = null; + private currentlyComposedEditorState: SerializedPart[] = null; + private dispatcherRef: string; + private sendHistoryManager: SendHistoryManager; + + constructor(props: IProps, context: React.ContextType) { super(props, context); - this.model = null; - this._editorRef = null; - this.currentlyComposedEditorState = null; - if (this.context.isCryptoEnabled() && this.context.isRoomEncrypted(this.props.room.roomId)) { - this._prepareToEncrypt = new RateLimitedFunc(() => { + if (context.isCryptoEnabled() && context.isRoomEncrypted(this.props.room.roomId)) { + this.prepareToEncrypt = new RateLimitedFunc(() => { this.context.prepareToEncrypt(this.props.room); }, 60000); } - window.addEventListener("beforeunload", this._saveStoredEditorState); + window.addEventListener("beforeunload", this.saveStoredEditorState); } - _setEditorRef = ref => { - this._editorRef = ref; - }; - - _onKeyDown = (event) => { + private onKeyDown = (event: KeyboardEvent): void => { // ignore any keypress while doing IME compositions - if (this._editorRef.isComposing(event)) { + if (this.editorRef.current?.isComposing(event)) { return; } const action = getKeyBindingsManager().getMessageComposerAction(event); switch (action) { case MessageComposerAction.Send: - this._sendMessage(); + this.sendMessage(); event.preventDefault(); break; case MessageComposerAction.SelectPrevSendHistory: @@ -165,7 +179,7 @@ export default class SendMessageComposer extends React.Component { } case MessageComposerAction.EditPrevMessage: // selection must be collapsed and caret at start - if (this._editorRef.isSelectionCollapsed() && this._editorRef.isCaretAtStart()) { + if (this.editorRef.current?.isSelectionCollapsed() && this.editorRef.current?.isCaretAtStart()) { const editEvent = findEditableEvent(this.props.room, false); if (editEvent) { // We're selecting history, so prevent the key event from doing anything else @@ -184,16 +198,16 @@ export default class SendMessageComposer extends React.Component { }); break; default: - if (this._prepareToEncrypt) { + if (this.prepareToEncrypt) { // This needs to be last! - this._prepareToEncrypt(); + this.prepareToEncrypt(); } } }; // we keep sent messages/commands in a separate history (separate from undo history) // so you can alt+up/down in them - selectSendHistory(up) { + private selectSendHistory(up: boolean): void { const delta = up ? -1 : 1; // True if we are not currently selecting history, but composing a message if (this.sendHistoryManager.currentIndex === this.sendHistoryManager.history.length) { @@ -215,11 +229,11 @@ export default class SendMessageComposer extends React.Component { }); if (parts) { this.model.reset(parts); - this._editorRef.focus(); + this.editorRef.current?.focus(); } } - _isSlashCommand() { + private isSlashCommand(): boolean { const parts = this.model.parts; const firstPart = parts[0]; if (firstPart) { @@ -237,7 +251,7 @@ export default class SendMessageComposer extends React.Component { return false; } - _sendQuickReaction() { + private sendQuickReaction(): void { const timeline = this.props.room.getLiveTimeline(); const events = timeline.getEvents(); const reaction = this.model.parts[1].text; @@ -272,7 +286,7 @@ export default class SendMessageComposer extends React.Component { } } - _getSlashCommand() { + private getSlashCommand(): [Command, string, string] { const commandText = this.model.parts.reduce((text, part) => { // use mxid to textify user pills in a command if (part.type === "user-pill") { @@ -284,7 +298,7 @@ export default class SendMessageComposer extends React.Component { return [cmd, args, commandText]; } - async _runSlashCommand(cmd, args) { + private async runSlashCommand(cmd: Command, args: string): Promise { const result = cmd.run(this.props.room.roomId, args); let messageContent; let error = result.error; @@ -302,7 +316,6 @@ export default class SendMessageComposer extends React.Component { } if (error) { console.error("Command failure: %s", error); - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); // assume the error is a server error when the command is async const isServerError = !!result.promise; const title = isServerError ? _td("Server error") : _td("Command error"); @@ -326,7 +339,7 @@ export default class SendMessageComposer extends React.Component { } } - async _sendMessage() { + public async sendMessage(): Promise { if (this.model.isEmpty) { return; } @@ -335,21 +348,20 @@ export default class SendMessageComposer extends React.Component { let shouldSend = true; let content; - if (!containsEmote(this.model) && this._isSlashCommand()) { - const [cmd, args, commandText] = this._getSlashCommand(); + if (!containsEmote(this.model) && this.isSlashCommand()) { + const [cmd, args, commandText] = this.getSlashCommand(); if (cmd) { if (cmd.category === CommandCategories.messages) { - content = await this._runSlashCommand(cmd, args); + content = await this.runSlashCommand(cmd, args); if (replyToEvent) { addReplyToMessageContent(content, replyToEvent, this.props.permalinkCreator); } } else { - this._runSlashCommand(cmd, args); + this.runSlashCommand(cmd, args); shouldSend = false; } } else { // ask the user if their unknown command should be sent as a message - const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); const { finished } = Modal.createTrackedDialog("Unknown command", "", QuestionDialog, { title: _t("Unknown Command"), description:
@@ -378,7 +390,7 @@ export default class SendMessageComposer extends React.Component { if (isQuickReaction(this.model)) { shouldSend = false; - this._sendQuickReaction(); + this.sendQuickReaction(); } if (shouldSend) { @@ -411,9 +423,9 @@ export default class SendMessageComposer extends React.Component { this.sendHistoryManager.save(this.model, replyToEvent); // clear composer this.model.reset([]); - this._editorRef.clearUndoHistory(); - this._editorRef.focus(); - this._clearStoredEditorState(); + this.editorRef.current?.clearUndoHistory(); + this.editorRef.current?.focus(); + this.clearStoredEditorState(); if (SettingsStore.getValue("scrollToBottomOnMessageSent")) { dis.dispatch({ action: "scroll_to_bottom" }); } @@ -421,33 +433,33 @@ export default class SendMessageComposer extends React.Component { componentWillUnmount() { dis.unregister(this.dispatcherRef); - window.removeEventListener("beforeunload", this._saveStoredEditorState); - this._saveStoredEditorState(); + window.removeEventListener("beforeunload", this.saveStoredEditorState); + this.saveStoredEditorState(); } // TODO: [REACT-WARNING] Move this to constructor UNSAFE_componentWillMount() { // eslint-disable-line camelcase const partCreator = new CommandPartCreator(this.props.room, this.context); - const parts = this._restoreStoredEditorState(partCreator) || []; + const parts = this.restoreStoredEditorState(partCreator) || []; this.model = new EditorModel(parts, partCreator); this.dispatcherRef = dis.register(this.onAction); this.sendHistoryManager = new SendHistoryManager(this.props.room.roomId, 'mx_cider_history_'); } - get _editorStateKey() { + private get editorStateKey() { return `mx_cider_state_${this.props.room.roomId}`; } - _clearStoredEditorState() { - localStorage.removeItem(this._editorStateKey); + private clearStoredEditorState(): void { + localStorage.removeItem(this.editorStateKey); } - _restoreStoredEditorState(partCreator) { - const json = localStorage.getItem(this._editorStateKey); + private restoreStoredEditorState(partCreator: PartCreator): Part[] { + const json = localStorage.getItem(this.editorStateKey); if (json) { try { const { parts: serializedParts, replyEventId } = JSON.parse(json); - const parts = serializedParts.map(p => partCreator.deserializePart(p)); + const parts: Part[] = serializedParts.map(p => partCreator.deserializePart(p)); if (replyEventId) { dis.dispatch({ action: 'reply_to_event', @@ -462,20 +474,20 @@ export default class SendMessageComposer extends React.Component { } // should save state when editor has contents or reply is open - _shouldSaveStoredEditorState = () => { - return !this.model.isEmpty || this.props.replyToEvent; - } + private shouldSaveStoredEditorState = (): boolean => { + return !this.model.isEmpty || !!this.props.replyToEvent; + }; - _saveStoredEditorState = () => { - if (this._shouldSaveStoredEditorState()) { + private saveStoredEditorState = (): void => { + if (this.shouldSaveStoredEditorState()) { const item = SendHistoryManager.createItem(this.model, this.props.replyToEvent); - localStorage.setItem(this._editorStateKey, JSON.stringify(item)); + localStorage.setItem(this.editorStateKey, JSON.stringify(item)); } else { - this._clearStoredEditorState(); + this.clearStoredEditorState(); } - } + }; - onAction = (payload) => { + private onAction = (payload: ActionPayload): void => { // don't let the user into the composer if it is disabled - all of these branches lead // to the cursor being in the composer if (this.props.disabled) return; @@ -483,21 +495,21 @@ export default class SendMessageComposer extends React.Component { switch (payload.action) { case 'reply_to_event': case Action.FocusComposer: - this._editorRef && this._editorRef.focus(); + this.editorRef.current?.focus(); break; case "send_composer_insert": if (payload.userId) { - this._editorRef && this._editorRef.insertMention(payload.userId); + this.editorRef.current?.insertMention(payload.userId); } else if (payload.event) { - this._editorRef && this._editorRef.insertQuotedMessage(payload.event); + this.editorRef.current?.insertQuotedMessage(payload.event); } else if (payload.text) { - this._editorRef && this._editorRef.insertPlaintext(payload.text); + this.editorRef.current?.insertPlaintext(payload.text); } break; } }; - _onPaste = (event) => { + private onPaste = (event: ClipboardEvent): boolean => { const { clipboardData } = event; // Prioritize text on the clipboard over files as Office on macOS puts a bitmap // in the clipboard as well as the content being copied. @@ -511,23 +523,27 @@ export default class SendMessageComposer extends React.Component { ); return true; // to skip internal onPaste handler } - } + }; - onChange = () => { + private onChange = (): void => { if (this.props.onChange) this.props.onChange(this.model); - } + }; + + private focusComposer = (): void => { + this.editorRef.current?.focus(); + }; render() { return ( -
+
diff --git a/src/editor/model.ts b/src/editor/model.ts index 1e8498a69e..da1c2f47f5 100644 --- a/src/editor/model.ts +++ b/src/editor/model.ts @@ -70,7 +70,7 @@ export default class EditorModel { * on the model that can span multiple parts. Also see `startRange()`. * @param {TransformCallback} transformCallback */ - setTransformCallback(transformCallback: TransformCallback) { + public setTransformCallback(transformCallback: TransformCallback): void { this.transformCallback = transformCallback; } @@ -78,23 +78,23 @@ export default class EditorModel { * Set a callback for rerendering the model after it has been updated. * @param {ModelCallback} updateCallback */ - setUpdateCallback(updateCallback: UpdateCallback) { + public setUpdateCallback(updateCallback: UpdateCallback): void { this.updateCallback = updateCallback; } - get partCreator() { + public get partCreator(): PartCreator { return this._partCreator; } - get isEmpty() { + public get isEmpty(): boolean { return this._parts.reduce((len, part) => len + part.text.length, 0) === 0; } - clone() { + public clone(): EditorModel { return new EditorModel(this._parts, this._partCreator, this.updateCallback); } - private insertPart(index: number, part: Part) { + private insertPart(index: number, part: Part): void { this._parts.splice(index, 0, part); if (this.activePartIdx >= index) { ++this.activePartIdx; @@ -104,7 +104,7 @@ export default class EditorModel { } } - private removePart(index: number) { + private removePart(index: number): void { this._parts.splice(index, 1); if (index === this.activePartIdx) { this.activePartIdx = null; @@ -118,22 +118,22 @@ export default class EditorModel { } } - private replacePart(index: number, part: Part) { + private replacePart(index: number, part: Part): void { this._parts.splice(index, 1, part); } - get parts() { + public get parts(): Part[] { return this._parts; } - get autoComplete() { + public get autoComplete(): AutocompleteWrapperModel { if (this.activePartIdx === this.autoCompletePartIdx) { return this._autoComplete; } return null; } - getPositionAtEnd() { + public getPositionAtEnd(): DocumentPosition { if (this._parts.length) { const index = this._parts.length - 1; const part = this._parts[index]; @@ -144,11 +144,11 @@ export default class EditorModel { } } - serializeParts() { + public serializeParts(): SerializedPart[] { return this._parts.map(p => p.serialize()); } - private diff(newValue: string, inputType: string, caret: DocumentOffset) { + private diff(newValue: string, inputType: string, caret: DocumentOffset): IDiff { const previousValue = this.parts.reduce((text, p) => text + p.text, ""); // can't use caret position with drag and drop if (inputType === "deleteByDrag") { @@ -158,7 +158,7 @@ export default class EditorModel { } } - reset(serializedParts: SerializedPart[], caret?: Caret, inputType?: string) { + public reset(serializedParts: SerializedPart[], caret?: Caret, inputType?: string): void { this._parts = serializedParts.map(p => this._partCreator.deserializePart(p)); if (!caret) { caret = this.getPositionAtEnd(); @@ -180,7 +180,7 @@ export default class EditorModel { * @param {DocumentPosition} position the position to start inserting at * @return {Number} the amount of characters added */ - insert(parts: Part[], position: IPosition) { + public insert(parts: Part[], position: IPosition): number { const insertIndex = this.splitAt(position); let newTextLength = 0; for (let i = 0; i < parts.length; ++i) { @@ -191,7 +191,7 @@ export default class EditorModel { return newTextLength; } - update(newValue: string, inputType: string, caret: DocumentOffset) { + public update(newValue: string, inputType: string, caret: DocumentOffset): Promise { const diff = this.diff(newValue, inputType, caret); const position = this.positionForOffset(diff.at, caret.atNodeEnd); let removedOffsetDecrease = 0; @@ -220,7 +220,7 @@ export default class EditorModel { return Number.isFinite(result) ? result as number : 0; } - private setActivePart(pos: DocumentPosition, canOpenAutoComplete: boolean) { + private setActivePart(pos: DocumentPosition, canOpenAutoComplete: boolean): Promise { const { index } = pos; const part = this._parts[index]; if (part) { @@ -250,7 +250,7 @@ export default class EditorModel { return Promise.resolve(); } - private onAutoComplete = ({ replaceParts, close }: ICallback) => { + private onAutoComplete = ({ replaceParts, close }: ICallback): void => { let pos; if (replaceParts) { this._parts.splice(this.autoCompletePartIdx, this.autoCompletePartCount, ...replaceParts); @@ -270,7 +270,7 @@ export default class EditorModel { this.updateCallback(pos); }; - private mergeAdjacentParts() { + private mergeAdjacentParts(): void { let prevPart; for (let i = 0; i < this._parts.length; ++i) { let part = this._parts[i]; @@ -294,7 +294,7 @@ export default class EditorModel { * @return {Number} how many characters before pos were also removed, * usually because of non-editable parts that can only be removed in their entirety. */ - removeText(pos: IPosition, len: number) { + public removeText(pos: IPosition, len: number): number { let { index, offset } = pos; let removedOffsetDecrease = 0; while (len > 0) { @@ -329,7 +329,7 @@ export default class EditorModel { } // return part index where insertion will insert between at offset - private splitAt(pos: IPosition) { + private splitAt(pos: IPosition): number { if (pos.index === -1) { return 0; } @@ -356,7 +356,7 @@ export default class EditorModel { * @return {Number} how far from position (in characters) the insertion ended. * This can be more than the length of `str` when crossing non-editable parts, which are skipped. */ - private addText(pos: IPosition, str: string, inputType: string) { + private addText(pos: IPosition, str: string, inputType: string): number { let { index } = pos; const { offset } = pos; let addLen = str.length; @@ -390,7 +390,7 @@ export default class EditorModel { return addLen; } - positionForOffset(totalOffset: number, atPartEnd = false) { + public positionForOffset(totalOffset: number, atPartEnd = false): DocumentPosition { let currentOffset = 0; const index = this._parts.findIndex(part => { const partLen = part.text.length; @@ -416,11 +416,11 @@ export default class EditorModel { * @param {DocumentPosition?} positionB the other boundary of the range, optional * @return {Range} */ - startRange(positionA: DocumentPosition, positionB = positionA) { + public startRange(positionA: DocumentPosition, positionB = positionA): Range { return new Range(this, positionA, positionB); } - replaceRange(startPosition: DocumentPosition, endPosition: DocumentPosition, parts: Part[]) { + public replaceRange(startPosition: DocumentPosition, endPosition: DocumentPosition, parts: Part[]): void { // convert end position to offset, so it is independent of how the document is split into parts // which we'll change when splitting up at the start position const endOffset = endPosition.asOffset(this); @@ -445,9 +445,9 @@ export default class EditorModel { * @param {ManualTransformCallback} callback to run the transformations in * @return {Promise} a promise when auto-complete (if applicable) is done updating */ - transform(callback: ManualTransformCallback) { + public transform(callback: ManualTransformCallback): Promise { const pos = callback(); - let acPromise = null; + let acPromise: Promise = null; if (!(pos instanceof Range)) { acPromise = this.setActivePart(pos, true); } else { diff --git a/src/editor/parts.ts b/src/editor/parts.ts index c16a95dbc9..351df5062f 100644 --- a/src/editor/parts.ts +++ b/src/editor/parts.ts @@ -552,7 +552,7 @@ export class PartCreator { // part creator that support auto complete for /commands, // used in SendMessageComposer export class CommandPartCreator extends PartCreator { - createPartForInput(text: string, partIndex: number) { + public createPartForInput(text: string, partIndex: number): Part { // at beginning and starts with /? create if (partIndex === 0 && text[0] === "/") { // text will be inserted by model, so pass empty string @@ -562,11 +562,11 @@ export class CommandPartCreator extends PartCreator { } } - command(text: string) { + public command(text: string): CommandPart { return new CommandPart(text, this.autoCompleteCreator); } - deserializePart(part: Part): Part { + public deserializePart(part: SerializedPart): Part { if (part.type === "command") { return this.command(part.text); } else { @@ -576,7 +576,7 @@ export class CommandPartCreator extends PartCreator { } class CommandPart extends PillCandidatePart { - get type(): IPillCandidatePart["type"] { + public get type(): IPillCandidatePart["type"] { return Type.Command; } } diff --git a/src/utils/EditorStateTransfer.ts b/src/utils/EditorStateTransfer.ts index ba303f9b73..d2ce58f7dc 100644 --- a/src/utils/EditorStateTransfer.ts +++ b/src/utils/EditorStateTransfer.ts @@ -17,7 +17,7 @@ limitations under the License. import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { SerializedPart } from "../editor/parts"; -import { Caret } from "../editor/caret"; +import DocumentOffset from "../editor/offset"; /** * Used while editing, to pass the event, and to preserve editor state @@ -26,28 +26,28 @@ import { Caret } from "../editor/caret"; */ export default class EditorStateTransfer { private serializedParts: SerializedPart[] = null; - private caret: Caret = null; + private caret: DocumentOffset = null; constructor(private readonly event: MatrixEvent) {} - public setEditorState(caret: Caret, serializedParts: SerializedPart[]) { + public setEditorState(caret: DocumentOffset, serializedParts: SerializedPart[]) { this.caret = caret; this.serializedParts = serializedParts; } - public hasEditorState() { + public hasEditorState(): boolean { return !!this.serializedParts; } - public getSerializedParts() { + public getSerializedParts(): SerializedPart[] { return this.serializedParts; } - public getCaret() { + public getCaret(): DocumentOffset { return this.caret; } - public getEvent() { + public getEvent(): MatrixEvent { return this.event; } } diff --git a/src/utils/pillify.js b/src/utils/pillify.tsx similarity index 91% rename from src/utils/pillify.js rename to src/utils/pillify.tsx index 489ba5d504..22240fcda5 100644 --- a/src/utils/pillify.js +++ b/src/utils/pillify.tsx @@ -16,9 +16,11 @@ limitations under the License. import React from "react"; import ReactDOM from 'react-dom'; +import { PushProcessor } from 'matrix-js-sdk/src/pushprocessor'; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; + import { MatrixClientPeg } from '../MatrixClientPeg'; import SettingsStore from "../settings/SettingsStore"; -import { PushProcessor } from 'matrix-js-sdk/src/pushprocessor'; import Pill from "../components/views/elements/Pill"; import { parseAppLocalLink } from "./permalinks/Permalinks"; @@ -27,15 +29,15 @@ import { parseAppLocalLink } from "./permalinks/Permalinks"; * into pills based on the context of a given room. Returns a list of * the resulting React nodes so they can be unmounted rather than leaking. * - * @param {Node[]} nodes - a list of sibling DOM nodes to traverse to try + * @param {Element[]} nodes - a list of sibling DOM nodes to traverse to try * to turn into pills. * @param {MatrixEvent} mxEvent - the matrix event which the DOM nodes are * part of representing. - * @param {Node[]} pills: an accumulator of the DOM nodes which contain + * @param {Element[]} pills: an accumulator of the DOM nodes which contain * React components which have been mounted as part of this. * The initial caller should pass in an empty array to seed the accumulator. */ -export function pillifyLinks(nodes, mxEvent, pills) { +export function pillifyLinks(nodes: ArrayLike, mxEvent: MatrixEvent, pills: Element[]) { const room = MatrixClientPeg.get().getRoom(mxEvent.getRoomId()); const shouldShowPillAvatar = SettingsStore.getValue("Pill.shouldShowPillAvatar"); let node = nodes[0]; @@ -73,7 +75,7 @@ export function pillifyLinks(nodes, mxEvent, pills) { // to clear the pills from the last run of pillifyLinks !node.parentElement.classList.contains("mx_AtRoomPill") ) { - let currentTextNode = node; + let currentTextNode = node as Node as Text; const roomNotifTextNodes = []; // Take a textNode and break it up to make all the instances of @room their @@ -125,10 +127,10 @@ export function pillifyLinks(nodes, mxEvent, pills) { } if (node.childNodes && node.childNodes.length && !pillified) { - pillifyLinks(node.childNodes, mxEvent, pills); + pillifyLinks(node.childNodes as NodeListOf, mxEvent, pills); } - node = node.nextSibling; + node = node.nextSibling as Element; } } @@ -140,10 +142,10 @@ export function pillifyLinks(nodes, mxEvent, pills) { * emitter on BaseAvatar as per * https://github.com/vector-im/element-web/issues/12417 * - * @param {Node[]} pills - array of pill containers whose React + * @param {Element[]} pills - array of pill containers whose React * components should be unmounted. */ -export function unmountPills(pills) { +export function unmountPills(pills: Element[]) { for (const pillContainer of pills) { ReactDOM.unmountComponentAtNode(pillContainer); } diff --git a/test/components/views/rooms/SendMessageComposer-test.js b/test/components/views/rooms/SendMessageComposer-test.js index 2fddf8b691..2947f0fe60 100644 --- a/test/components/views/rooms/SendMessageComposer-test.js +++ b/test/components/views/rooms/SendMessageComposer-test.js @@ -147,7 +147,7 @@ describe('', () => { wrapper.update(); }); - const key = wrapper.find(SendMessageComposer).instance()._editorStateKey; + const key = wrapper.find(SendMessageComposer).instance().editorStateKey; expect(wrapper.text()).toBe("Test Text"); expect(localStorage.getItem(key)).toBeNull(); @@ -188,7 +188,7 @@ describe('', () => { wrapper.update(); }); - const key = wrapper.find(SendMessageComposer).instance()._editorStateKey; + const key = wrapper.find(SendMessageComposer).instance().editorStateKey; expect(wrapper.text()).toBe("Hello World"); expect(localStorage.getItem(key)).toBeNull(); From 0a5abb09f4111533aa44638788368a700ef38557 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 30 Jun 2021 13:03:29 +0100 Subject: [PATCH 076/221] Fixes identified by TS --- src/components/views/rooms/EditMessageComposer.tsx | 2 +- src/components/views/rooms/SendMessageComposer.tsx | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/components/views/rooms/EditMessageComposer.tsx b/src/components/views/rooms/EditMessageComposer.tsx index b06b57dced..9c0bea4e60 100644 --- a/src/components/views/rooms/EditMessageComposer.tsx +++ b/src/components/views/rooms/EditMessageComposer.tsx @@ -395,7 +395,7 @@ export default class EditMessageComposer extends React.Component const sel = document.getSelection(); let caret; if (sel.focusNode) { - caret = getCaretOffsetAndText(this.editorRef.current, sel).caret; + caret = getCaretOffsetAndText(this.editorRef.current?.editorRef.current, sel).caret; } const parts = this.model.serializeParts(); // if caret is undefined because for some reason there isn't a valid selection, diff --git a/src/components/views/rooms/SendMessageComposer.tsx b/src/components/views/rooms/SendMessageComposer.tsx index 2c670f6f64..a934216884 100644 --- a/src/components/views/rooms/SendMessageComposer.tsx +++ b/src/components/views/rooms/SendMessageComposer.tsx @@ -207,20 +207,20 @@ export default class SendMessageComposer extends React.Component { // we keep sent messages/commands in a separate history (separate from undo history) // so you can alt+up/down in them - private selectSendHistory(up: boolean): void { + private selectSendHistory(up: boolean): boolean { const delta = up ? -1 : 1; // True if we are not currently selecting history, but composing a message if (this.sendHistoryManager.currentIndex === this.sendHistoryManager.history.length) { // We can't go any further - there isn't any more history, so nop. if (!up) { - return; + return false; } this.currentlyComposedEditorState = this.model.serializeParts(); } else if (this.sendHistoryManager.currentIndex + delta === this.sendHistoryManager.history.length) { // True when we return to the message being composed currently this.model.reset(this.currentlyComposedEditorState); this.sendHistoryManager.currentIndex = this.sendHistoryManager.history.length; - return; + return true; } const { parts, replyEventId } = this.sendHistoryManager.getItem(delta); dis.dispatch({ @@ -231,6 +231,7 @@ export default class SendMessageComposer extends React.Component { this.model.reset(parts); this.editorRef.current?.focus(); } + return true; } private isSlashCommand(): boolean { From 3a80df422225e5249f84976b7ec2f9e721935640 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 30 Jun 2021 13:45:43 +0100 Subject: [PATCH 077/221] Fix react context not being assigned during construction --- src/components/views/rooms/EditMessageComposer.tsx | 5 +++-- src/components/views/rooms/SendMessageComposer.tsx | 7 ++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/components/views/rooms/EditMessageComposer.tsx b/src/components/views/rooms/EditMessageComposer.tsx index 9c0bea4e60..4e51a0105b 100644 --- a/src/components/views/rooms/EditMessageComposer.tsx +++ b/src/components/views/rooms/EditMessageComposer.tsx @@ -121,14 +121,15 @@ interface IState { @replaceableComponent("views.rooms.EditMessageComposer") export default class EditMessageComposer extends React.Component { static contextType = MatrixClientContext; - context: React.ContextType; + context!: React.ContextType; private readonly editorRef = createRef(); private readonly dispatcherRef: string; private model: EditorModel = null; constructor(props: IProps, context: React.ContextType) { - super(props, context); + super(props); + this.context = context; // otherwise React will only set it prior to render due to type def above this.state = { saveDisabled: true, diff --git a/src/components/views/rooms/SendMessageComposer.tsx b/src/components/views/rooms/SendMessageComposer.tsx index a934216884..f088ec0c8e 100644 --- a/src/components/views/rooms/SendMessageComposer.tsx +++ b/src/components/views/rooms/SendMessageComposer.tsx @@ -136,7 +136,7 @@ interface IProps { @replaceableComponent("views.rooms.SendMessageComposer") export default class SendMessageComposer extends React.Component { static contextType = MatrixClientContext; - context: React.ContextType; + context!: React.ContextType; private readonly prepareToEncrypt?: RateLimitedFunc; private readonly editorRef = createRef(); @@ -146,8 +146,9 @@ export default class SendMessageComposer extends React.Component { private sendHistoryManager: SendHistoryManager; constructor(props: IProps, context: React.ContextType) { - super(props, context); - if (context.isCryptoEnabled() && context.isRoomEncrypted(this.props.room.roomId)) { + super(props); + this.context = context; // otherwise React will only set it prior to render due to type def above + if (this.context.isCryptoEnabled() && this.context.isRoomEncrypted(this.props.room.roomId)) { this.prepareToEncrypt = new RateLimitedFunc(() => { this.context.prepareToEncrypt(this.props.room); }, 60000); From a9f35e8c69c203b5b8c79ef345b4330faad35d06 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Wed, 30 Jun 2021 14:19:39 +0100 Subject: [PATCH 078/221] Lint MXC APIs to centralise access This adds linting rules to ensure that MXC APIs are only accessed via the `Media` helper so they can be customised easily when desired. Fixes https://github.com/vector-im/element-web/issues/16933 --- .eslintrc.js | 11 +++++++++-- src/customisations/Media.ts | 4 ++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index bf6e245b93..381cbd7417 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -54,7 +54,11 @@ module.exports = { "error", ...buildRestrictedPropertiesOptions( ["window.innerHeight", "window.innerWidth", "window.visualViewport"], - "Use UIStore to access window dimensions instead", + "Use UIStore to access window dimensions instead.", + ), + ...buildRestrictedPropertiesOptions( + ["*.mxcUrlToHttp", "*.getHttpUriForMxc"], + "Use Media helper instead to centralise access for customisation.", ), ], }, @@ -63,7 +67,10 @@ module.exports = { function buildRestrictedPropertiesOptions(properties, message) { return properties.map(prop => { - const [object, property] = prop.split("."); + let [object, property] = prop.split("."); + if (object === "*") { + object = undefined; + } return { object, property, diff --git a/src/customisations/Media.ts b/src/customisations/Media.ts index f8ee834102..ce8f88f6f1 100644 --- a/src/customisations/Media.ts +++ b/src/customisations/Media.ts @@ -75,6 +75,7 @@ export class Media { * The HTTP URL for the source media. */ public get srcHttp(): string { + // eslint-disable-next-line no-restricted-properties return this.client.mxcUrlToHttp(this.srcMxc); } @@ -84,6 +85,7 @@ export class Media { */ public get thumbnailHttp(): string | undefined | null { if (!this.hasThumbnail) return null; + // eslint-disable-next-line no-restricted-properties return this.client.mxcUrlToHttp(this.thumbnailMxc); } @@ -100,6 +102,7 @@ export class Media { // scale using the device pixel ratio to keep images clear width = Math.floor(width * window.devicePixelRatio); height = Math.floor(height * window.devicePixelRatio); + // eslint-disable-next-line no-restricted-properties return this.client.mxcUrlToHttp(this.thumbnailMxc, width, height, mode); } @@ -114,6 +117,7 @@ export class Media { // scale using the device pixel ratio to keep images clear width = Math.floor(width * window.devicePixelRatio); height = Math.floor(height * window.devicePixelRatio); + // eslint-disable-next-line no-restricted-properties return this.client.mxcUrlToHttp(this.srcMxc, width, height, mode); } From 2baace7658e0cdbfcb0b0bf48d2157d52d79098a Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Wed, 30 Jun 2021 14:27:03 +0100 Subject: [PATCH 079/221] Apply restricted property rules to all files --- .eslintrc.js | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 381cbd7417..827b373949 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -24,6 +24,18 @@ module.exports = { // It's disabled here, but we should using it sparingly. "react/jsx-no-bind": "off", "react/jsx-key": ["error"], + + "no-restricted-properties": [ + "error", + ...buildRestrictedPropertiesOptions( + ["window.innerHeight", "window.innerWidth", "window.visualViewport"], + "Use UIStore to access window dimensions instead.", + ), + ...buildRestrictedPropertiesOptions( + ["*.mxcUrlToHttp", "*.getHttpUriForMxc"], + "Use Media helper instead to centralise access for customisation.", + ), + ], }, overrides: [{ files: [ @@ -49,18 +61,6 @@ module.exports = { "@typescript-eslint/no-explicit-any": "off", // We'd rather not do this but we do "@typescript-eslint/ban-ts-comment": "off", - - "no-restricted-properties": [ - "error", - ...buildRestrictedPropertiesOptions( - ["window.innerHeight", "window.innerWidth", "window.visualViewport"], - "Use UIStore to access window dimensions instead.", - ), - ...buildRestrictedPropertiesOptions( - ["*.mxcUrlToHttp", "*.getHttpUriForMxc"], - "Use Media helper instead to centralise access for customisation.", - ), - ], }, }], }; From a5a4f2ed7d0eee484b527f8e19138d1b5c65984f Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 30 Jun 2021 13:29:37 -0600 Subject: [PATCH 080/221] Fix linter + merge --- .../views/audio_messages/AudioPlayer.tsx | 2 +- .../views/audio_messages/DurationClock.tsx | 2 +- .../audio_messages/RecordingPlayback.tsx | 6 +- .../views/audio_messages/SeekBar.tsx | 2 +- src/components/views/messages/MAudioBody.js | 112 ------------------ src/components/views/messages/MAudioBody.tsx | 2 +- 6 files changed, 7 insertions(+), 119 deletions(-) delete mode 100644 src/components/views/messages/MAudioBody.js diff --git a/src/components/views/audio_messages/AudioPlayer.tsx b/src/components/views/audio_messages/AudioPlayer.tsx index 2f74c7b069..fa7c10b81c 100644 --- a/src/components/views/audio_messages/AudioPlayer.tsx +++ b/src/components/views/audio_messages/AudioPlayer.tsx @@ -118,6 +118,6 @@ export default class AudioPlayer extends React.PureComponent { />
-
+
; } } diff --git a/src/components/views/audio_messages/DurationClock.tsx b/src/components/views/audio_messages/DurationClock.tsx index f6271d1cf4..81852b5944 100644 --- a/src/components/views/audio_messages/DurationClock.tsx +++ b/src/components/views/audio_messages/DurationClock.tsx @@ -46,7 +46,7 @@ export default class DurationClock extends React.PureComponent { } private onTimeUpdate = (time: number[]) => { - this.setState({durationSeconds: time[1]}); + this.setState({ durationSeconds: time[1] }); }; public render() { diff --git a/src/components/views/audio_messages/RecordingPlayback.tsx b/src/components/views/audio_messages/RecordingPlayback.tsx index 7c7a0a87c1..a0dea1c6db 100644 --- a/src/components/views/audio_messages/RecordingPlayback.tsx +++ b/src/components/views/audio_messages/RecordingPlayback.tsx @@ -15,7 +15,7 @@ limitations under the License. */ import { Playback, PlaybackState } from "../../../voice/Playback"; -import React, {ReactNode} from "react"; +import React, { ReactNode } from "react"; import { UPDATE_EVENT } from "../../../stores/AsyncStore"; import PlaybackWaveform from "./PlaybackWaveform"; import PlayPauseButton from "./PlayPauseButton"; @@ -51,7 +51,7 @@ export default class RecordingPlayback extends React.PureComponent { - this.setState({playbackPhase: ev}); + this.setState({ playbackPhase: ev }); }; public render(): ReactNode { @@ -59,6 +59,6 @@ export default class RecordingPlayback extends React.PureComponent -
+
; } } diff --git a/src/components/views/audio_messages/SeekBar.tsx b/src/components/views/audio_messages/SeekBar.tsx index ed07c44482..5231a2fb79 100644 --- a/src/components/views/audio_messages/SeekBar.tsx +++ b/src/components/views/audio_messages/SeekBar.tsx @@ -105,7 +105,7 @@ export default class SeekBar extends React.PureComponent { max={1} value={this.state.percentage} step={0.001} - style={{'--fillTo': this.state.percentage} as ISeekCSS} + style={{ '--fillTo': this.state.percentage } as ISeekCSS} disabled={this.props.playbackPhase === PlaybackState.Decoding} />; } diff --git a/src/components/views/messages/MAudioBody.js b/src/components/views/messages/MAudioBody.js deleted file mode 100644 index 977c88448b..0000000000 --- a/src/components/views/messages/MAudioBody.js +++ /dev/null @@ -1,112 +0,0 @@ -/* - Copyright 2016 OpenMarket Ltd - - 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. - */ - -import React from 'react'; -import MFileBody from './MFileBody'; - -import { decryptFile } from '../../../utils/DecryptFile'; -import { _t } from '../../../languageHandler'; -import InlineSpinner from '../elements/InlineSpinner'; -import { replaceableComponent } from "../../../utils/replaceableComponent"; -import { mediaFromContent } from "../../../customisations/Media"; - -@replaceableComponent("views.messages.MAudioBody") -export default class MAudioBody extends React.Component { - constructor(props) { - super(props); - this.state = { - playing: false, - decryptedUrl: null, - decryptedBlob: null, - error: null, - }; - } - onPlayToggle() { - this.setState({ - playing: !this.state.playing, - }); - } - - _getContentUrl() { - const media = mediaFromContent(this.props.mxEvent.getContent()); - if (media.isEncrypted) { - return this.state.decryptedUrl; - } else { - return media.srcHttp; - } - } - - componentDidMount() { - const content = this.props.mxEvent.getContent(); - if (content.file !== undefined && this.state.decryptedUrl === null) { - let decryptedBlob; - decryptFile(content.file).then(function(blob) { - decryptedBlob = blob; - return URL.createObjectURL(decryptedBlob); - }).then((url) => { - this.setState({ - decryptedUrl: url, - decryptedBlob: decryptedBlob, - }); - }, (err) => { - console.warn("Unable to decrypt attachment: ", err); - this.setState({ - error: err, - }); - }); - } - } - - componentWillUnmount() { - if (this.state.decryptedUrl) { - URL.revokeObjectURL(this.state.decryptedUrl); - } - } - - render() { - const content = this.props.mxEvent.getContent(); - - if (this.state.error !== null) { - return ( - - - { _t("Error decrypting audio") } - - ); - } - - if (content.file !== undefined && this.state.decryptedUrl === null) { - // Need to decrypt the attachment - // The attachment is decrypted in componentDidMount. - // For now add an img tag with a 16x16 spinner. - // Not sure how tall the audio player is so not sure how tall it should actually be. - return ( - - - - ); - } - - const contentUrl = this._getContentUrl(); - - return ( - - - ); - } -} diff --git a/src/components/views/messages/MAudioBody.tsx b/src/components/views/messages/MAudioBody.tsx index 8791828828..bc7216f42c 100644 --- a/src/components/views/messages/MAudioBody.tsx +++ b/src/components/views/messages/MAudioBody.tsx @@ -105,6 +105,6 @@ export default class MAudioBody extends React.PureComponent { - ) + ); } } From da8a783ca027cc71fd927898f4d680c632b119a3 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 30 Jun 2021 14:51:18 -0600 Subject: [PATCH 081/221] lint --- res/css/views/audio_messages/_AudioPlayer.scss | 2 +- src/components/views/audio_messages/AudioPlayer.tsx | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/res/css/views/audio_messages/_AudioPlayer.scss b/res/css/views/audio_messages/_AudioPlayer.scss index 3f02e54aab..9a65ad008f 100644 --- a/res/css/views/audio_messages/_AudioPlayer.scss +++ b/res/css/views/audio_messages/_AudioPlayer.scss @@ -15,7 +15,7 @@ limitations under the License. */ .mx_AudioPlayer_container { - padding: 16px 12px 11px 12px; + padding: 16px 12px 12px 12px; max-width: 267px; // use max to make the control fit in the files/pinned panels .mx_AudioPlayer_primaryContainer { diff --git a/src/components/views/audio_messages/AudioPlayer.tsx b/src/components/views/audio_messages/AudioPlayer.tsx index fa7c10b81c..66efa64658 100644 --- a/src/components/views/audio_messages/AudioPlayer.tsx +++ b/src/components/views/audio_messages/AudioPlayer.tsx @@ -83,7 +83,8 @@ export default class AudioPlayer extends React.PureComponent { const bytes = this.props.playback.sizeBytes; if (!bytes) return null; - // Not translated as these are units, and therefore universal + // Not translated here - we're just presenting the data which should already + // be translated if needed. return `(${formatBytes(bytes)})`; } From 30d027d2b46f7e4d9e91bd0315ce6bb95c1f3317 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 1 Jul 2021 08:29:53 +0100 Subject: [PATCH 082/221] fix js-sdk lib import --- src/components/views/messages/TextualBody.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/messages/TextualBody.tsx b/src/components/views/messages/TextualBody.tsx index cb9de013bf..f9e0f695ab 100644 --- a/src/components/views/messages/TextualBody.tsx +++ b/src/components/views/messages/TextualBody.tsx @@ -18,7 +18,7 @@ import React, { createRef, SyntheticEvent } from 'react'; import ReactDOM from 'react-dom'; import highlight from 'highlight.js'; import { MatrixEvent } from 'matrix-js-sdk/src/models/event'; -import { MsgType } from "matrix-js-sdk/lib/@types/event"; +import { MsgType } from "matrix-js-sdk/src/@types/event"; import * as HtmlUtils from '../../../HtmlUtils'; import { formatDate } from '../../../DateUtils'; From fcdb0a27d00d8fa46504571399454abfd7aa6738 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 1 Jul 2021 09:53:54 +0100 Subject: [PATCH 083/221] Convert EventTimeline, EventTimelineSet and TimelineWindow to TS --- src/components/structures/FilePanel.tsx | 2 +- src/components/structures/TimelinePanel.tsx | 22 ++++++++++++--------- src/indexing/EventIndex.ts | 6 +++--- 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/src/components/structures/FilePanel.tsx b/src/components/structures/FilePanel.tsx index 21ef0c4f31..14380341d1 100644 --- a/src/components/structures/FilePanel.tsx +++ b/src/components/structures/FilePanel.tsx @@ -129,7 +129,7 @@ class FilePanel extends React.Component { } } - public async fetchFileEventsServer(room: Room): Promise { + public async fetchFileEventsServer(room: Room): Promise { const client = MatrixClientPeg.get(); const filter = new Filter(client.credentials.userId); diff --git a/src/components/structures/TimelinePanel.tsx b/src/components/structures/TimelinePanel.tsx index e4c7d15596..d7d3b33b3e 100644 --- a/src/components/structures/TimelinePanel.tsx +++ b/src/components/structures/TimelinePanel.tsx @@ -16,11 +16,13 @@ limitations under the License. import React, { createRef, ReactNode, SyntheticEvent } from 'react'; import ReactDOM from "react-dom"; -import { Room } from "matrix-js-sdk/src/models/room"; +import { NotificationCountType, Room } from "matrix-js-sdk/src/models/room"; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; -import { TimelineSet } from "matrix-js-sdk/src/models/event-timeline-set"; +import { EventTimelineSet } from "matrix-js-sdk/src/models/event-timeline-set"; import { EventTimeline } from "matrix-js-sdk/src/models/event-timeline"; import { TimelineWindow } from "matrix-js-sdk/src/timeline-window"; +import { EventType, RelationType } from 'matrix-js-sdk/src/@types/event'; +import { SyncState } from 'matrix-js-sdk/src/sync.api'; import SettingsStore from "../../settings/SettingsStore"; import { Layout } from "../../settings/Layout"; @@ -39,10 +41,8 @@ import { UIFeature } from "../../settings/UIFeature"; import { replaceableComponent } from "../../utils/replaceableComponent"; import { arrayFastClone } from "../../utils/arrays"; import MessagePanel from "./MessagePanel"; -import { SyncState } from 'matrix-js-sdk/src/sync.api'; import { IScrollState } from "./ScrollPanel"; import { ActionPayload } from "../../dispatcher/payloads"; -import { EventType } from 'matrix-js-sdk/src/@types/event'; import ResizeNotifier from "../../utils/ResizeNotifier"; import { RoomPermalinkCreator } from "../../utils/permalinks/Permalinks"; import Spinner from "../views/elements/Spinner"; @@ -65,7 +65,7 @@ interface IProps { // representing. This may or may not have a room, depending on what it's // a timeline representing. If it has a room, we maintain RRs etc for // that room. - timelineSet: TimelineSet; + timelineSet: EventTimelineSet; showReadReceipts?: boolean; // Enable managing RRs and RMs. These require the timelineSet to have a room. manageReadReceipts?: boolean; @@ -579,7 +579,7 @@ class TimelinePanel extends React.Component { }); }; - private onRoomTimelineReset = (room: Room, timelineSet: TimelineSet): void => { + private onRoomTimelineReset = (room: Room, timelineSet: EventTimelineSet): void => { if (timelineSet !== this.props.timelineSet) return; if (this.messagePanel.current && this.messagePanel.current.isAtBottom()) { @@ -792,8 +792,8 @@ class TimelinePanel extends React.Component { // that sending an RR for the latest message will set our notif counter // to zero: it may not do this if we send an RR for somewhere before the end. if (this.isAtEndOfLiveTimeline()) { - this.props.timelineSet.room.setUnreadNotificationCount('total', 0); - this.props.timelineSet.room.setUnreadNotificationCount('highlight', 0); + this.props.timelineSet.room.setUnreadNotificationCount(NotificationCountType.Total, 0); + this.props.timelineSet.room.setUnreadNotificationCount(NotificationCountType.Highlight, 0); dis.dispatch({ action: 'on_room_read', roomId: this.props.timelineSet.room.roomId, @@ -1416,7 +1416,11 @@ class TimelinePanel extends React.Component { }); } - private getRelationsForEvent = (...args) => this.props.timelineSet.getRelationsForEvent(...args); + private getRelationsForEvent = ( + eventId: string, + relationType: RelationType, + eventType: EventType | string, + ) => this.props.timelineSet.getRelationsForEvent(eventId, relationType, eventType); render() { // just show a spinner while the timeline loads. diff --git a/src/indexing/EventIndex.ts b/src/indexing/EventIndex.ts index 43b3de42ed..103a2f0809 100644 --- a/src/indexing/EventIndex.ts +++ b/src/indexing/EventIndex.ts @@ -16,7 +16,7 @@ limitations under the License. import { EventEmitter } from "events"; import { RoomMember } from 'matrix-js-sdk/src/models/room-member'; -import { EventTimeline } from 'matrix-js-sdk/src/models/event-timeline'; +import { Direction, EventTimeline } from 'matrix-js-sdk/src/models/event-timeline'; import { Room } from 'matrix-js-sdk/src/models/room'; import { MatrixEvent } from 'matrix-js-sdk/src/models/event'; import { EventTimelineSet } from 'matrix-js-sdk/src/models/event-timeline-set'; @@ -109,7 +109,7 @@ export default class EventIndex extends EventEmitter { // our message crawler. await Promise.all(encryptedRooms.map(async (room) => { const timeline = room.getLiveTimeline(); - const token = timeline.getPaginationToken("b"); + const token = timeline.getPaginationToken(Direction.Backward); const backCheckpoint: ICrawlerCheckpoint = { roomId: room.roomId, @@ -371,7 +371,7 @@ export default class EventIndex extends EventEmitter { if (!room) return; const timeline = room.getLiveTimeline(); - const token = timeline.getPaginationToken("b"); + const token = timeline.getPaginationToken(Direction.Backward); if (!token) { // The room doesn't contain any tokens, meaning the live timeline From d354a0a23b966549a5fab0b8c7c38dbd6a5a3a3d Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 1 Jul 2021 09:57:33 +0100 Subject: [PATCH 084/221] Prevent RoomDirectory from exploding when filterString is wrongly nullified --- src/components/structures/RoomDirectory.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/structures/RoomDirectory.tsx b/src/components/structures/RoomDirectory.tsx index 2ac990436f..3acd9f1a2e 100644 --- a/src/components/structures/RoomDirectory.tsx +++ b/src/components/structures/RoomDirectory.tsx @@ -370,7 +370,7 @@ export default class RoomDirectory extends React.Component { private onFilterChange = (alias: string) => { this.setState({ - filterString: alias || null, + filterString: alias || "", }); // don't send the request for a little bit, @@ -389,7 +389,7 @@ export default class RoomDirectory extends React.Component { private onFilterClear = () => { // update immediately this.setState({ - filterString: null, + filterString: "", }, this.refreshRoomList); if (this.filterTimeout) { From 5c6466d8245fc3b700fbb5217f03c3abd98e521c Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 1 Jul 2021 10:01:48 +0100 Subject: [PATCH 085/221] use better types --- src/Terms.ts | 3 ++- src/components/structures/FilePanel.tsx | 7 ++++++- src/components/structures/TimelinePanel.tsx | 4 ++-- src/indexing/EventIndex.ts | 2 +- 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/Terms.ts b/src/Terms.ts index 3859cc1c73..e96081b7b7 100644 --- a/src/Terms.ts +++ b/src/Terms.ts @@ -15,6 +15,7 @@ limitations under the License. */ import classNames from 'classnames'; +import { SERVICE_TYPES } from '../../matrix-js-sdk/src/service-types'; import { MatrixClientPeg } from './MatrixClientPeg'; import * as sdk from '.'; @@ -32,7 +33,7 @@ export class Service { * @param {string} baseUrl The Base URL of the service (ie. before '/_matrix') * @param {string} accessToken The user's access token for the service */ - constructor(public serviceType: string, public baseUrl: string, public accessToken: string) { + constructor(public serviceType: SERVICE_TYPES, public baseUrl: string, public accessToken: string) { } } diff --git a/src/components/structures/FilePanel.tsx b/src/components/structures/FilePanel.tsx index 14380341d1..d47ea11a3c 100644 --- a/src/components/structures/FilePanel.tsx +++ b/src/components/structures/FilePanel.tsx @@ -19,6 +19,7 @@ import React from 'react'; import { Filter } from 'matrix-js-sdk/src/filter'; import { EventTimelineSet } from "matrix-js-sdk/src/models/event-timeline-set"; +import { Direction } from "matrix-js-sdk/src/models/event-timeline"; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { Room } from 'matrix-js-sdk/src/models/room'; import { TimelineWindow } from 'matrix-js-sdk/src/timeline-window'; @@ -153,7 +154,11 @@ class FilePanel extends React.Component { return timelineSet; } - private onPaginationRequest = (timelineWindow: TimelineWindow, direction: string, limit: number): void => { + private onPaginationRequest = ( + timelineWindow: TimelineWindow, + direction: Direction, + limit: number, + ): Promise => { const client = MatrixClientPeg.get(); const eventIndex = EventIndexPeg.get(); const roomId = this.props.roomId; diff --git a/src/components/structures/TimelinePanel.tsx b/src/components/structures/TimelinePanel.tsx index d7d3b33b3e..9253fb786b 100644 --- a/src/components/structures/TimelinePanel.tsx +++ b/src/components/structures/TimelinePanel.tsx @@ -19,7 +19,7 @@ import ReactDOM from "react-dom"; import { NotificationCountType, Room } from "matrix-js-sdk/src/models/room"; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { EventTimelineSet } from "matrix-js-sdk/src/models/event-timeline-set"; -import { EventTimeline } from "matrix-js-sdk/src/models/event-timeline"; +import { Direction, EventTimeline } from "matrix-js-sdk/src/models/event-timeline"; import { TimelineWindow } from "matrix-js-sdk/src/timeline-window"; import { EventType, RelationType } from 'matrix-js-sdk/src/@types/event'; import { SyncState } from 'matrix-js-sdk/src/sync.api'; @@ -388,7 +388,7 @@ class TimelinePanel extends React.Component { private onPaginationRequest = ( timelineWindow: TimelineWindow, - direction: string, + direction: Direction, size: number, ): Promise => { if (this.props.onPaginationRequest) { diff --git a/src/indexing/EventIndex.ts b/src/indexing/EventIndex.ts index 103a2f0809..c84d446197 100644 --- a/src/indexing/EventIndex.ts +++ b/src/indexing/EventIndex.ts @@ -862,7 +862,7 @@ export default class EventIndex extends EventEmitter { * @returns {Promise} Resolves to a boolean which is true if more * events were successfully retrieved. */ - public paginateTimelineWindow(room: Room, timelineWindow: TimelineWindow, direction: string, limit: number) { + public paginateTimelineWindow(room: Room, timelineWindow: TimelineWindow, direction: Direction, limit: number) { const tl = timelineWindow.getTimelineIndex(direction); if (!tl) return Promise.resolve(false); From d47194e61d1f6833bdf7f91c324b0fbf3a5e03fb Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Thu, 1 Jul 2021 11:17:18 +0100 Subject: [PATCH 086/221] Migrate SearchResultTile to TypeScript --- src/components/views/rooms/EventTile.tsx | 2 +- ...archResultTile.js => SearchResultTile.tsx} | 36 +++++++++---------- 2 files changed, 17 insertions(+), 21 deletions(-) rename src/components/views/rooms/{SearchResultTile.js => SearchResultTile.tsx} (76%) diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 277f3ccb7c..baaaa16b57 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -267,7 +267,7 @@ interface IProps { showReactions?: boolean; // which layout to use - layout: Layout; + layout?: Layout; // whether or not to show flair at all enableFlair?: boolean; diff --git a/src/components/views/rooms/SearchResultTile.js b/src/components/views/rooms/SearchResultTile.tsx similarity index 76% rename from src/components/views/rooms/SearchResultTile.js rename to src/components/views/rooms/SearchResultTile.tsx index 3581a26351..766abaff69 100644 --- a/src/components/views/rooms/SearchResultTile.js +++ b/src/components/views/rooms/SearchResultTile.tsx @@ -16,31 +16,27 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; -import * as sdk from '../../../index'; -import { haveTileForEvent } from "./EventTile"; +import EventTile, { haveTileForEvent } from "./EventTile"; +import DateSeparator from '../messages/DateSeparator'; import SettingsStore from "../../../settings/SettingsStore"; import { UIFeature } from "../../../settings/UIFeature"; +import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks'; import { replaceableComponent } from "../../../utils/replaceableComponent"; +interface IProps { + // a matrix-js-sdk SearchResult containing the details of this result + searchResult: any; + // a list of strings to be highlighted in the results + searchHighlights?: string[]; + // href for the highlights in this result + resultLink?: string; + onHeightChanged?: () => void; + permalinkCreator?: RoomPermalinkCreator; +} + @replaceableComponent("views.rooms.SearchResultTile") -export default class SearchResultTile extends React.Component { - static propTypes = { - // a matrix-js-sdk SearchResult containing the details of this result - searchResult: PropTypes.object.isRequired, - - // a list of strings to be highlighted in the results - searchHighlights: PropTypes.array, - - // href for the highlights in this result - resultLink: PropTypes.string, - - onHeightChanged: PropTypes.func, - }; - - render() { - const DateSeparator = sdk.getComponent('messages.DateSeparator'); - const EventTile = sdk.getComponent('rooms.EventTile'); +export default class SearchResultTile extends React.Component { + public render() { const result = this.props.searchResult; const mxEv = result.context.getEvent(); const eventId = mxEv.getId(); From 6f62233634b0913da5384c42153ad83768e86bb2 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Thu, 1 Jul 2021 11:18:07 +0100 Subject: [PATCH 087/221] Prevent browser to crash when unclosed HTML tag is sent to sanitizeHtml --- src/HtmlUtils.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index c80b50c566..59ec8811aa 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -37,6 +37,7 @@ import { tryTransformPermalinkToLocalHref } from "./utils/permalinks/Permalinks" import { SHORTCODE_TO_EMOJI, getEmojiFromUnicode } from "./emoji"; import ReplyThread from "./components/views/elements/ReplyThread"; import { mediaFromMxc } from "./customisations/Media"; +import { highlight } from 'highlight.js'; linkifyMatrix(linkify); @@ -403,9 +404,11 @@ export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts try { if (highlights && highlights.length > 0) { const highlighter = new HtmlHighlighter("mx_EventTile_searchHighlight", opts.highlightLink); - const safeHighlights = highlights.map(function(highlight) { - return sanitizeHtml(highlight, sanitizeParams); - }); + const safeHighlights = highlights + // sanitizeHtml can hang if an unclosed HTML tag is thrown at it + // A search for ` !highlight.includes("<")) + .map((highlight: string): string => sanitizeHtml(highlight, sanitizeParams)); // XXX: hacky bodge to temporarily apply a textFilter to the sanitizeParams structure. sanitizeParams.textFilter = function(safeText) { return highlighter.applyHighlights(safeText, safeHighlights).join(''); From ede87129b2512146be554fff69dd994359f6f969 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Thu, 1 Jul 2021 11:45:29 +0100 Subject: [PATCH 088/221] Add a layout fallback for EventTile --- src/components/views/rooms/EventTile.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index baaaa16b57..0c4d2f6fa9 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -267,7 +267,7 @@ interface IProps { showReactions?: boolean; // which layout to use - layout?: Layout; + layout: Layout; // whether or not to show flair at all enableFlair?: boolean; @@ -321,6 +321,7 @@ export default class EventTile extends React.Component { static defaultProps = { // no-op function because onHeightChanged is optional yet some sub-components assume its existence onHeightChanged: function() {}, + layout: Layout.Group, }; static contextType = MatrixClientContext; From 04db8333e3059b6ddc89f5e34d9e402461bf4c0b Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Thu, 1 Jul 2021 12:13:43 +0100 Subject: [PATCH 089/221] Fix typing and unused import --- src/HtmlUtils.tsx | 1 - src/components/views/rooms/SearchResultTile.tsx | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index 59ec8811aa..4a1ad2f074 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -37,7 +37,6 @@ import { tryTransformPermalinkToLocalHref } from "./utils/permalinks/Permalinks" import { SHORTCODE_TO_EMOJI, getEmojiFromUnicode } from "./emoji"; import ReplyThread from "./components/views/elements/ReplyThread"; import { mediaFromMxc } from "./customisations/Media"; -import { highlight } from 'highlight.js'; linkifyMatrix(linkify); diff --git a/src/components/views/rooms/SearchResultTile.tsx b/src/components/views/rooms/SearchResultTile.tsx index 766abaff69..980e8835f8 100644 --- a/src/components/views/rooms/SearchResultTile.tsx +++ b/src/components/views/rooms/SearchResultTile.tsx @@ -16,6 +16,7 @@ limitations under the License. */ import React from 'react'; +import { SearchResult } from "matrix-js-sdk/src/models/search-result"; import EventTile, { haveTileForEvent } from "./EventTile"; import DateSeparator from '../messages/DateSeparator'; import SettingsStore from "../../../settings/SettingsStore"; @@ -25,7 +26,7 @@ import { replaceableComponent } from "../../../utils/replaceableComponent"; interface IProps { // a matrix-js-sdk SearchResult containing the details of this result - searchResult: any; + searchResult: SearchResult; // a list of strings to be highlighted in the results searchHighlights?: string[]; // href for the highlights in this result From 9c93b9002f32dbb219b103ef7efb0544b639aaa5 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Thu, 1 Jul 2021 12:23:36 +0100 Subject: [PATCH 090/221] Add extra context for filtering out '>' for sanitizeHtml --- src/HtmlUtils.tsx | 3 +++ src/components/views/rooms/EventTile.tsx | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index 4a1ad2f074..91245c943e 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -406,6 +406,9 @@ export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts const safeHighlights = highlights // sanitizeHtml can hang if an unclosed HTML tag is thrown at it // A search for ` !highlight.includes("<")) .map((highlight: string): string => sanitizeHtml(highlight, sanitizeParams)); // XXX: hacky bodge to temporarily apply a textFilter to the sanitizeParams structure. diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 0c4d2f6fa9..cebb631708 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -267,7 +267,7 @@ interface IProps { showReactions?: boolean; // which layout to use - layout: Layout; + layout?: Layout; // whether or not to show flair at all enableFlair?: boolean; From c9fa3470156e567cc8af660cb811e2b2298adcdc Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Thu, 1 Jul 2021 12:27:06 +0100 Subject: [PATCH 091/221] Upgrade browserlist target versions --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 89e11fcea5..6f08372e18 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2345,9 +2345,9 @@ camelcase@^6.0.0: integrity sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg== caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001173: - version "1.0.30001178" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001178.tgz#3ad813b2b2c7d585b0be0a2440e1e233c6eabdbc" - integrity sha512-VtdZLC0vsXykKni8Uztx45xynytOi71Ufx9T8kHptSw9AL4dpqailUJJHavttuzUe1KYuBYtChiWv+BAb7mPmQ== + version "1.0.30001241" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001241.tgz" + integrity sha512-1uoSZ1Pq1VpH0WerIMqwptXHNNGfdl7d1cJUFs80CwQ/lVzdhTvsFZCeNFslze7AjsQnb4C85tzclPa1VShbeQ== capture-exit@^2.0.0: version "2.0.0" From 46b2f0404a6f192b29903a9b85a44b94ec757bdf Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 1 Jul 2021 13:45:33 +0100 Subject: [PATCH 092/221] Basic ts-ification of SetupEncryptionBody --- ...ryptionBody.js => SetupEncryptionBody.tsx} | 40 ++++++++++++------- .../views/right_panel/EncryptionPanel.tsx | 2 +- 2 files changed, 27 insertions(+), 15 deletions(-) rename src/components/structures/auth/{SetupEncryptionBody.js => SetupEncryptionBody.tsx} (91%) diff --git a/src/components/structures/auth/SetupEncryptionBody.js b/src/components/structures/auth/SetupEncryptionBody.tsx similarity index 91% rename from src/components/structures/auth/SetupEncryptionBody.js rename to src/components/structures/auth/SetupEncryptionBody.tsx index f0798b6d1a..0bda596b5c 100644 --- a/src/components/structures/auth/SetupEncryptionBody.js +++ b/src/components/structures/auth/SetupEncryptionBody.tsx @@ -1,5 +1,5 @@ /* -Copyright 2020 The Matrix.org Foundation C.I.C. +Copyright 2020-2021 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. @@ -15,7 +15,6 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import { _t } from '../../../languageHandler'; import { MatrixClientPeg } from '../../../MatrixClientPeg'; import Modal from '../../../Modal'; @@ -23,23 +22,31 @@ import VerificationRequestDialog from '../../views/dialogs/VerificationRequestDi import * as sdk from '../../../index'; import { SetupEncryptionStore, Phase } from '../../../stores/SetupEncryptionStore'; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { ISecretStorageKeyInfo } from 'matrix-js-sdk'; +import EncryptionPanel from "../../views/right_panel/EncryptionPanel" -function keyHasPassphrase(keyInfo) { - return ( +function keyHasPassphrase(keyInfo: ISecretStorageKeyInfo): boolean { + return Boolean( keyInfo.passphrase && keyInfo.passphrase.salt && keyInfo.passphrase.iterations ); } -@replaceableComponent("structures.auth.SetupEncryptionBody") -export default class SetupEncryptionBody extends React.Component { - static propTypes = { - onFinished: PropTypes.func.isRequired, - }; +interface IProps { + onFinished: (boolean) => void; +} - constructor() { - super(); +interface IState { + phase: Phase; + verificationRequest: any; + backupInfo: any; +} + +@replaceableComponent("structures.auth.SetupEncryptionBody") +export default class SetupEncryptionBody extends React.Component { + constructor(props) { + super(props); const store = SetupEncryptionStore.sharedInstance(); store.on("update", this._onStoreUpdate); store.start(); @@ -56,7 +63,7 @@ export default class SetupEncryptionBody extends React.Component { _onStoreUpdate = () => { const store = SetupEncryptionStore.sharedInstance(); if (store.phase === Phase.Finished) { - this.props.onFinished(); + this.props.onFinished(true); return; } this.setState({ @@ -113,6 +120,10 @@ export default class SetupEncryptionBody extends React.Component { store.done(); } + onEncryptionPanelClose = () => { + this.props.onFinished(false); + } + render() { const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); @@ -121,12 +132,13 @@ export default class SetupEncryptionBody extends React.Component { } = this.state; if (this.state.verificationRequest) { - const EncryptionPanel = sdk.getComponent("views.right_panel.EncryptionPanel"); return ; } else if (phase === Phase.Intro) { const store = SetupEncryptionStore.sharedInstance(); diff --git a/src/components/views/right_panel/EncryptionPanel.tsx b/src/components/views/right_panel/EncryptionPanel.tsx index 3a26427246..b2e6e5fc2d 100644 --- a/src/components/views/right_panel/EncryptionPanel.tsx +++ b/src/components/views/right_panel/EncryptionPanel.tsx @@ -39,7 +39,7 @@ interface IProps { member: RoomMember | User; onClose: () => void; verificationRequest: VerificationRequest; - verificationRequestPromise: Promise; + verificationRequestPromise?: Promise; layout: string; inDialog: boolean; isRoomEncrypted: boolean; From 70a3679d4363fb3dd227635d2c4b278daddb12f4 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 1 Jul 2021 13:49:58 +0100 Subject: [PATCH 093/221] more ts --- .../structures/auth/SetupEncryptionBody.tsx | 33 +++++++++---------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/src/components/structures/auth/SetupEncryptionBody.tsx b/src/components/structures/auth/SetupEncryptionBody.tsx index 0bda596b5c..35377d2a0e 100644 --- a/src/components/structures/auth/SetupEncryptionBody.tsx +++ b/src/components/structures/auth/SetupEncryptionBody.tsx @@ -24,6 +24,8 @@ import { SetupEncryptionStore, Phase } from '../../../stores/SetupEncryptionStor import { replaceableComponent } from "../../../utils/replaceableComponent"; import { ISecretStorageKeyInfo } from 'matrix-js-sdk'; import EncryptionPanel from "../../views/right_panel/EncryptionPanel" +import AccessibleButton from '../../views/elements/AccessibleButton'; +import Spinner from '../../views/elements/Spinner'; function keyHasPassphrase(keyInfo: ISecretStorageKeyInfo): boolean { return Boolean( @@ -48,7 +50,7 @@ export default class SetupEncryptionBody extends React.Component constructor(props) { super(props); const store = SetupEncryptionStore.sharedInstance(); - store.on("update", this._onStoreUpdate); + store.on("update", this.onStoreUpdate); store.start(); this.state = { phase: store.phase, @@ -60,7 +62,7 @@ export default class SetupEncryptionBody extends React.Component }; } - _onStoreUpdate = () => { + private onStoreUpdate = () => { const store = SetupEncryptionStore.sharedInstance(); if (store.phase === Phase.Finished) { this.props.onFinished(true); @@ -73,18 +75,18 @@ export default class SetupEncryptionBody extends React.Component }); }; - componentWillUnmount() { + public componentWillUnmount() { const store = SetupEncryptionStore.sharedInstance(); - store.off("update", this._onStoreUpdate); + store.off("update", this.onStoreUpdate); store.stop(); } - _onUsePassphraseClick = async () => { + private onUsePassphraseClick = async () => { const store = SetupEncryptionStore.sharedInstance(); store.usePassPhrase(); } - _onVerifyClick = () => { + private onVerifyClick = () => { const cli = MatrixClientPeg.get(); const userId = cli.getUserId(); const requestPromise = cli.requestVerification(userId); @@ -100,33 +102,31 @@ export default class SetupEncryptionBody extends React.Component }); } - onSkipClick = () => { + private onSkipClick = () => { const store = SetupEncryptionStore.sharedInstance(); store.skip(); } - onSkipConfirmClick = () => { + private onSkipConfirmClick = () => { const store = SetupEncryptionStore.sharedInstance(); store.skipConfirm(); } - onSkipBackClick = () => { + private onSkipBackClick = () => { const store = SetupEncryptionStore.sharedInstance(); store.returnAfterSkip(); } - onDoneClick = () => { + private onDoneClick = () => { const store = SetupEncryptionStore.sharedInstance(); store.done(); } - onEncryptionPanelClose = () => { + private onEncryptionPanelClose = () => { this.props.onFinished(false); } - render() { - const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); - + public render() { const { phase, } = this.state; @@ -151,14 +151,14 @@ export default class SetupEncryptionBody extends React.Component let useRecoveryKeyButton; if (recoveryKeyPrompt) { - useRecoveryKeyButton = + useRecoveryKeyButton = {recoveryKeyPrompt} ; } let verifyButton; if (store.hasDevicesToVerifyAgainst) { - verifyButton = + verifyButton = { _t("Use another login") } ; } @@ -229,7 +229,6 @@ export default class SetupEncryptionBody extends React.Component
); } else if (phase === Phase.Busy || phase === Phase.Loading) { - const Spinner = sdk.getComponent('views.elements.Spinner'); return ; } else { console.log(`SetupEncryptionBody: Unknown phase ${phase}`); From 642405dbd04cd4ca7676cbad5d92e873a1fe7186 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 1 Jul 2021 13:55:57 +0100 Subject: [PATCH 094/221] Use new types --- src/components/structures/auth/SetupEncryptionBody.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/structures/auth/SetupEncryptionBody.tsx b/src/components/structures/auth/SetupEncryptionBody.tsx index 35377d2a0e..bd03c5bddb 100644 --- a/src/components/structures/auth/SetupEncryptionBody.tsx +++ b/src/components/structures/auth/SetupEncryptionBody.tsx @@ -19,13 +19,14 @@ import { _t } from '../../../languageHandler'; import { MatrixClientPeg } from '../../../MatrixClientPeg'; import Modal from '../../../Modal'; import VerificationRequestDialog from '../../views/dialogs/VerificationRequestDialog'; -import * as sdk from '../../../index'; import { SetupEncryptionStore, Phase } from '../../../stores/SetupEncryptionStore'; import { replaceableComponent } from "../../../utils/replaceableComponent"; import { ISecretStorageKeyInfo } from 'matrix-js-sdk'; import EncryptionPanel from "../../views/right_panel/EncryptionPanel" import AccessibleButton from '../../views/elements/AccessibleButton'; import Spinner from '../../views/elements/Spinner'; +import { IKeyBackupInfo } from "matrix-js-sdk/src/crypto/keybackup"; +import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; function keyHasPassphrase(keyInfo: ISecretStorageKeyInfo): boolean { return Boolean( @@ -41,8 +42,8 @@ interface IProps { interface IState { phase: Phase; - verificationRequest: any; - backupInfo: any; + verificationRequest: VerificationRequest; + backupInfo: IKeyBackupInfo; } @replaceableComponent("structures.auth.SetupEncryptionBody") From 16fb24fa09c39c2ead32ddd6d58a7433d5c20cf5 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 1 Jul 2021 14:03:01 +0100 Subject: [PATCH 095/221] Remove unused prop --- src/components/structures/auth/SetupEncryptionBody.tsx | 1 - src/components/views/right_panel/EncryptionPanel.tsx | 1 - 2 files changed, 2 deletions(-) diff --git a/src/components/structures/auth/SetupEncryptionBody.tsx b/src/components/structures/auth/SetupEncryptionBody.tsx index bd03c5bddb..81fee432f6 100644 --- a/src/components/structures/auth/SetupEncryptionBody.tsx +++ b/src/components/structures/auth/SetupEncryptionBody.tsx @@ -135,7 +135,6 @@ export default class SetupEncryptionBody extends React.Component if (this.state.verificationRequest) { return ; layout: string; - inDialog: boolean; isRoomEncrypted: boolean; } From 5d3b94b7c9e8e2c8c97a41bc43bae1fdbd2b4b0f Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 1 Jul 2021 14:22:58 +0100 Subject: [PATCH 096/221] Convert VerificationRequestDialog --- ...ialog.js => VerificationRequestDialog.tsx} | 43 +++++++++++-------- .../views/toasts/VerificationRequestToast.tsx | 3 +- 2 files changed, 25 insertions(+), 21 deletions(-) rename src/components/views/dialogs/{VerificationRequestDialog.js => VerificationRequestDialog.tsx} (70%) diff --git a/src/components/views/dialogs/VerificationRequestDialog.js b/src/components/views/dialogs/VerificationRequestDialog.tsx similarity index 70% rename from src/components/views/dialogs/VerificationRequestDialog.js rename to src/components/views/dialogs/VerificationRequestDialog.tsx index bf5d63b895..4d3123c274 100644 --- a/src/components/views/dialogs/VerificationRequestDialog.js +++ b/src/components/views/dialogs/VerificationRequestDialog.tsx @@ -1,5 +1,5 @@ /* -Copyright 2020 The Matrix.org Foundation C.I.C. +Copyright 2020-2021 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. @@ -15,27 +15,33 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import { MatrixClientPeg } from '../../../MatrixClientPeg'; -import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; +import BaseDialog from "./BaseDialog"; +import EncryptionPanel from "../right_panel/EncryptionPanel"; +import { User } from 'matrix-js-sdk'; + +interface IProps { + verificationRequest: VerificationRequest; + verificationRequestPromise: Promise; + onFinished: () => void; + member: User; +} + +interface IState { + verificationRequest: VerificationRequest; +} @replaceableComponent("views.dialogs.VerificationRequestDialog") -export default class VerificationRequestDialog extends React.Component { - static propTypes = { - verificationRequest: PropTypes.object, - verificationRequestPromise: PropTypes.object, - onFinished: PropTypes.func.isRequired, - member: PropTypes.string, - }; - - constructor(...args) { - super(...args); - this.state = {}; - if (this.props.verificationRequest) { - this.state.verificationRequest = this.props.verificationRequest; - } else if (this.props.verificationRequestPromise) { +export default class VerificationRequestDialog extends React.Component { + constructor(props) { + super(props); + this.state = { + verificationRequest: this.props.verificationRequest, + }; + if (this.props.verificationRequestPromise) { this.props.verificationRequestPromise.then(r => { this.setState({ verificationRequest: r }); }); @@ -43,8 +49,6 @@ export default class VerificationRequestDialog extends React.Component { } render() { - const BaseDialog = sdk.getComponent("views.dialogs.BaseDialog"); - const EncryptionPanel = sdk.getComponent("views.right_panel.EncryptionPanel"); const request = this.state.verificationRequest; const otherUserId = request && request.otherUserId; const member = this.props.member || @@ -65,6 +69,7 @@ export default class VerificationRequestDialog extends React.Component { verificationRequestPromise={this.props.verificationRequestPromise} onClose={this.props.onFinished} member={member} + isRoomEncrypted={false} /> ; } diff --git a/src/components/views/toasts/VerificationRequestToast.tsx b/src/components/views/toasts/VerificationRequestToast.tsx index 8f6b552334..14b3c23737 100644 --- a/src/components/views/toasts/VerificationRequestToast.tsx +++ b/src/components/views/toasts/VerificationRequestToast.tsx @@ -16,7 +16,6 @@ limitations under the License. import React from "react"; -import * as sdk from "../../../index"; import { _t } from '../../../languageHandler'; import { MatrixClientPeg } from '../../../MatrixClientPeg'; import { RightPanelPhases } from "../../../stores/RightPanelStorePhases"; @@ -30,6 +29,7 @@ import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/reque import { DeviceInfo } from "matrix-js-sdk/src/crypto/deviceinfo"; import { Action } from "../../../dispatcher/actions"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import VerificationRequestDialog from "../dialogs/VerificationRequestDialog" interface IProps { toastKey: string; @@ -123,7 +123,6 @@ export default class VerificationRequestToast extends React.PureComponent { From 676163d5cd81312c813662343176c710ff885c52 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 1 Jul 2021 14:51:07 +0100 Subject: [PATCH 097/221] convert NewSessionReviewDialog --- ...ewDialog.js => NewSessionReviewDialog.tsx} | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) rename src/components/views/dialogs/{NewSessionReviewDialog.js => NewSessionReviewDialog.tsx} (90%) diff --git a/src/components/views/dialogs/NewSessionReviewDialog.js b/src/components/views/dialogs/NewSessionReviewDialog.tsx similarity index 90% rename from src/components/views/dialogs/NewSessionReviewDialog.js rename to src/components/views/dialogs/NewSessionReviewDialog.tsx index 749f48ef48..ae334474ef 100644 --- a/src/components/views/dialogs/NewSessionReviewDialog.js +++ b/src/components/views/dialogs/NewSessionReviewDialog.tsx @@ -1,5 +1,5 @@ /* -Copyright 2020 The Matrix.org Foundation C.I.C. +Copyright 2020-2021 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. @@ -15,7 +15,6 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import { _t } from '../../../languageHandler'; import Modal from '../../../Modal'; import { replaceableComponent } from '../../../utils/replaceableComponent'; @@ -23,18 +22,18 @@ import VerificationRequestDialog from './VerificationRequestDialog'; import BaseDialog from './BaseDialog'; import DialogButtons from '../elements/DialogButtons'; import { MatrixClientPeg } from "../../../MatrixClientPeg"; -import * as sdk from '../../../index'; +import { DeviceInfo } from 'matrix-js-sdk/src/crypto/deviceinfo'; +import ErrorDialog from "./ErrorDialog"; + +interface IProps { + userId: string; + device: DeviceInfo, + onFinished: (boolean) => void; +} @replaceableComponent("views.dialogs.NewSessionReviewDialog") -export default class NewSessionReviewDialog extends React.PureComponent { - static propTypes = { - userId: PropTypes.string.isRequired, - device: PropTypes.object.isRequired, - onFinished: PropTypes.func.isRequired, - } - - onCancelClick = () => { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); +export default class NewSessionReviewDialog extends React.PureComponent { + private onCancelClick = () => { Modal.createTrackedDialog("Verification failed", "insecure", ErrorDialog, { headerImage: require("../../../../res/img/e2e/warning.svg"), title: _t("Your account is not secure"), @@ -54,7 +53,7 @@ export default class NewSessionReviewDialog extends React.PureComponent { }); } - onContinueClick = () => { + private onContinueClick = () => { const { userId, device } = this.props; const cli = MatrixClientPeg.get(); const requestPromise = cli.requestVerification( @@ -73,7 +72,7 @@ export default class NewSessionReviewDialog extends React.PureComponent { }); } - render() { + public render() { const { device } = this.props; const icon = ; From 66d95ed7b24dd8b59adee380fa29c2aec6a7f3f0 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 1 Jul 2021 14:51:20 +0100 Subject: [PATCH 098/221] Fix button styling in verification bubbles --- res/css/views/messages/_common_CryptoEvent.scss | 1 + src/components/views/messages/MKeyVerificationRequest.tsx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/res/css/views/messages/_common_CryptoEvent.scss b/res/css/views/messages/_common_CryptoEvent.scss index bcc40f1181..d43df1b66c 100644 --- a/res/css/views/messages/_common_CryptoEvent.scss +++ b/res/css/views/messages/_common_CryptoEvent.scss @@ -43,6 +43,7 @@ limitations under the License. .mx_cryptoEvent_state, .mx_cryptoEvent_buttons { grid-column: 3; grid-row: 1 / 3; + gap: 5px; } .mx_cryptoEvent_buttons { diff --git a/src/components/views/messages/MKeyVerificationRequest.tsx b/src/components/views/messages/MKeyVerificationRequest.tsx index d690513d55..8b8eb25f6b 100644 --- a/src/components/views/messages/MKeyVerificationRequest.tsx +++ b/src/components/views/messages/MKeyVerificationRequest.tsx @@ -154,7 +154,7 @@ export default class MKeyVerificationRequest extends React.Component { {_t("Decline")} - + {_t("Accept")}
); From 7e8bb70621db1e293503f5c5fbfdae19c660135d Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 1 Jul 2021 14:55:00 +0100 Subject: [PATCH 099/221] Never mind, it wasn't even used --- .../views/dialogs/NewSessionReviewDialog.tsx | 120 ------------------ 1 file changed, 120 deletions(-) delete mode 100644 src/components/views/dialogs/NewSessionReviewDialog.tsx diff --git a/src/components/views/dialogs/NewSessionReviewDialog.tsx b/src/components/views/dialogs/NewSessionReviewDialog.tsx deleted file mode 100644 index ae334474ef..0000000000 --- a/src/components/views/dialogs/NewSessionReviewDialog.tsx +++ /dev/null @@ -1,120 +0,0 @@ -/* -Copyright 2020-2021 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. -*/ - -import React from 'react'; -import { _t } from '../../../languageHandler'; -import Modal from '../../../Modal'; -import { replaceableComponent } from '../../../utils/replaceableComponent'; -import VerificationRequestDialog from './VerificationRequestDialog'; -import BaseDialog from './BaseDialog'; -import DialogButtons from '../elements/DialogButtons'; -import { MatrixClientPeg } from "../../../MatrixClientPeg"; -import { DeviceInfo } from 'matrix-js-sdk/src/crypto/deviceinfo'; -import ErrorDialog from "./ErrorDialog"; - -interface IProps { - userId: string; - device: DeviceInfo, - onFinished: (boolean) => void; -} - -@replaceableComponent("views.dialogs.NewSessionReviewDialog") -export default class NewSessionReviewDialog extends React.PureComponent { - private onCancelClick = () => { - Modal.createTrackedDialog("Verification failed", "insecure", ErrorDialog, { - headerImage: require("../../../../res/img/e2e/warning.svg"), - title: _t("Your account is not secure"), - description:
- {_t("One of the following may be compromised:")} -
    -
  • {_t("Your password")}
  • -
  • {_t("Your homeserver")}
  • -
  • {_t("This session, or the other session")}
  • -
  • {_t("The internet connection either session is using")}
  • -
-
- {_t("We recommend you change your password and Security Key in Settings immediately")} -
-
, - onFinished: () => this.props.onFinished(false), - }); - } - - private onContinueClick = () => { - const { userId, device } = this.props; - const cli = MatrixClientPeg.get(); - const requestPromise = cli.requestVerification( - userId, - [device.deviceId], - ); - - this.props.onFinished(true); - Modal.createTrackedDialog('New Session Verification', 'Starting dialog', VerificationRequestDialog, { - verificationRequestPromise: requestPromise, - member: cli.getUser(userId), - onFinished: async () => { - const request = await requestPromise; - request.cancel(); - }, - }); - } - - public render() { - const { device } = this.props; - - const icon = ; - const titleText = _t("New session"); - - const title =

- {icon} - {titleText} -

; - - return ( - -
-

{_t( - "Use this session to verify your new one, " + - "granting it access to encrypted messages:", - )}

-
-
- - {device.getDisplayName()} - - ({device.deviceId}) - -
-
-

{_t( - "If you didn’t sign in to this session, " + - "your account may be compromised.", - )}

- -
-
- ); - } -} From c6d1dc7e8e0c194f7d94ce801dc744024e1d38b6 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 1 Jul 2021 15:11:18 +0100 Subject: [PATCH 100/221] lint --- .../structures/auth/SetupEncryptionBody.tsx | 18 +++++++++--------- .../views/toasts/VerificationRequestToast.tsx | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/components/structures/auth/SetupEncryptionBody.tsx b/src/components/structures/auth/SetupEncryptionBody.tsx index 81fee432f6..13790c2e47 100644 --- a/src/components/structures/auth/SetupEncryptionBody.tsx +++ b/src/components/structures/auth/SetupEncryptionBody.tsx @@ -22,7 +22,7 @@ import VerificationRequestDialog from '../../views/dialogs/VerificationRequestDi import { SetupEncryptionStore, Phase } from '../../../stores/SetupEncryptionStore'; import { replaceableComponent } from "../../../utils/replaceableComponent"; import { ISecretStorageKeyInfo } from 'matrix-js-sdk'; -import EncryptionPanel from "../../views/right_panel/EncryptionPanel" +import EncryptionPanel from "../../views/right_panel/EncryptionPanel"; import AccessibleButton from '../../views/elements/AccessibleButton'; import Spinner from '../../views/elements/Spinner'; import { IKeyBackupInfo } from "matrix-js-sdk/src/crypto/keybackup"; @@ -32,7 +32,7 @@ function keyHasPassphrase(keyInfo: ISecretStorageKeyInfo): boolean { return Boolean( keyInfo.passphrase && keyInfo.passphrase.salt && - keyInfo.passphrase.iterations + keyInfo.passphrase.iterations, ); } @@ -85,7 +85,7 @@ export default class SetupEncryptionBody extends React.Component private onUsePassphraseClick = async () => { const store = SetupEncryptionStore.sharedInstance(); store.usePassPhrase(); - } + }; private onVerifyClick = () => { const cli = MatrixClientPeg.get(); @@ -101,31 +101,31 @@ export default class SetupEncryptionBody extends React.Component request.cancel(); }, }); - } + }; private onSkipClick = () => { const store = SetupEncryptionStore.sharedInstance(); store.skip(); - } + }; private onSkipConfirmClick = () => { const store = SetupEncryptionStore.sharedInstance(); store.skipConfirm(); - } + }; private onSkipBackClick = () => { const store = SetupEncryptionStore.sharedInstance(); store.returnAfterSkip(); - } + }; private onDoneClick = () => { const store = SetupEncryptionStore.sharedInstance(); store.done(); - } + }; private onEncryptionPanelClose = () => { this.props.onFinished(false); - } + }; public render() { const { diff --git a/src/components/views/toasts/VerificationRequestToast.tsx b/src/components/views/toasts/VerificationRequestToast.tsx index 14b3c23737..75254d7c62 100644 --- a/src/components/views/toasts/VerificationRequestToast.tsx +++ b/src/components/views/toasts/VerificationRequestToast.tsx @@ -29,7 +29,7 @@ import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/reque import { DeviceInfo } from "matrix-js-sdk/src/crypto/deviceinfo"; import { Action } from "../../../dispatcher/actions"; import { replaceableComponent } from "../../../utils/replaceableComponent"; -import VerificationRequestDialog from "../dialogs/VerificationRequestDialog" +import VerificationRequestDialog from "../dialogs/VerificationRequestDialog"; interface IProps { toastKey: string; From 404aa96f487eb002bfb1e616a257d4e01007cfd2 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 1 Jul 2021 15:13:24 +0100 Subject: [PATCH 101/221] i18n --- src/i18n/strings/en_EN.json | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 6f06e3d6a4..618d5763fa 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2342,15 +2342,6 @@ "Message edits": "Message edits", "Modal Widget": "Modal Widget", "Data on this screen is shared with %(widgetDomain)s": "Data on this screen is shared with %(widgetDomain)s", - "Your account is not secure": "Your account is not secure", - "Your password": "Your password", - "This session, or the other session": "This session, or the other session", - "The internet connection either session is using": "The internet connection either session is using", - "We recommend you change your password and Security Key in Settings immediately": "We recommend you change your password and Security Key in Settings immediately", - "New session": "New session", - "Use this session to verify your new one, granting it access to encrypted messages:": "Use this session to verify your new one, granting it access to encrypted messages:", - "If you didn’t sign in to this session, your account may be compromised.": "If you didn’t sign in to this session, your account may be compromised.", - "This wasn't me": "This wasn't me", "Doesn't look like a valid email address": "Doesn't look like a valid email address", "Continuing without email": "Continuing without email", "Just a heads up, if you don't add an email and forget your password, you could permanently lose access to your account.": "Just a heads up, if you don't add an email and forget your password, you could permanently lose access to your account.", From 5e3c8fae5c9277fa556081d33e1411fa704ec88a Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 1 Jul 2021 15:22:44 +0100 Subject: [PATCH 102/221] put this just on the buttons --- res/css/views/messages/_common_CryptoEvent.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/views/messages/_common_CryptoEvent.scss b/res/css/views/messages/_common_CryptoEvent.scss index d43df1b66c..afaed50fa4 100644 --- a/res/css/views/messages/_common_CryptoEvent.scss +++ b/res/css/views/messages/_common_CryptoEvent.scss @@ -43,12 +43,12 @@ limitations under the License. .mx_cryptoEvent_state, .mx_cryptoEvent_buttons { grid-column: 3; grid-row: 1 / 3; - gap: 5px; } .mx_cryptoEvent_buttons { align-items: center; display: flex; + gap: 5px; } .mx_cryptoEvent_state { From 3cc041a4ae023d91fab5de0407f816cdc82aa082 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 1 Jul 2021 18:53:38 +0200 Subject: [PATCH 103/221] Change displayName of ctrlEnterToSend MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/settings/Settings.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 5ad48c887f..f6c7c04902 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -419,7 +419,7 @@ export const SETTINGS: {[setting: string]: ISetting} = { }, "ctrlFForSearch": { supportedLevels: LEVELS_ACCOUNT_SETTINGS, - displayName: isMac ? _td("Use Command + F to search current room") : _td("Use Ctrl + F to search current room"), + displayName: isMac ? _td("Use Command + F to search timeline") : _td("Use Ctrl + F to search timeline"), default: false, }, "MessageComposerInput.ctrlEnterToSend": { From 725f69a400f9d1bf1255d84e964edd9a7539be21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 1 Jul 2021 19:03:49 +0200 Subject: [PATCH 104/221] Add button to show all keybaord shortcuts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- .../views/settings/tabs/user/PreferencesUserSettingsTab.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx index 8fef57f565..405125bb59 100644 --- a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx @@ -24,6 +24,8 @@ import * as sdk from "../../../../.."; import PlatformPeg from "../../../../../PlatformPeg"; import { SettingLevel } from "../../../../../settings/SettingLevel"; import { replaceableComponent } from "../../../../../utils/replaceableComponent"; +import * as KeyboardShortcuts from "../../../../../accessibility/KeyboardShortcuts"; +import AccessibleButton from "../../../elements/AccessibleButton"; interface IState { autoLaunch: boolean; @@ -232,6 +234,9 @@ export default class PreferencesUserSettingsTab extends React.Component<{}, ISta
{_t("Keyboard shortcuts")} + + { _t("To view all keyboard shortcuts, click here.") } + {this.renderGroup(PreferencesUserSettingsTab.KEYBINDINGS_SETTINGS)}
From 4f70d9021ab0664496fadd0777c172196ee96af6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 1 Jul 2021 19:05:11 +0200 Subject: [PATCH 105/221] i18n MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/i18n/strings/en_EN.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 8536a4dcfe..ef9b5c621f 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -840,8 +840,8 @@ "Enable big emoji in chat": "Enable big emoji in chat", "Send typing notifications": "Send typing notifications", "Show typing notifications": "Show typing notifications", - "Use Command + F to search current room": "Use Command + F to search current room", - "Use Ctrl + F to search current room": "Use Ctrl + F to search current room", + "Use Command + F to search timeline": "Use Command + F to search timeline", + "Use Ctrl + F to search timeline": "Use Ctrl + F to search timeline", "Use Command + Enter to send a message": "Use Command + Enter to send a message", "Use Ctrl + Enter to send a message": "Use Ctrl + Enter to send a message", "Automatically replace plain text Emoji": "Automatically replace plain text Emoji", @@ -1340,6 +1340,7 @@ "Preferences": "Preferences", "Room list": "Room list", "Keyboard shortcuts": "Keyboard shortcuts", + "To view all keyboard shortcuts, click here.": "To view all keyboard shortcuts, click here.", "Displaying time": "Displaying time", "Composer": "Composer", "Code blocks": "Code blocks", From ae16efcf5bdc4ffcdf6f893d94f9142663b2a1b5 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 1 Jul 2021 18:35:38 +0100 Subject: [PATCH 106/221] Remove rateLimitedFunc --- .eslintignore.errorfiles | 1 - src/components/structures/RightPanel.tsx | 8 ++-- src/components/structures/RoomView.tsx | 12 ++--- src/components/views/rooms/AuxPanel.tsx | 6 +-- src/components/views/rooms/MemberList.tsx | 8 ++-- src/components/views/rooms/RoomHeader.js | 7 ++- .../views/rooms/SendMessageComposer.tsx | 8 ++-- src/ratelimitedfunc.js | 47 ------------------- 8 files changed, 24 insertions(+), 73 deletions(-) delete mode 100644 src/ratelimitedfunc.js diff --git a/.eslintignore.errorfiles b/.eslintignore.errorfiles index d9177bebb5..ea66720529 100644 --- a/.eslintignore.errorfiles +++ b/.eslintignore.errorfiles @@ -4,7 +4,6 @@ src/Markdown.js src/NodeAnimator.js src/components/structures/RoomDirectory.js src/components/views/rooms/MemberList.js -src/ratelimitedfunc.js src/utils/DMRoomMap.js src/utils/MultiInviter.js test/components/structures/MessagePanel-test.js diff --git a/src/components/structures/RightPanel.tsx b/src/components/structures/RightPanel.tsx index c608f0eee9..338bfc06ab 100644 --- a/src/components/structures/RightPanel.tsx +++ b/src/components/structures/RightPanel.tsx @@ -23,7 +23,6 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; import dis from '../../dispatcher/dispatcher'; -import RateLimitedFunc from '../../ratelimitedfunc'; import GroupStore from '../../stores/GroupStore'; import { RIGHT_PANEL_PHASES_NO_ARGS, @@ -48,6 +47,7 @@ import FilePanel from "./FilePanel"; import NotificationPanel from "./NotificationPanel"; import ResizeNotifier from "../../utils/ResizeNotifier"; import PinnedMessagesCard from "../views/right_panel/PinnedMessagesCard"; +import { DebouncedFunc, throttle } from 'lodash'; interface IProps { room?: Room; // if showing panels for a given room, this is set @@ -73,7 +73,7 @@ interface IState { export default class RightPanel extends React.Component { static contextType = MatrixClientContext; - private readonly delayedUpdate: RateLimitedFunc; + private readonly delayedUpdate: DebouncedFunc<() => void>; private dispatcherRef: string; constructor(props, context) { @@ -85,9 +85,9 @@ export default class RightPanel extends React.Component { member: this.getUserForPanel(), }; - this.delayedUpdate = new RateLimitedFunc(() => { + this.delayedUpdate = throttle(() => { this.forceUpdate(); - }, 500); + }, 500, { leading: true, trailing: true }); } // Helper function to split out the logic for getPhaseFromProps() and the constructor diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 81000a87a6..d08eaa2ecd 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -37,7 +37,6 @@ import Modal from '../../Modal'; import * as sdk from '../../index'; import CallHandler, { PlaceCallType } from '../../CallHandler'; import dis from '../../dispatcher/dispatcher'; -import rateLimitedFunc from '../../ratelimitedfunc'; import * as Rooms from '../../Rooms'; import eventSearch, { searchPagination } from '../../Searching'; import MainSplit from './MainSplit'; @@ -82,6 +81,7 @@ import { IOpts } from "../../createRoom"; import { replaceableComponent } from "../../utils/replaceableComponent"; import UIStore from "../../stores/UIStore"; import EditorStateTransfer from "../../utils/EditorStateTransfer"; +import { throttle } from "lodash"; const DEBUG = false; let debuglog = function(msg: string) {}; @@ -675,8 +675,8 @@ export default class RoomView extends React.Component { ); } - // cancel any pending calls to the rate_limited_funcs - this.updateRoomMembers.cancelPendingCall(); + // cancel any pending calls to the throttled updated + this.updateRoomMembers.cancel(); for (const watcher of this.settingWatchers) { SettingsStore.unwatchSetting(watcher); @@ -1092,7 +1092,7 @@ export default class RoomView extends React.Component { return; } - this.updateRoomMembers(member); + this.updateRoomMembers(); }; private onMyMembership = (room: Room, membership: string, oldMembership: string) => { @@ -1114,10 +1114,10 @@ export default class RoomView extends React.Component { } // rate limited because a power level change will emit an event for every member in the room. - private updateRoomMembers = rateLimitedFunc(() => { + private updateRoomMembers = throttle(() => { this.updateDMState(); this.updateE2EStatus(this.state.room); - }, 500); + }, 500, { leading: true, trailing: true }); private checkDesktopNotifications() { const memberCount = this.state.room.getJoinedMemberCount() + this.state.room.getInvitedMemberCount(); diff --git a/src/components/views/rooms/AuxPanel.tsx b/src/components/views/rooms/AuxPanel.tsx index 1c817140fa..99286dfa07 100644 --- a/src/components/views/rooms/AuxPanel.tsx +++ b/src/components/views/rooms/AuxPanel.tsx @@ -21,7 +21,6 @@ import { Room } from 'matrix-js-sdk/src/models/room'; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import AppsDrawer from './AppsDrawer'; -import RateLimitedFunc from '../../../ratelimitedfunc'; import SettingsStore from "../../../settings/SettingsStore"; import AutoHideScrollbar from "../../structures/AutoHideScrollbar"; import { UIFeature } from "../../../settings/UIFeature"; @@ -29,6 +28,7 @@ import ResizeNotifier from "../../../utils/ResizeNotifier"; import CallViewForRoom from '../voip/CallViewForRoom'; import { objectHasDiff } from "../../../utils/objects"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { throttle } from 'lodash'; interface IProps { // js-sdk room object @@ -99,9 +99,9 @@ export default class AuxPanel extends React.Component { } } - private rateLimitedUpdate = new RateLimitedFunc(() => { + private rateLimitedUpdate = throttle(() => { this.setState({ counters: this.computeCounters() }); - }, 500); + }, 500, { leading: true, trailing: true }); private computeCounters() { const counters = []; diff --git a/src/components/views/rooms/MemberList.tsx b/src/components/views/rooms/MemberList.tsx index 68f87580df..f4df70c7ee 100644 --- a/src/components/views/rooms/MemberList.tsx +++ b/src/components/views/rooms/MemberList.tsx @@ -22,7 +22,6 @@ import { _t } from '../../../languageHandler'; import SdkConfig from '../../../SdkConfig'; import dis from '../../../dispatcher/dispatcher'; import { isValid3pidInvite } from "../../../RoomInvite"; -import rateLimitedFunction from "../../../ratelimitedfunc"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore"; import BaseCard from "../right_panel/BaseCard"; @@ -43,6 +42,7 @@ import AccessibleButton from '../elements/AccessibleButton'; import EntityTile from "./EntityTile"; import MemberTile from "./MemberTile"; import BaseAvatar from '../avatars/BaseAvatar'; +import { throttle } from 'lodash'; const INITIAL_LOAD_NUM_MEMBERS = 30; const INITIAL_LOAD_NUM_INVITED = 5; @@ -133,7 +133,7 @@ export default class MemberList extends React.Component { } // cancel any pending calls to the rate_limited_funcs - this.updateList.cancelPendingCall(); + this.updateList.cancel(); } /** @@ -237,9 +237,9 @@ export default class MemberList extends React.Component { if (this.canInvite !== this.state.canInvite) this.setState({ canInvite: this.canInvite }); }; - private updateList = rateLimitedFunction(() => { + private updateList = throttle(() => { this.updateListNow(); - }, 500); + }, 500, { leading: true, trailing: true }); private updateListNow(): void { const members = this.roomMembers(); diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js index 886317f2bf..b05b709e36 100644 --- a/src/components/views/rooms/RoomHeader.js +++ b/src/components/views/rooms/RoomHeader.js @@ -20,7 +20,6 @@ import PropTypes from 'prop-types'; import classNames from 'classnames'; import { _t } from '../../../languageHandler'; import { MatrixClientPeg } from '../../../MatrixClientPeg'; -import RateLimitedFunc from '../../../ratelimitedfunc'; import SettingsStore from "../../../settings/SettingsStore"; import RoomHeaderButtons from '../right_panel/RoomHeaderButtons'; @@ -31,6 +30,7 @@ import RoomTopic from "../elements/RoomTopic"; import RoomName from "../elements/RoomName"; import { PlaceCallType } from "../../../CallHandler"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { throttle } from 'lodash'; @replaceableComponent("views.rooms.RoomHeader") export default class RoomHeader extends React.Component { @@ -73,10 +73,9 @@ export default class RoomHeader extends React.Component { this._rateLimitedUpdate(); }; - _rateLimitedUpdate = new RateLimitedFunc(function() { - /* eslint-disable @babel/no-invalid-this */ + _rateLimitedUpdate = throttle(() => { this.forceUpdate(); - }, 500); + }, 500, { leading: true, trailing: true }); render() { let searchStatus = null; diff --git a/src/components/views/rooms/SendMessageComposer.tsx b/src/components/views/rooms/SendMessageComposer.tsx index f088ec0c8e..ec190c829a 100644 --- a/src/components/views/rooms/SendMessageComposer.tsx +++ b/src/components/views/rooms/SendMessageComposer.tsx @@ -39,7 +39,6 @@ import Modal from '../../../Modal'; import { _t, _td } from '../../../languageHandler'; import ContentMessages from '../../../ContentMessages'; import MatrixClientContext from "../../../contexts/MatrixClientContext"; -import RateLimitedFunc from '../../../ratelimitedfunc'; import { Action } from "../../../dispatcher/actions"; import { containsEmoji } from "../../../effects/utils"; import { CHAT_EFFECTS } from '../../../effects'; @@ -53,6 +52,7 @@ import { Room } from 'matrix-js-sdk/src/models/room'; import ErrorDialog from "../dialogs/ErrorDialog"; import QuestionDialog from "../dialogs/QuestionDialog"; import { ActionPayload } from "../../../dispatcher/payloads"; +import { DebouncedFunc, throttle } from 'lodash'; function addReplyToMessageContent( content: IContent, @@ -138,7 +138,7 @@ export default class SendMessageComposer extends React.Component { static contextType = MatrixClientContext; context!: React.ContextType; - private readonly prepareToEncrypt?: RateLimitedFunc; + private readonly prepareToEncrypt?: DebouncedFunc<() => void>; private readonly editorRef = createRef(); private model: EditorModel = null; private currentlyComposedEditorState: SerializedPart[] = null; @@ -149,9 +149,9 @@ export default class SendMessageComposer extends React.Component { super(props); this.context = context; // otherwise React will only set it prior to render due to type def above if (this.context.isCryptoEnabled() && this.context.isRoomEncrypted(this.props.room.roomId)) { - this.prepareToEncrypt = new RateLimitedFunc(() => { + this.prepareToEncrypt = throttle(() => { this.context.prepareToEncrypt(this.props.room); - }, 60000); + }, 60000, { leading: true, trailing: false }); } window.addEventListener("beforeunload", this.saveStoredEditorState); diff --git a/src/ratelimitedfunc.js b/src/ratelimitedfunc.js deleted file mode 100644 index 3df3db615e..0000000000 --- a/src/ratelimitedfunc.js +++ /dev/null @@ -1,47 +0,0 @@ -/* -Copyright 2016 OpenMarket Ltd - -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. -*/ - -/** - * 'debounces' a function to only execute every n milliseconds. - * Useful when react-sdk gets many, many events but only wants - * to update the interface once for all of them. - * - * Note that the function must not take arguments, since the args - * could be different for each invocation of the function. - * - * The returned function has a 'cancelPendingCall' property which can be called - * on unmount or similar to cancel any pending update. - */ - -import {throttle} from "lodash"; - -export default function ratelimitedfunc(fn, time) { - const throttledFn = throttle(fn, time, { - leading: true, - trailing: true, - }); - const _bind = throttledFn.bind; - throttledFn.bind = function() { - const boundFn = _bind.apply(throttledFn, arguments); - boundFn.cancelPendingCall = throttledFn.cancelPendingCall; - return boundFn; - }; - - throttledFn.cancelPendingCall = function() { - throttledFn.cancel(); - }; - return throttledFn; -} From 9ea4ca39c63e7f4f98fee429b9681f78edc5fc6d Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 1 Jul 2021 18:54:51 +0100 Subject: [PATCH 107/221] Be more like other arrow functions --- src/components/structures/RightPanel.tsx | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/components/structures/RightPanel.tsx b/src/components/structures/RightPanel.tsx index 338bfc06ab..63027ab627 100644 --- a/src/components/structures/RightPanel.tsx +++ b/src/components/structures/RightPanel.tsx @@ -47,7 +47,7 @@ import FilePanel from "./FilePanel"; import NotificationPanel from "./NotificationPanel"; import ResizeNotifier from "../../utils/ResizeNotifier"; import PinnedMessagesCard from "../views/right_panel/PinnedMessagesCard"; -import { DebouncedFunc, throttle } from 'lodash'; +import { throttle } from 'lodash'; interface IProps { room?: Room; // if showing panels for a given room, this is set @@ -73,7 +73,6 @@ interface IState { export default class RightPanel extends React.Component { static contextType = MatrixClientContext; - private readonly delayedUpdate: DebouncedFunc<() => void>; private dispatcherRef: string; constructor(props, context) { @@ -84,12 +83,12 @@ export default class RightPanel extends React.Component { isUserPrivilegedInGroup: null, member: this.getUserForPanel(), }; - - this.delayedUpdate = throttle(() => { - this.forceUpdate(); - }, 500, { leading: true, trailing: true }); } + private readonly delayedUpdate = throttle((): void => { + this.forceUpdate(); + }, 500, { leading: true, trailing: true }); + // Helper function to split out the logic for getPhaseFromProps() and the constructor // as both are called at the same time in the constructor. private getUserForPanel() { From 8bbb768825ab1154e0685b3557cebe35d623eae6 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 1 Jul 2021 19:54:05 +0100 Subject: [PATCH 108/221] Convert RoomHeader to TS Also add a type of OOBData... possibly this should be in the js-sdk, but threepid invites themselves have a type in react-sdk so it can live alongside it I guess (also I'm not entirely sure we actually use it anymore...) --- src/components/structures/LoggedInView.tsx | 4 +- src/components/structures/RoomView.tsx | 26 +-------- .../views/avatars/DecoratedRoomAvatar.tsx | 3 +- src/components/views/avatars/RoomAvatar.tsx | 4 +- .../rooms/{RoomHeader.js => RoomHeader.tsx} | 58 +++++++++++-------- src/stores/ThreepidInviteStore.ts | 10 ++++ 6 files changed, 52 insertions(+), 53 deletions(-) rename src/components/views/rooms/{RoomHeader.js => RoomHeader.tsx} (86%) diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx index 26bb0fe24a..65b26cb505 100644 --- a/src/components/structures/LoggedInView.tsx +++ b/src/components/structures/LoggedInView.tsx @@ -48,7 +48,7 @@ import { ViewRoomDeltaPayload } from "../../dispatcher/payloads/ViewRoomDeltaPay import RoomListStore from "../../stores/room-list/RoomListStore"; import NonUrgentToastContainer from "./NonUrgentToastContainer"; import { ToggleRightPanelPayload } from "../../dispatcher/payloads/ToggleRightPanelPayload"; -import { IThreepidInvite } from "../../stores/ThreepidInviteStore"; +import { IOOBData, IThreepidInvite } from "../../stores/ThreepidInviteStore"; import Modal from "../../Modal"; import { ICollapseConfig } from "../../resizer/distributors/collapse"; import HostSignupContainer from '../views/host_signup/HostSignupContainer'; @@ -81,7 +81,7 @@ interface IProps { page_type: string; autoJoin: boolean; threepidInvite?: IThreepidInvite; - roomOobData?: object; + roomOobData?: IOOBData; currentRoomId: string; collapseLhs: boolean; config: { diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index d08eaa2ecd..a8eb8d662a 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -63,7 +63,7 @@ import RoomUpgradeWarningBar from "../views/rooms/RoomUpgradeWarningBar"; import AuxPanel from "../views/rooms/AuxPanel"; import RoomHeader from "../views/rooms/RoomHeader"; import { XOR } from "../../@types/common"; -import { IThreepidInvite } from "../../stores/ThreepidInviteStore"; +import { IOOBData, IThreepidInvite } from "../../stores/ThreepidInviteStore"; import EffectsOverlay from "../views/elements/EffectsOverlay"; import { containsEmoji } from '../../effects/utils'; import { CHAT_EFFECTS } from '../../effects'; @@ -95,21 +95,7 @@ if (DEBUG) { interface IProps { threepidInvite: IThreepidInvite, - - // Any data about the room that would normally come from the homeserver - // but has been passed out-of-band, eg. the room name and avatar URL - // from an email invite (a workaround for the fact that we can't - // get this information from the HS using an email invite). - // Fields: - // * name (string) The room's name - // * avatarUrl (string) The mxc:// avatar URL for the room - // * inviterName (string) The display name of the person who - // * invited us to the room - oobData?: { - name?: string; - avatarUrl?: string; - inviterName?: string; - }; + oobData?: IOOBData; resizeNotifier: ResizeNotifier; justCreatedOpts?: IOpts; @@ -1460,13 +1446,6 @@ export default class RoomView extends React.Component { }); }; - private onLeaveClick = () => { - dis.dispatch({ - action: 'leave_room', - room_id: this.state.room.roomId, - }); - }; - private onForgetClick = () => { dis.dispatch({ action: 'forget_room', @@ -2106,7 +2085,6 @@ export default class RoomView extends React.Component { onSearchClick={this.onSearchClick} onSettingsClick={this.onSettingsClick} onForgetClick={(myMembership === "leave") ? this.onForgetClick : null} - onLeaveClick={(myMembership === "join") ? this.onLeaveClick : null} e2eStatus={this.state.e2eStatus} onAppsClick={this.state.hasPinnedWidgets ? this.onAppsClick : null} appsShown={this.state.showApps} diff --git a/src/components/views/avatars/DecoratedRoomAvatar.tsx b/src/components/views/avatars/DecoratedRoomAvatar.tsx index 950caefa02..5e6bf45f07 100644 --- a/src/components/views/avatars/DecoratedRoomAvatar.tsx +++ b/src/components/views/avatars/DecoratedRoomAvatar.tsx @@ -30,13 +30,14 @@ import { _t } from "../../../languageHandler"; import TextWithTooltip from "../elements/TextWithTooltip"; import DMRoomMap from "../../../utils/DMRoomMap"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { IOOBData } from "../../../stores/ThreepidInviteStore"; interface IProps { room: Room; avatarSize: number; displayBadge?: boolean; forceCount?: boolean; - oobData?: object; + oobData?: IOOBData; viewAvatarOnClick?: boolean; } diff --git a/src/components/views/avatars/RoomAvatar.tsx b/src/components/views/avatars/RoomAvatar.tsx index c3f49d4a12..8ac8de8233 100644 --- a/src/components/views/avatars/RoomAvatar.tsx +++ b/src/components/views/avatars/RoomAvatar.tsx @@ -24,14 +24,14 @@ import Modal from '../../../Modal'; import * as Avatar from '../../../Avatar'; import { replaceableComponent } from "../../../utils/replaceableComponent"; import { mediaFromMxc } from "../../../customisations/Media"; +import { IOOBData } from '../../../stores/ThreepidInviteStore'; interface IProps extends Omit, "name" | "idName" | "url" | "onClick"> { // Room may be left unset here, but if it is, // oobData.avatarUrl should be set (else there // would be nowhere to get the avatar from) room?: Room; - // TODO: type when js-sdk has types - oobData?: any; + oobData?: IOOBData; width?: number; height?: number; resizeMethod?: ResizeMethod; diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.tsx similarity index 86% rename from src/components/views/rooms/RoomHeader.js rename to src/components/views/rooms/RoomHeader.tsx index b05b709e36..c20b68cb9b 100644 --- a/src/components/views/rooms/RoomHeader.js +++ b/src/components/views/rooms/RoomHeader.tsx @@ -1,6 +1,6 @@ /* Copyright 2015, 2016 OpenMarket Ltd -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2019, 2021 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. @@ -16,7 +16,6 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import classNames from 'classnames'; import { _t } from '../../../languageHandler'; import { MatrixClientPeg } from '../../../MatrixClientPeg'; @@ -31,53 +30,64 @@ import RoomName from "../elements/RoomName"; import { PlaceCallType } from "../../../CallHandler"; import { replaceableComponent } from "../../../utils/replaceableComponent"; import { throttle } from 'lodash'; +import { Room } from '../../../../../matrix-js-sdk/src'; +import { E2EStatus } from '../../../utils/ShieldUtils'; +import { IOOBData } from '../../../stores/ThreepidInviteStore'; +import { SearchScope } from './SearchBar'; + +export interface ISearchInfo { + searchTerm: string; + searchScope: SearchScope; + searchCount: number; +} + +interface IProps { + room: Room, + oobData?: IOOBData, + inRoom: boolean, + onSettingsClick: () => void; + onSearchClick: () => void; + onForgetClick: () => void; + onCallPlaced: (PlaceCallType) => void; + onAppsClick: () => void; + e2eStatus: E2EStatus; + appsShown: boolean; + searchInfo: ISearchInfo; +} @replaceableComponent("views.rooms.RoomHeader") -export default class RoomHeader extends React.Component { - static propTypes = { - room: PropTypes.object, - oobData: PropTypes.object, - inRoom: PropTypes.bool, - onSettingsClick: PropTypes.func, - onSearchClick: PropTypes.func, - onLeaveClick: PropTypes.func, - e2eStatus: PropTypes.string, - onAppsClick: PropTypes.func, - appsShown: PropTypes.bool, - onCallPlaced: PropTypes.func, // (PlaceCallType) => void; - }; - +export default class RoomHeader extends React.Component { static defaultProps = { editing: false, inRoom: false, }; - componentDidMount() { + public componentDidMount() { const cli = MatrixClientPeg.get(); - cli.on("RoomState.events", this._onRoomStateEvents); + cli.on("RoomState.events", this.onRoomStateEvents); } - componentWillUnmount() { + public componentWillUnmount() { const cli = MatrixClientPeg.get(); if (cli) { - cli.removeListener("RoomState.events", this._onRoomStateEvents); + cli.removeListener("RoomState.events", this.onRoomStateEvents); } } - _onRoomStateEvents = (event, state) => { + private onRoomStateEvents = (event, state) => { if (!this.props.room || event.getRoomId() !== this.props.room.roomId) { return; } // redisplay the room name, topic, etc. - this._rateLimitedUpdate(); + this.rateLimitedUpdate(); }; - _rateLimitedUpdate = throttle(() => { + private rateLimitedUpdate = throttle(() => { this.forceUpdate(); }, 500, { leading: true, trailing: true }); - render() { + public render() { let searchStatus = null; // don't display the search count until the search completes and diff --git a/src/stores/ThreepidInviteStore.ts b/src/stores/ThreepidInviteStore.ts index 74a5f5f8ec..d0cf40941c 100644 --- a/src/stores/ThreepidInviteStore.ts +++ b/src/stores/ThreepidInviteStore.ts @@ -45,6 +45,16 @@ export interface IThreepidInvite { inviterName: string; } +// Any data about the room that would normally come from the homeserver +// but has been passed out-of-band, eg. the room name and avatar URL +// from an email invite (a workaround for the fact that we can't +// get this information from the HS using an email invite). +export interface IOOBData { + name?: string; // The room's name + avatarUrl?: string; // The mxc:// avatar URL for the room + inviterName?: string; // The display name of the person who invited us to the room +} + const STORAGE_PREFIX = "mx_threepid_invite_"; export default class ThreepidInviteStore extends EventEmitter { From e121e5bf8a6ebf3fc7a8d75ababfb54a0ffb8e6a Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 1 Jul 2021 19:58:25 +0100 Subject: [PATCH 109/221] nice try, vscode --- src/components/views/rooms/RoomHeader.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomHeader.tsx b/src/components/views/rooms/RoomHeader.tsx index c20b68cb9b..48b8951c81 100644 --- a/src/components/views/rooms/RoomHeader.tsx +++ b/src/components/views/rooms/RoomHeader.tsx @@ -30,7 +30,7 @@ import RoomName from "../elements/RoomName"; import { PlaceCallType } from "../../../CallHandler"; import { replaceableComponent } from "../../../utils/replaceableComponent"; import { throttle } from 'lodash'; -import { Room } from '../../../../../matrix-js-sdk/src'; +import { Room } from 'matrix-js-sdk/src'; import { E2EStatus } from '../../../utils/ShieldUtils'; import { IOOBData } from '../../../stores/ThreepidInviteStore'; import { SearchScope } from './SearchBar'; From e6b754f8dd511d6f0bd77357e23121d6c3c5f04c Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 1 Jul 2021 20:33:25 +0100 Subject: [PATCH 110/221] Convert Markdown to TypeScript --- src/{Markdown.js => Markdown.ts} | 57 +++++++++++++++++--------------- 1 file changed, 30 insertions(+), 27 deletions(-) rename src/{Markdown.js => Markdown.ts} (85%) diff --git a/src/Markdown.js b/src/Markdown.ts similarity index 85% rename from src/Markdown.js rename to src/Markdown.ts index f670bded12..64037369db 100644 --- a/src/Markdown.js +++ b/src/Markdown.ts @@ -1,5 +1,6 @@ /* Copyright 2016 OpenMarket Ltd +Copyright 2021 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. @@ -15,16 +16,16 @@ limitations under the License. */ import * as commonmark from 'commonmark'; -import {escape} from "lodash"; +import { escape } from "lodash"; const ALLOWED_HTML_TAGS = ['sub', 'sup', 'del', 'u']; // These types of node are definitely text const TEXT_NODES = ['text', 'softbreak', 'linebreak', 'paragraph', 'document']; -function is_allowed_html_tag(node) { +function isAllowedHtmlTag(node: any) { if (node.literal != null && - node.literal.match('^<((div|span) data-mx-maths="[^"]*"|\/(div|span))>$') != null) { + node.literal.match('^<((div|span) data-mx-maths="[^"]*"|/(div|span))>$') != null) { return true; } @@ -39,21 +40,12 @@ function is_allowed_html_tag(node) { return false; } -function html_if_tag_allowed(node) { - if (is_allowed_html_tag(node)) { - this.lit(node.literal); - return; - } else { - this.lit(escape(node.literal)); - } -} - /* * Returns true if the parse output containing the node * comprises multiple block level elements (ie. lines), * or false if it is only a single line. */ -function is_multi_line(node) { +function isMultiLine(node) { let par = node; while (par.parent) { par = par.parent; @@ -65,8 +57,13 @@ function is_multi_line(node) { * Class that wraps commonmark, adding the ability to see whether * a given message actually uses any markdown syntax or whether * it's plain text. + * + * Types are a bit of a struggle here as commonmark doesn't have types */ export default class Markdown { + private input: string; + private parsed: any; + constructor(input) { this.input = input; @@ -87,7 +84,7 @@ export default class Markdown { // if it's an allowed html tag, we need to render it and therefore // we will need to use HTML. If it's not allowed, it's not HTML since // we'll just be treating it as text. - if (is_allowed_html_tag(node)) { + if (isAllowedHtmlTag(node)) { return false; } } else { @@ -118,7 +115,7 @@ export default class Markdown { // // Let's try sending with

s anyway for now, though. - const real_paragraph = renderer.paragraph; + const realParagraph = renderer.paragraph; renderer.paragraph = function(node, entering) { // If there is only one top level node, just return the @@ -126,8 +123,8 @@ export default class Markdown { // 'inline', rather than unnecessarily wrapped in its own // p tag. If, however, we have multiple nodes, each gets // its own p tag to keep them as separate paragraphs. - if (is_multi_line(node)) { - real_paragraph.call(this, node, entering); + if (isMultiLine(node)) { + realParagraph.call(this, node, entering); } }; @@ -150,19 +147,26 @@ export default class Markdown { } }; - renderer.html_inline = html_if_tag_allowed; + renderer.html_inline = function(node: any) { + if (isAllowedHtmlTag(node)) { + this.lit(node.literal); + return; + } else { + this.lit(escape(node.literal)); + } + }; renderer.html_block = function(node) { -/* + /* // as with `paragraph`, we only insert line breaks // if there are multiple lines in the markdown. const isMultiLine = is_multi_line(node); if (isMultiLine) this.cr(); -*/ - html_if_tag_allowed.call(this, node); -/* + */ + renderer.html_inline(node); + /* if (isMultiLine) this.cr(); -*/ + */ }; return renderer.render(this.parsed); @@ -178,13 +182,12 @@ export default class Markdown { * which has no formatting. Otherwise it emits HTML(!). */ toPlaintext() { - const renderer = new commonmark.HtmlRenderer({safe: false}); - const real_paragraph = renderer.paragraph; + const renderer = new commonmark.HtmlRenderer({ safe: false }); renderer.paragraph = function(node, entering) { // as with toHTML, only append lines to paragraphs if there are // multiple paragraphs - if (is_multi_line(node)) { + if (isMultiLine(node)) { if (!entering && node.next) { this.lit('\n\n'); } @@ -193,7 +196,7 @@ export default class Markdown { renderer.html_block = function(node) { this.lit(node.literal); - if (is_multi_line(node) && node.next) this.lit('\n\n'); + if (isMultiLine(node) && node.next) this.lit('\n\n'); }; return renderer.render(this.parsed); From ee9be5438ebc094edee0055151d1e70feb095890 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 1 Jul 2021 20:43:05 +0100 Subject: [PATCH 111/221] eslint --fix --- .eslintignore.errorfiles | 15 - package.json | 2 +- scripts/generate-eslint-error-ignore-file | 23 - .../structures/MessagePanel-test.js | 5 +- .../dialogs/InteractiveAuthDialog-test.js | 12 +- test/mock-clock.js | 588 +++++++++--------- 6 files changed, 301 insertions(+), 344 deletions(-) delete mode 100644 .eslintignore.errorfiles delete mode 100755 scripts/generate-eslint-error-ignore-file diff --git a/.eslintignore.errorfiles b/.eslintignore.errorfiles deleted file mode 100644 index ea66720529..0000000000 --- a/.eslintignore.errorfiles +++ /dev/null @@ -1,15 +0,0 @@ -# autogenerated file: run scripts/generate-eslint-error-ignore-file to update. - -src/Markdown.js -src/NodeAnimator.js -src/components/structures/RoomDirectory.js -src/components/views/rooms/MemberList.js -src/utils/DMRoomMap.js -src/utils/MultiInviter.js -test/components/structures/MessagePanel-test.js -test/components/views/dialogs/InteractiveAuthDialog-test.js -test/mock-clock.js -src/component-index.js -test/end-to-end-tests/node_modules/ -test/end-to-end-tests/element/ -test/end-to-end-tests/synapse/ diff --git a/package.json b/package.json index 4ad585ba7d..bd997f8559 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "start:all": "concurrently --kill-others-on-fail --prefix \"{time} [{name}]\" -n build,reskindex \"yarn start:build\" \"yarn reskindex:watch\"", "start:build": "babel src -w -s -d lib --verbose --extensions \".ts,.js\"", "lint": "yarn lint:types && yarn lint:js && yarn lint:style", - "lint:js": "eslint --max-warnings 0 --ignore-path .eslintignore.errorfiles src test", + "lint:js": "eslint --max-warnings 0 src test", "lint:types": "tsc --noEmit --jsx react", "lint:style": "stylelint 'res/css/**/*.scss'", "test": "jest", diff --git a/scripts/generate-eslint-error-ignore-file b/scripts/generate-eslint-error-ignore-file deleted file mode 100755 index 54aacfc9fa..0000000000 --- a/scripts/generate-eslint-error-ignore-file +++ /dev/null @@ -1,23 +0,0 @@ -#!/bin/sh -# -# generates .eslintignore.errorfiles to list the files which have errors in, -# so that they can be ignored in future automated linting. - -out=.eslintignore.errorfiles - -cd `dirname $0`/.. - -echo "generating $out" - -{ - cat < 0) | .filePath' | - sed -e 's/.*matrix-react-sdk\///'; -} > "$out" -# also append rules from eslintignore file -cat .eslintignore >> $out diff --git a/test/components/structures/MessagePanel-test.js b/test/components/structures/MessagePanel-test.js index d32970a278..d836de0785 100644 --- a/test/components/structures/MessagePanel-test.js +++ b/test/components/structures/MessagePanel-test.js @@ -26,7 +26,7 @@ import { EventEmitter } from "events"; import sdk from '../../skinned-sdk'; const MessagePanel = sdk.getComponent('structures.MessagePanel'); -import {MatrixClientPeg} from '../../../src/MatrixClientPeg'; +import { MatrixClientPeg } from '../../../src/MatrixClientPeg'; import Matrix from 'matrix-js-sdk'; const test_utils = require('../../test-utils'); @@ -79,7 +79,7 @@ describe('MessagePanel', function() { beforeEach(function() { test_utils.stubClient(); client = MatrixClientPeg.get(); - client.credentials = {userId: '@me:here'}; + client.credentials = { userId: '@me:here' }; // HACK: We assume all settings want to be disabled SettingsStore.getValue = jest.fn((arg) => { @@ -120,7 +120,6 @@ describe('MessagePanel', function() { return events; } - // make a collection of events with some member events that should be collapsed // with a MemberEventListSummary function mkMelsEvents() { diff --git a/test/components/views/dialogs/InteractiveAuthDialog-test.js b/test/components/views/dialogs/InteractiveAuthDialog-test.js index fa44fc8d92..c75b2fdcc8 100644 --- a/test/components/views/dialogs/InteractiveAuthDialog-test.js +++ b/test/components/views/dialogs/InteractiveAuthDialog-test.js @@ -20,10 +20,10 @@ import ReactTestUtils from 'react-dom/test-utils'; import MatrixReactTestUtils from 'matrix-react-test-utils'; import sdk from '../../../skinned-sdk'; -import {MatrixClientPeg} from '../../../../src/MatrixClientPeg'; +import { MatrixClientPeg } from '../../../../src/MatrixClientPeg'; import * as test_utils from '../../../test-utils'; -import {sleep} from "../../../../src/utils/promise"; +import { sleep } from "../../../../src/utils/promise"; const InteractiveAuthDialog = sdk.getComponent( 'views.dialogs.InteractiveAuthDialog', @@ -45,11 +45,11 @@ describe('InteractiveAuthDialog', function() { it('Should successfully complete a password flow', function() { const onFinished = jest.fn(); - const doRequest = jest.fn().mockResolvedValue({a: 1}); + const doRequest = jest.fn().mockResolvedValue({ a: 1 }); // tell the stub matrixclient to return a real userid const client = MatrixClientPeg.get(); - client.credentials = {userId: "@user:id"}; + client.credentials = { userId: "@user:id" }; const dlg = ReactDOM.render( { expect(onFinished).toBeCalledTimes(1); - expect(onFinished).toBeCalledWith(true, {a: 1}); + expect(onFinished).toBeCalledWith(true, { a: 1 }); }); }); }); diff --git a/test/mock-clock.js b/test/mock-clock.js index 1a4d6086de..4306433b6f 100644 --- a/test/mock-clock.js +++ b/test/mock-clock.js @@ -47,369 +47,366 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. const j$ = {}; j$.Clock = function() { - function Clock(global, delayedFunctionSchedulerFactory, mockDate) { - let self = this, - realTimingFunctions = { + function Clock(global, delayedFunctionSchedulerFactory, mockDate) { + const self = this; + const realTimingFunctions = { setTimeout: global.setTimeout, clearTimeout: global.clearTimeout, setInterval: global.setInterval, clearInterval: global.clearInterval, - }, - fakeTimingFunctions = { + }; + const fakeTimingFunctions = { setTimeout: setTimeout, clearTimeout: clearTimeout, setInterval: setInterval, clearInterval: clearInterval, - }, - installed = false, - delayedFunctionScheduler, - timer; + }; + let installed = false; + let delayedFunctionScheduler; + let timer; + self.install = function() { + if (!originalTimingFunctionsIntact()) { + throw new Error('Jasmine Clock was unable to install over custom global timer functions. Is the clock already installed?'); + } + replace(global, fakeTimingFunctions); + timer = fakeTimingFunctions; + delayedFunctionScheduler = delayedFunctionSchedulerFactory(); + installed = true; - self.install = function() { - if(!originalTimingFunctionsIntact()) { - throw new Error('Jasmine Clock was unable to install over custom global timer functions. Is the clock already installed?'); - } - replace(global, fakeTimingFunctions); - timer = fakeTimingFunctions; - delayedFunctionScheduler = delayedFunctionSchedulerFactory(); - installed = true; + return self; + }; - return self; - }; + self.uninstall = function() { + delayedFunctionScheduler = null; + mockDate.uninstall(); + replace(global, realTimingFunctions); - self.uninstall = function() { - delayedFunctionScheduler = null; - mockDate.uninstall(); - replace(global, realTimingFunctions); + timer = realTimingFunctions; + installed = false; + }; - timer = realTimingFunctions; - installed = false; - }; + self.withMock = function(closure) { + this.install(); + try { + closure(); + } finally { + this.uninstall(); + } + }; - self.withMock = function(closure) { - this.install(); - try { - closure(); - } finally { - this.uninstall(); - } - }; + self.mockDate = function(initialDate) { + mockDate.install(initialDate); + }; - self.mockDate = function(initialDate) { - mockDate.install(initialDate); - }; + self.setTimeout = function(fn, delay, params) { + if (legacyIE()) { + if (arguments.length > 2) { + throw new Error('IE < 9 cannot support extra params to setTimeout without a polyfill'); + } + return timer.setTimeout(fn, delay); + } + return Function.prototype.apply.apply(timer.setTimeout, [global, arguments]); + }; - self.setTimeout = function(fn, delay, params) { - if (legacyIE()) { - if (arguments.length > 2) { - throw new Error('IE < 9 cannot support extra params to setTimeout without a polyfill'); - } - return timer.setTimeout(fn, delay); - } - return Function.prototype.apply.apply(timer.setTimeout, [global, arguments]); - }; + self.setInterval = function(fn, delay, params) { + if (legacyIE()) { + if (arguments.length > 2) { + throw new Error('IE < 9 cannot support extra params to setInterval without a polyfill'); + } + return timer.setInterval(fn, delay); + } + return Function.prototype.apply.apply(timer.setInterval, [global, arguments]); + }; - self.setInterval = function(fn, delay, params) { - if (legacyIE()) { - if (arguments.length > 2) { - throw new Error('IE < 9 cannot support extra params to setInterval without a polyfill'); - } - return timer.setInterval(fn, delay); - } - return Function.prototype.apply.apply(timer.setInterval, [global, arguments]); - }; + self.clearTimeout = function(id) { + return Function.prototype.call.apply(timer.clearTimeout, [global, id]); + }; - self.clearTimeout = function(id) { - return Function.prototype.call.apply(timer.clearTimeout, [global, id]); - }; + self.clearInterval = function(id) { + return Function.prototype.call.apply(timer.clearInterval, [global, id]); + }; - self.clearInterval = function(id) { - return Function.prototype.call.apply(timer.clearInterval, [global, id]); - }; + self.tick = function(millis) { + if (installed) { + mockDate.tick(millis); + delayedFunctionScheduler.tick(millis); + } else { + throw new Error('Mock clock is not installed, use jasmine.clock().install()'); + } + }; - self.tick = function(millis) { - if (installed) { - mockDate.tick(millis); - delayedFunctionScheduler.tick(millis); - } else { - throw new Error('Mock clock is not installed, use jasmine.clock().install()'); - } - }; + return self; - return self; - - function originalTimingFunctionsIntact() { - return global.setTimeout === realTimingFunctions.setTimeout && + function originalTimingFunctionsIntact() { + return global.setTimeout === realTimingFunctions.setTimeout && global.clearTimeout === realTimingFunctions.clearTimeout && global.setInterval === realTimingFunctions.setInterval && global.clearInterval === realTimingFunctions.clearInterval; + } + + function legacyIE() { + //if these methods are polyfilled, apply will be present + return !(realTimingFunctions.setTimeout || realTimingFunctions.setInterval).apply; + } + + function replace(dest, source) { + for (const prop in source) { + dest[prop] = source[prop]; + } + } + + function setTimeout(fn, delay) { + return delayedFunctionScheduler.scheduleFunction(fn, delay, argSlice(arguments, 2)); + } + + function clearTimeout(id) { + return delayedFunctionScheduler.removeFunctionWithId(id); + } + + function setInterval(fn, interval) { + return delayedFunctionScheduler.scheduleFunction(fn, interval, argSlice(arguments, 2), true); + } + + function clearInterval(id) { + return delayedFunctionScheduler.removeFunctionWithId(id); + } + + function argSlice(argsObj, n) { + return Array.prototype.slice.call(argsObj, n); + } } - function legacyIE() { - //if these methods are polyfilled, apply will be present - return !(realTimingFunctions.setTimeout || realTimingFunctions.setInterval).apply; - } - - function replace(dest, source) { - for (const prop in source) { - dest[prop] = source[prop]; - } - } - - function setTimeout(fn, delay) { - return delayedFunctionScheduler.scheduleFunction(fn, delay, argSlice(arguments, 2)); - } - - function clearTimeout(id) { - return delayedFunctionScheduler.removeFunctionWithId(id); - } - - function setInterval(fn, interval) { - return delayedFunctionScheduler.scheduleFunction(fn, interval, argSlice(arguments, 2), true); - } - - function clearInterval(id) { - return delayedFunctionScheduler.removeFunctionWithId(id); - } - - function argSlice(argsObj, n) { - return Array.prototype.slice.call(argsObj, n); - } - } - - return Clock; + return Clock; }(); - j$.DelayedFunctionScheduler = function() { - function DelayedFunctionScheduler() { - const self = this; - const scheduledLookup = []; - const scheduledFunctions = {}; - let currentTime = 0; - let delayedFnCount = 0; + function DelayedFunctionScheduler() { + const self = this; + const scheduledLookup = []; + const scheduledFunctions = {}; + let currentTime = 0; + let delayedFnCount = 0; - self.tick = function(millis) { - millis = millis || 0; - const endTime = currentTime + millis; + self.tick = function(millis) { + millis = millis || 0; + const endTime = currentTime + millis; - runScheduledFunctions(endTime); - currentTime = endTime; - }; + runScheduledFunctions(endTime); + currentTime = endTime; + }; - self.scheduleFunction = function(funcToCall, millis, params, recurring, timeoutKey, runAtMillis) { - let f; - if (typeof(funcToCall) === 'string') { - /* jshint evil: true */ - f = function() { return eval(funcToCall); }; - /* jshint evil: false */ - } else { - f = funcToCall; - } + self.scheduleFunction = function(funcToCall, millis, params, recurring, timeoutKey, runAtMillis) { + let f; + if (typeof(funcToCall) === 'string') { + /* jshint evil: true */ + f = function() { return eval(funcToCall); }; + /* jshint evil: false */ + } else { + f = funcToCall; + } - millis = millis || 0; - timeoutKey = timeoutKey || ++delayedFnCount; - runAtMillis = runAtMillis || (currentTime + millis); + millis = millis || 0; + timeoutKey = timeoutKey || ++delayedFnCount; + runAtMillis = runAtMillis || (currentTime + millis); - const funcToSchedule = { + const funcToSchedule = { runAtMillis: runAtMillis, funcToCall: f, recurring: recurring, params: params, timeoutKey: timeoutKey, millis: millis, - }; + }; - if (runAtMillis in scheduledFunctions) { - scheduledFunctions[runAtMillis].push(funcToSchedule); - } else { - scheduledFunctions[runAtMillis] = [funcToSchedule]; - scheduledLookup.push(runAtMillis); - scheduledLookup.sort(function(a, b) { - return a - b; - }); - } + if (runAtMillis in scheduledFunctions) { + scheduledFunctions[runAtMillis].push(funcToSchedule); + } else { + scheduledFunctions[runAtMillis] = [funcToSchedule]; + scheduledLookup.push(runAtMillis); + scheduledLookup.sort(function(a, b) { + return a - b; + }); + } - return timeoutKey; - }; + return timeoutKey; + }; - self.removeFunctionWithId = function(timeoutKey) { - for (const runAtMillis in scheduledFunctions) { - const funcs = scheduledFunctions[runAtMillis]; - const i = indexOfFirstToPass(funcs, function(func) { - return func.timeoutKey === timeoutKey; - }); + self.removeFunctionWithId = function(timeoutKey) { + for (const runAtMillis in scheduledFunctions) { + const funcs = scheduledFunctions[runAtMillis]; + const i = indexOfFirstToPass(funcs, function(func) { + return func.timeoutKey === timeoutKey; + }); - if (i > -1) { - if (funcs.length === 1) { - delete scheduledFunctions[runAtMillis]; - deleteFromLookup(runAtMillis); - } else { - funcs.splice(i, 1); - } + if (i > -1) { + if (funcs.length === 1) { + delete scheduledFunctions[runAtMillis]; + deleteFromLookup(runAtMillis); + } else { + funcs.splice(i, 1); + } - // intervals get rescheduled when executed, so there's never more - // than a single scheduled function with a given timeoutKey - break; + // intervals get rescheduled when executed, so there's never more + // than a single scheduled function with a given timeoutKey + break; + } + } + }; + + return self; + + function indexOfFirstToPass(array, testFn) { + let index = -1; + + for (let i = 0; i < array.length; ++i) { + if (testFn(array[i])) { + index = i; + break; + } + } + + return index; } - } - }; - return self; + function deleteFromLookup(key) { + const value = Number(key); + const i = indexOfFirstToPass(scheduledLookup, function(millis) { + return millis === value; + }); - function indexOfFirstToPass(array, testFn) { - let index = -1; - - for (let i = 0; i < array.length; ++i) { - if (testFn(array[i])) { - index = i; - break; + if (i > -1) { + scheduledLookup.splice(i, 1); + } } - } - return index; - } + function reschedule(scheduledFn) { + self.scheduleFunction(scheduledFn.funcToCall, + scheduledFn.millis, + scheduledFn.params, + true, + scheduledFn.timeoutKey, + scheduledFn.runAtMillis + scheduledFn.millis); + } - function deleteFromLookup(key) { - const value = Number(key); - const i = indexOfFirstToPass(scheduledLookup, function(millis) { - return millis === value; - }); + function forEachFunction(funcsToRun, callback) { + for (let i = 0; i < funcsToRun.length; ++i) { + callback(funcsToRun[i]); + } + } - if (i > -1) { - scheduledLookup.splice(i, 1); - } - } + function runScheduledFunctions(endTime) { + if (scheduledLookup.length === 0 || scheduledLookup[0] > endTime) { + return; + } - function reschedule(scheduledFn) { - self.scheduleFunction(scheduledFn.funcToCall, - scheduledFn.millis, - scheduledFn.params, - true, - scheduledFn.timeoutKey, - scheduledFn.runAtMillis + scheduledFn.millis); - } + do { + currentTime = scheduledLookup.shift(); - function forEachFunction(funcsToRun, callback) { - for (let i = 0; i < funcsToRun.length; ++i) { - callback(funcsToRun[i]); - } - } + const funcsToRun = scheduledFunctions[currentTime]; + delete scheduledFunctions[currentTime]; - function runScheduledFunctions(endTime) { - if (scheduledLookup.length === 0 || scheduledLookup[0] > endTime) { - return; - } + forEachFunction(funcsToRun, function(funcToRun) { + if (funcToRun.recurring) { + reschedule(funcToRun); + } + }); - do { - currentTime = scheduledLookup.shift(); - - const funcsToRun = scheduledFunctions[currentTime]; - delete scheduledFunctions[currentTime]; - - forEachFunction(funcsToRun, function(funcToRun) { - if (funcToRun.recurring) { - reschedule(funcToRun); - } - }); - - forEachFunction(funcsToRun, function(funcToRun) { - funcToRun.funcToCall.apply(null, funcToRun.params || []); - }); - } while (scheduledLookup.length > 0 && - // checking first if we're out of time prevents setTimeout(0) - // scheduled in a funcToRun from forcing an extra iteration + forEachFunction(funcsToRun, function(funcToRun) { + funcToRun.funcToCall.apply(null, funcToRun.params || []); + }); + } while (scheduledLookup.length > 0 && + // checking first if we're out of time prevents setTimeout(0) + // scheduled in a funcToRun from forcing an extra iteration currentTime !== endTime && scheduledLookup[0] <= endTime); + } } - } - return DelayedFunctionScheduler; + return DelayedFunctionScheduler; }(); - j$.MockDate = function() { - function MockDate(global) { - const self = this; - let currentTime = 0; + function MockDate(global) { + const self = this; + let currentTime = 0; - if (!global || !global.Date) { - self.install = function() {}; - self.tick = function() {}; - self.uninstall = function() {}; - return self; - } - - const GlobalDate = global.Date; - - self.install = function(mockDate) { - if (mockDate instanceof GlobalDate) { - currentTime = mockDate.getTime(); - } else { - currentTime = new GlobalDate().getTime(); - } - - global.Date = FakeDate; - }; - - self.tick = function(millis) { - millis = millis || 0; - currentTime = currentTime + millis; - }; - - self.uninstall = function() { - currentTime = 0; - global.Date = GlobalDate; - }; - - createDateProperties(); - - return self; - - function FakeDate() { - switch(arguments.length) { - case 0: - return new GlobalDate(currentTime); - case 1: - return new GlobalDate(arguments[0]); - case 2: - return new GlobalDate(arguments[0], arguments[1]); - case 3: - return new GlobalDate(arguments[0], arguments[1], arguments[2]); - case 4: - return new GlobalDate(arguments[0], arguments[1], arguments[2], arguments[3]); - case 5: - return new GlobalDate(arguments[0], arguments[1], arguments[2], arguments[3], - arguments[4]); - case 6: - return new GlobalDate(arguments[0], arguments[1], arguments[2], arguments[3], - arguments[4], arguments[5]); - default: - return new GlobalDate(arguments[0], arguments[1], arguments[2], arguments[3], - arguments[4], arguments[5], arguments[6]); - } - } - - function createDateProperties() { - FakeDate.prototype = GlobalDate.prototype; - - FakeDate.now = function() { - if (GlobalDate.now) { - return currentTime; - } else { - throw new Error('Browser does not support Date.now()'); + if (!global || !global.Date) { + self.install = function() {}; + self.tick = function() {}; + self.uninstall = function() {}; + return self; } - }; - FakeDate.toSource = GlobalDate.toSource; - FakeDate.toString = GlobalDate.toString; - FakeDate.parse = GlobalDate.parse; - FakeDate.UTC = GlobalDate.UTC; + const GlobalDate = global.Date; + + self.install = function(mockDate) { + if (mockDate instanceof GlobalDate) { + currentTime = mockDate.getTime(); + } else { + currentTime = new GlobalDate().getTime(); + } + + global.Date = FakeDate; + }; + + self.tick = function(millis) { + millis = millis || 0; + currentTime = currentTime + millis; + }; + + self.uninstall = function() { + currentTime = 0; + global.Date = GlobalDate; + }; + + createDateProperties(); + + return self; + + function FakeDate() { + switch (arguments.length) { + case 0: + return new GlobalDate(currentTime); + case 1: + return new GlobalDate(arguments[0]); + case 2: + return new GlobalDate(arguments[0], arguments[1]); + case 3: + return new GlobalDate(arguments[0], arguments[1], arguments[2]); + case 4: + return new GlobalDate(arguments[0], arguments[1], arguments[2], arguments[3]); + case 5: + return new GlobalDate(arguments[0], arguments[1], arguments[2], arguments[3], + arguments[4]); + case 6: + return new GlobalDate(arguments[0], arguments[1], arguments[2], arguments[3], + arguments[4], arguments[5]); + default: + return new GlobalDate(arguments[0], arguments[1], arguments[2], arguments[3], + arguments[4], arguments[5], arguments[6]); + } + } + + function createDateProperties() { + FakeDate.prototype = GlobalDate.prototype; + + FakeDate.now = function() { + if (GlobalDate.now) { + return currentTime; + } else { + throw new Error('Browser does not support Date.now()'); + } + }; + + FakeDate.toSource = GlobalDate.toSource; + FakeDate.toString = GlobalDate.toString; + FakeDate.parse = GlobalDate.parse; + FakeDate.UTC = GlobalDate.UTC; + } } - } - return MockDate; + return MockDate; }(); const _clock = new j$.Clock(global, function() { return new j$.DelayedFunctionScheduler(); }, new j$.MockDate(global)); @@ -418,4 +415,3 @@ export function clock() { return _clock; } - From 12cce2a93884046d39eae98df1b9fc214a2b3e58 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 1 Jul 2021 20:51:13 +0100 Subject: [PATCH 112/221] delint --- src/components/views/elements/BlurhashPlaceholder.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/elements/BlurhashPlaceholder.tsx b/src/components/views/elements/BlurhashPlaceholder.tsx index bed45bfe5a..0e59253fe8 100644 --- a/src/components/views/elements/BlurhashPlaceholder.tsx +++ b/src/components/views/elements/BlurhashPlaceholder.tsx @@ -15,7 +15,7 @@ */ import React from 'react'; -import {decode} from "blurhash"; +import { decode } from "blurhash"; interface IProps { blurhash: string; @@ -38,7 +38,7 @@ export default class BlurhashPlaceholder extends React.PureComponent { if (!this.canvas.current) return; try { - const {width, height} = this.props; + const { width, height } = this.props; const pixels = decode(this.props.blurhash, Math.ceil(width), Math.ceil(height)); const ctx = this.canvas.current.getContext("2d"); From 543fe6382d2d4e8f7feb643627667ca5b78aa2c0 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 1 Jul 2021 20:54:04 +0100 Subject: [PATCH 113/221] Fix linting in tests Or in the case of one test, disbale just the one rule it breaks --- .../structures/MessagePanel-test.js | 22 +++++++++---------- .../dialogs/InteractiveAuthDialog-test.js | 4 ++-- test/mock-clock.js | 11 +++++++++- 3 files changed, 23 insertions(+), 14 deletions(-) diff --git a/test/components/structures/MessagePanel-test.js b/test/components/structures/MessagePanel-test.js index d836de0785..ed2b7a6a36 100644 --- a/test/components/structures/MessagePanel-test.js +++ b/test/components/structures/MessagePanel-test.js @@ -1,6 +1,6 @@ /* Copyright 2016 OpenMarket Ltd -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2019, 2021 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. @@ -29,7 +29,7 @@ const MessagePanel = sdk.getComponent('structures.MessagePanel'); import { MatrixClientPeg } from '../../../src/MatrixClientPeg'; import Matrix from 'matrix-js-sdk'; -const test_utils = require('../../test-utils'); +const TestUtilsMatrix = require('../../test-utils'); const mockclock = require('../../mock-clock'); import Adapter from "@wojtekmaj/enzyme-adapter-react-17"; @@ -77,7 +77,7 @@ describe('MessagePanel', function() { const events = mkEvents(); beforeEach(function() { - test_utils.stubClient(); + TestUtilsMatrix.stubClient(); client = MatrixClientPeg.get(); client.credentials = { userId: '@me:here' }; @@ -97,7 +97,7 @@ describe('MessagePanel', function() { const events = []; const ts0 = Date.now(); for (let i = 0; i < 10; i++) { - events.push(test_utils.mkMessage( + events.push(TestUtilsMatrix.mkMessage( { event: true, room: "!room:id", user: "@user:id", ts: ts0 + i * 1000, @@ -111,7 +111,7 @@ describe('MessagePanel', function() { const events = []; const ts0 = Date.parse('09 May 2004 00:12:00 GMT'); for (let i = 0; i < 10; i++) { - events.push(test_utils.mkMessage( + events.push(TestUtilsMatrix.mkMessage( { event: true, room: "!room:id", user: "@user:id", ts: ts0 + i * 1000, @@ -127,13 +127,13 @@ describe('MessagePanel', function() { const ts0 = Date.now(); let i = 0; - events.push(test_utils.mkMessage({ + events.push(TestUtilsMatrix.mkMessage({ event: true, room: "!room:id", user: "@user:id", ts: ts0 + ++i * 1000, })); for (i = 0; i < 10; i++) { - events.push(test_utils.mkMembership({ + events.push(TestUtilsMatrix.mkMembership({ event: true, room: "!room:id", user: "@user:id", target: { userId: "@user:id", @@ -150,7 +150,7 @@ describe('MessagePanel', function() { })); } - events.push(test_utils.mkMessage({ + events.push(TestUtilsMatrix.mkMessage({ event: true, room: "!room:id", user: "@user:id", ts: ts0 + ++i*1000, })); @@ -166,7 +166,7 @@ describe('MessagePanel', function() { let i = 0; for (i = 0; i < 10; i++) { - events.push(test_utils.mkMembership({ + events.push(TestUtilsMatrix.mkMembership({ event: true, room: "!room:id", user: "@user:id", target: { userId: "@user:id", @@ -188,8 +188,8 @@ describe('MessagePanel', function() { // A list of room creation, encryption, and invite events. function mkCreationEvents() { - const mkEvent = test_utils.mkEvent; - const mkMembership = test_utils.mkMembership; + const mkEvent = TestUtilsMatrix.mkEvent; + const mkMembership = TestUtilsMatrix.mkMembership; const roomId = "!someroom"; const alice = "@alice:example.org"; const ts0 = Date.now(); diff --git a/test/components/views/dialogs/InteractiveAuthDialog-test.js b/test/components/views/dialogs/InteractiveAuthDialog-test.js index c75b2fdcc8..86fa562b60 100644 --- a/test/components/views/dialogs/InteractiveAuthDialog-test.js +++ b/test/components/views/dialogs/InteractiveAuthDialog-test.js @@ -22,7 +22,7 @@ import MatrixReactTestUtils from 'matrix-react-test-utils'; import sdk from '../../../skinned-sdk'; import { MatrixClientPeg } from '../../../../src/MatrixClientPeg'; -import * as test_utils from '../../../test-utils'; +import * as TestUtilsMatrix from '../../../test-utils'; import { sleep } from "../../../../src/utils/promise"; const InteractiveAuthDialog = sdk.getComponent( @@ -33,7 +33,7 @@ describe('InteractiveAuthDialog', function() { let parentDiv; beforeEach(function() { - test_utils.stubClient(); + TestUtilsMatrix.stubClient(); parentDiv = document.createElement('div'); document.body.appendChild(parentDiv); }); diff --git a/test/mock-clock.js b/test/mock-clock.js index 4306433b6f..134bd107dd 100644 --- a/test/mock-clock.js +++ b/test/mock-clock.js @@ -42,8 +42,14 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * The reason for C&Ping jasmine's clock here is that jasmine itself is * difficult to webpack, and we don't really want all of it. Sinon also has a * mock-clock implementation, but again, it is difficult to webpack. + * + * Hopefully the reasons for c+ping this no longer apply, but until we can + * verify this / remove it, disable the eslint rule that it breaks (impresssively + * only one, having fixed the long line). */ +/* eslint-disable prefer-rest-params */ + const j$ = {}; j$.Clock = function() { @@ -67,7 +73,10 @@ j$.Clock = function() { self.install = function() { if (!originalTimingFunctionsIntact()) { - throw new Error('Jasmine Clock was unable to install over custom global timer functions. Is the clock already installed?'); + throw new Error( + 'Jasmine Clock was unable to install over custom global timer functions. ' + + 'Is the clock already installed?', + ); } replace(global, fakeTimingFunctions); timer = fakeTimingFunctions; From 3db95d607d5bd8108a687574f5805a5a6e72c370 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 1 Jul 2021 20:57:56 +0100 Subject: [PATCH 114/221] Use semicolons Co-authored-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/rooms/RoomHeader.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/views/rooms/RoomHeader.tsx b/src/components/views/rooms/RoomHeader.tsx index 48b8951c81..0871c9e400 100644 --- a/src/components/views/rooms/RoomHeader.tsx +++ b/src/components/views/rooms/RoomHeader.tsx @@ -42,9 +42,9 @@ export interface ISearchInfo { } interface IProps { - room: Room, - oobData?: IOOBData, - inRoom: boolean, + room: Room; + oobData?: IOOBData; + inRoom: boolean; onSettingsClick: () => void; onSearchClick: () => void; onForgetClick: () => void; From 38ed53d5d19e2d6f3122b358b840e790dbc32f0d Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 1 Jul 2021 20:58:51 +0100 Subject: [PATCH 115/221] Oops, don't just blindly insert the commented type Co-authored-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/rooms/RoomHeader.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomHeader.tsx b/src/components/views/rooms/RoomHeader.tsx index 0871c9e400..2b398b442a 100644 --- a/src/components/views/rooms/RoomHeader.tsx +++ b/src/components/views/rooms/RoomHeader.tsx @@ -48,7 +48,7 @@ interface IProps { onSettingsClick: () => void; onSearchClick: () => void; onForgetClick: () => void; - onCallPlaced: (PlaceCallType) => void; + onCallPlaced: (type: PlaceCallType) => void; onAppsClick: () => void; e2eStatus: E2EStatus; appsShown: boolean; From eb1a22ab19e2dd86000c8809dea36fb7bf25d03e Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 1 Jul 2021 21:00:54 +0100 Subject: [PATCH 116/221] Add types --- src/components/views/rooms/RoomHeader.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/RoomHeader.tsx b/src/components/views/rooms/RoomHeader.tsx index 2b398b442a..af5daed5bc 100644 --- a/src/components/views/rooms/RoomHeader.tsx +++ b/src/components/views/rooms/RoomHeader.tsx @@ -30,7 +30,7 @@ import RoomName from "../elements/RoomName"; import { PlaceCallType } from "../../../CallHandler"; import { replaceableComponent } from "../../../utils/replaceableComponent"; import { throttle } from 'lodash'; -import { Room } from 'matrix-js-sdk/src'; +import { MatrixEvent, Room, RoomState } from 'matrix-js-sdk/src'; import { E2EStatus } from '../../../utils/ShieldUtils'; import { IOOBData } from '../../../stores/ThreepidInviteStore'; import { SearchScope } from './SearchBar'; @@ -74,7 +74,7 @@ export default class RoomHeader extends React.Component { } } - private onRoomStateEvents = (event, state) => { + private onRoomStateEvents = (event: MatrixEvent, state: RoomState) => { if (!this.props.room || event.getRoomId() !== this.props.room.roomId) { return; } From ac0f1ebf93dd6ff48d524d66aba596d07084db62 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 1 Jul 2021 21:05:57 +0100 Subject: [PATCH 117/221] fix field accesses in tests and default params --- src/Terms.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Terms.ts b/src/Terms.ts index e96081b7b7..f2c01ebe24 100644 --- a/src/Terms.ts +++ b/src/Terms.ts @@ -15,7 +15,7 @@ limitations under the License. */ import classNames from 'classnames'; -import { SERVICE_TYPES } from '../../matrix-js-sdk/src/service-types'; +import { SERVICE_TYPES } from 'matrix-js-sdk/src/service-types'; import { MatrixClientPeg } from './MatrixClientPeg'; import * as sdk from '.'; From c2545923c1f189b76eb4d388d4eeea229643c340 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 1 Jul 2021 21:10:37 +0100 Subject: [PATCH 118/221] Use better types to make CI happy --- .../views/rooms/SendMessageComposer.tsx | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/components/views/rooms/SendMessageComposer.tsx b/src/components/views/rooms/SendMessageComposer.tsx index ec190c829a..2c45c1bbf8 100644 --- a/src/components/views/rooms/SendMessageComposer.tsx +++ b/src/components/views/rooms/SendMessageComposer.tsx @@ -17,17 +17,19 @@ limitations under the License. import React, { ClipboardEvent, createRef, KeyboardEvent } from 'react'; import EMOJI_REGEX from 'emojibase-regex'; import { IContent, MatrixEvent } from 'matrix-js-sdk/src/models/event'; +import { DebouncedFunc, throttle } from 'lodash'; +import { EventType, RelationType } from "matrix-js-sdk/src/@types/event"; import dis from '../../../dispatcher/dispatcher'; import EditorModel from '../../../editor/model'; import { - htmlSerializeIfNeeded, - textSerialize, containsEmote, - stripEmoteCommand, - unescapeMessage, + htmlSerializeIfNeeded, startsWith, + stripEmoteCommand, stripPrefix, + textSerialize, + unescapeMessage, } from '../../../editor/serialize'; import { CommandPartCreator, Part, PartCreator, SerializedPart } from '../../../editor/parts'; import BasicMessageComposer from "./BasicMessageComposer"; @@ -52,7 +54,6 @@ import { Room } from 'matrix-js-sdk/src/models/room'; import ErrorDialog from "../dialogs/ErrorDialog"; import QuestionDialog from "../dialogs/QuestionDialog"; import { ActionPayload } from "../../../dispatcher/payloads"; -import { DebouncedFunc, throttle } from 'lodash'; function addReplyToMessageContent( content: IContent, @@ -258,12 +259,12 @@ export default class SendMessageComposer extends React.Component { const events = timeline.getEvents(); const reaction = this.model.parts[1].text; for (let i = events.length - 1; i >= 0; i--) { - if (events[i].getType() === "m.room.message") { + if (events[i].getType() === EventType.RoomMessage) { let shouldReact = true; const lastMessage = events[i]; const userId = MatrixClientPeg.get().getUserId(); const messageReactions = this.props.room.getUnfilteredTimelineSet() - .getRelationsForEvent(lastMessage.getId(), "m.annotation", "m.reaction"); + .getRelationsForEvent(lastMessage.getId(), RelationType.Annotation, EventType.Reaction); // if we have already sent this reaction, don't redact but don't re-send if (messageReactions) { @@ -274,9 +275,9 @@ export default class SendMessageComposer extends React.Component { shouldReact = !myReactionKeys.includes(reaction); } if (shouldReact) { - MatrixClientPeg.get().sendEvent(lastMessage.getRoomId(), "m.reaction", { + MatrixClientPeg.get().sendEvent(lastMessage.getRoomId(), EventType.Reaction, { "m.relates_to": { - "rel_type": "m.annotation", + "rel_type": RelationType.Annotation, "event_id": lastMessage.getId(), "key": reaction, }, From c1310bcd9f11faeb5414443cc9ece721582e19eb Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 1 Jul 2021 21:31:17 +0100 Subject: [PATCH 119/221] Better types --- package.json | 1 + src/Markdown.ts | 26 ++++++++++++++++---------- yarn.lock | 5 +++++ 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 4ad585ba7d..c505e16dce 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ }, "dependencies": { "@babel/runtime": "^7.12.5", + "@types/commonmark": "^0.27.4", "await-lock": "^2.1.0", "browser-encrypt-attachment": "^0.3.0", "browser-request": "^0.3.3", diff --git a/src/Markdown.ts b/src/Markdown.ts index 64037369db..bb6d6d8df0 100644 --- a/src/Markdown.ts +++ b/src/Markdown.ts @@ -23,7 +23,15 @@ const ALLOWED_HTML_TAGS = ['sub', 'sup', 'del', 'u']; // These types of node are definitely text const TEXT_NODES = ['text', 'softbreak', 'linebreak', 'paragraph', 'document']; -function isAllowedHtmlTag(node: any) { +// As far as @types/commonmark is concerned, these are not public, so add them +interface CommonmarkHtmlRendererInternal extends commonmark.HtmlRenderer { + paragraph: (node: commonmark.Node, entering: boolean) => void; + link: (node: commonmark.Node, entering: boolean) => void; + html_inline: (node: commonmark.Node) => void; // eslint-disable-line camelcase + html_block: (node: commonmark.Node) => void; // eslint-disable-line camelcase +} + +function isAllowedHtmlTag(node: commonmark.Node): boolean { if (node.literal != null && node.literal.match('^<((div|span) data-mx-maths="[^"]*"|/(div|span))>$') != null) { return true; @@ -45,7 +53,7 @@ function isAllowedHtmlTag(node: any) { * comprises multiple block level elements (ie. lines), * or false if it is only a single line. */ -function isMultiLine(node) { +function isMultiLine(node: commonmark.Node): boolean { let par = node; while (par.parent) { par = par.parent; @@ -57,12 +65,10 @@ function isMultiLine(node) { * Class that wraps commonmark, adding the ability to see whether * a given message actually uses any markdown syntax or whether * it's plain text. - * - * Types are a bit of a struggle here as commonmark doesn't have types */ export default class Markdown { private input: string; - private parsed: any; + private parsed: commonmark.Node; constructor(input) { this.input = input; @@ -71,7 +77,7 @@ export default class Markdown { this.parsed = parser.parse(this.input); } - isPlainText() { + isPlainText(): boolean { const walker = this.parsed.walker(); let ev; @@ -94,7 +100,7 @@ export default class Markdown { return true; } - toHTML({ externalLinks = false } = {}) { + toHTML({ externalLinks = false } = {}): string { const renderer = new commonmark.HtmlRenderer({ safe: false, @@ -104,7 +110,7 @@ export default class Markdown { // block quote ends up all on one line // (https://github.com/vector-im/element-web/issues/3154) softbreak: '
', - }); + }) as CommonmarkHtmlRendererInternal; // Trying to strip out the wrapping

causes a lot more complication // than it's worth, i think. For instance, this code will go and strip @@ -181,8 +187,8 @@ export default class Markdown { * N.B. this does **NOT** render arbitrary MD to plain text - only MD * which has no formatting. Otherwise it emits HTML(!). */ - toPlaintext() { - const renderer = new commonmark.HtmlRenderer({ safe: false }); + toPlaintext(): string { + const renderer = new commonmark.HtmlRenderer({ safe: false }) as CommonmarkHtmlRendererInternal; renderer.paragraph = function(node, entering) { // as with toHTML, only append lines to paragraphs if there are diff --git a/yarn.lock b/yarn.lock index 6f08372e18..d59dc9d79f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1478,6 +1478,11 @@ resolved "https://registry.yarnpkg.com/@types/classnames/-/classnames-2.2.11.tgz#2521cc86f69d15c5b90664e4829d84566052c1cf" integrity sha512-2koNhpWm3DgWRp5tpkiJ8JGc1xTn2q0l+jUNUE7oMKXUf5NpI9AIdC4kbjGNFBdHtcxBD18LAksoudAVhFKCjw== +"@types/commonmark@^0.27.4": + version "0.27.4" + resolved "https://registry.yarnpkg.com/@types/commonmark/-/commonmark-0.27.4.tgz#8f42990e5cf3b6b95bd99eaa452e157aab679b82" + integrity sha512-7koSjp08QxKoS1/+3T15+kD7+vqOUvZRHvM8PutF3Xsk5aAEkdlIGRsHJ3/XsC3izoqTwBdRW/vH7rzCKkIicA== + "@types/counterpart@^0.18.1": version "0.18.1" resolved "https://registry.yarnpkg.com/@types/counterpart/-/counterpart-0.18.1.tgz#b1b784d9e54d9879f0a8cb12f2caedab65430fe8" From 08176beb58163f3695a16ff757d838886fea5026 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 1 Jul 2021 21:52:15 +0100 Subject: [PATCH 120/221] Kill mock-clock We already import sinon's one in UserActivitytest, so use that here too. --- .../structures/MessagePanel-test.js | 12 +- test/mock-clock.js | 426 ------------------ 2 files changed, 7 insertions(+), 431 deletions(-) delete mode 100644 test/mock-clock.js diff --git a/test/components/structures/MessagePanel-test.js b/test/components/structures/MessagePanel-test.js index ed2b7a6a36..f415b85105 100644 --- a/test/components/structures/MessagePanel-test.js +++ b/test/components/structures/MessagePanel-test.js @@ -30,7 +30,7 @@ import { MatrixClientPeg } from '../../../src/MatrixClientPeg'; import Matrix from 'matrix-js-sdk'; const TestUtilsMatrix = require('../../test-utils'); -const mockclock = require('../../mock-clock'); +import FakeTimers from '@sinonjs/fake-timers'; import Adapter from "@wojtekmaj/enzyme-adapter-react-17"; import { configure, mount } from "enzyme"; @@ -72,7 +72,7 @@ class WrappedMessagePanel extends React.Component { } describe('MessagePanel', function() { - const clock = mockclock.clock(); + let clock = null; const realSetTimeout = window.setTimeout; const events = mkEvents(); @@ -90,7 +90,10 @@ describe('MessagePanel', function() { }); afterEach(function() { - clock.uninstall(); + if (clock) { + clock.uninstall(); + clock = null; + } }); function mkEvents() { @@ -362,8 +365,7 @@ describe('MessagePanel', function() { it('shows a ghost read-marker when the read-marker moves', function(done) { // fake the clock so that we can test the velocity animation. - clock.install(); - clock.mockDate(); + clock = FakeTimers.install(); const parentDiv = document.createElement('div'); diff --git a/test/mock-clock.js b/test/mock-clock.js deleted file mode 100644 index 134bd107dd..0000000000 --- a/test/mock-clock.js +++ /dev/null @@ -1,426 +0,0 @@ -/* -Copyright (c) 2008-2015 Pivotal Labs -Copyright 2019 The Matrix.org Foundation C.I.C. - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -*/ - -/* This is jasmine's implementation of a mock clock, lifted from the depths of - * jasmine-core and exposed as a standalone module. The interface is just the - * same as that of jasmine.clock. For example: - * - * var mock_clock = require("../../mock-clock").clock(); - * mock_clock.install(); - * setTimeout(function() { - * timerCallback(); - * }, 100); - * - * expect(timerCallback).not.toHaveBeenCalled(); - * mock_clock.tick(101); - * expect(timerCallback).toHaveBeenCalled(); - * - * mock_clock.uninstall(); - * - * - * The reason for C&Ping jasmine's clock here is that jasmine itself is - * difficult to webpack, and we don't really want all of it. Sinon also has a - * mock-clock implementation, but again, it is difficult to webpack. - * - * Hopefully the reasons for c+ping this no longer apply, but until we can - * verify this / remove it, disable the eslint rule that it breaks (impresssively - * only one, having fixed the long line). - */ - -/* eslint-disable prefer-rest-params */ - -const j$ = {}; - -j$.Clock = function() { - function Clock(global, delayedFunctionSchedulerFactory, mockDate) { - const self = this; - const realTimingFunctions = { - setTimeout: global.setTimeout, - clearTimeout: global.clearTimeout, - setInterval: global.setInterval, - clearInterval: global.clearInterval, - }; - const fakeTimingFunctions = { - setTimeout: setTimeout, - clearTimeout: clearTimeout, - setInterval: setInterval, - clearInterval: clearInterval, - }; - let installed = false; - let delayedFunctionScheduler; - let timer; - - self.install = function() { - if (!originalTimingFunctionsIntact()) { - throw new Error( - 'Jasmine Clock was unable to install over custom global timer functions. ' + - 'Is the clock already installed?', - ); - } - replace(global, fakeTimingFunctions); - timer = fakeTimingFunctions; - delayedFunctionScheduler = delayedFunctionSchedulerFactory(); - installed = true; - - return self; - }; - - self.uninstall = function() { - delayedFunctionScheduler = null; - mockDate.uninstall(); - replace(global, realTimingFunctions); - - timer = realTimingFunctions; - installed = false; - }; - - self.withMock = function(closure) { - this.install(); - try { - closure(); - } finally { - this.uninstall(); - } - }; - - self.mockDate = function(initialDate) { - mockDate.install(initialDate); - }; - - self.setTimeout = function(fn, delay, params) { - if (legacyIE()) { - if (arguments.length > 2) { - throw new Error('IE < 9 cannot support extra params to setTimeout without a polyfill'); - } - return timer.setTimeout(fn, delay); - } - return Function.prototype.apply.apply(timer.setTimeout, [global, arguments]); - }; - - self.setInterval = function(fn, delay, params) { - if (legacyIE()) { - if (arguments.length > 2) { - throw new Error('IE < 9 cannot support extra params to setInterval without a polyfill'); - } - return timer.setInterval(fn, delay); - } - return Function.prototype.apply.apply(timer.setInterval, [global, arguments]); - }; - - self.clearTimeout = function(id) { - return Function.prototype.call.apply(timer.clearTimeout, [global, id]); - }; - - self.clearInterval = function(id) { - return Function.prototype.call.apply(timer.clearInterval, [global, id]); - }; - - self.tick = function(millis) { - if (installed) { - mockDate.tick(millis); - delayedFunctionScheduler.tick(millis); - } else { - throw new Error('Mock clock is not installed, use jasmine.clock().install()'); - } - }; - - return self; - - function originalTimingFunctionsIntact() { - return global.setTimeout === realTimingFunctions.setTimeout && - global.clearTimeout === realTimingFunctions.clearTimeout && - global.setInterval === realTimingFunctions.setInterval && - global.clearInterval === realTimingFunctions.clearInterval; - } - - function legacyIE() { - //if these methods are polyfilled, apply will be present - return !(realTimingFunctions.setTimeout || realTimingFunctions.setInterval).apply; - } - - function replace(dest, source) { - for (const prop in source) { - dest[prop] = source[prop]; - } - } - - function setTimeout(fn, delay) { - return delayedFunctionScheduler.scheduleFunction(fn, delay, argSlice(arguments, 2)); - } - - function clearTimeout(id) { - return delayedFunctionScheduler.removeFunctionWithId(id); - } - - function setInterval(fn, interval) { - return delayedFunctionScheduler.scheduleFunction(fn, interval, argSlice(arguments, 2), true); - } - - function clearInterval(id) { - return delayedFunctionScheduler.removeFunctionWithId(id); - } - - function argSlice(argsObj, n) { - return Array.prototype.slice.call(argsObj, n); - } - } - - return Clock; -}(); - -j$.DelayedFunctionScheduler = function() { - function DelayedFunctionScheduler() { - const self = this; - const scheduledLookup = []; - const scheduledFunctions = {}; - let currentTime = 0; - let delayedFnCount = 0; - - self.tick = function(millis) { - millis = millis || 0; - const endTime = currentTime + millis; - - runScheduledFunctions(endTime); - currentTime = endTime; - }; - - self.scheduleFunction = function(funcToCall, millis, params, recurring, timeoutKey, runAtMillis) { - let f; - if (typeof(funcToCall) === 'string') { - /* jshint evil: true */ - f = function() { return eval(funcToCall); }; - /* jshint evil: false */ - } else { - f = funcToCall; - } - - millis = millis || 0; - timeoutKey = timeoutKey || ++delayedFnCount; - runAtMillis = runAtMillis || (currentTime + millis); - - const funcToSchedule = { - runAtMillis: runAtMillis, - funcToCall: f, - recurring: recurring, - params: params, - timeoutKey: timeoutKey, - millis: millis, - }; - - if (runAtMillis in scheduledFunctions) { - scheduledFunctions[runAtMillis].push(funcToSchedule); - } else { - scheduledFunctions[runAtMillis] = [funcToSchedule]; - scheduledLookup.push(runAtMillis); - scheduledLookup.sort(function(a, b) { - return a - b; - }); - } - - return timeoutKey; - }; - - self.removeFunctionWithId = function(timeoutKey) { - for (const runAtMillis in scheduledFunctions) { - const funcs = scheduledFunctions[runAtMillis]; - const i = indexOfFirstToPass(funcs, function(func) { - return func.timeoutKey === timeoutKey; - }); - - if (i > -1) { - if (funcs.length === 1) { - delete scheduledFunctions[runAtMillis]; - deleteFromLookup(runAtMillis); - } else { - funcs.splice(i, 1); - } - - // intervals get rescheduled when executed, so there's never more - // than a single scheduled function with a given timeoutKey - break; - } - } - }; - - return self; - - function indexOfFirstToPass(array, testFn) { - let index = -1; - - for (let i = 0; i < array.length; ++i) { - if (testFn(array[i])) { - index = i; - break; - } - } - - return index; - } - - function deleteFromLookup(key) { - const value = Number(key); - const i = indexOfFirstToPass(scheduledLookup, function(millis) { - return millis === value; - }); - - if (i > -1) { - scheduledLookup.splice(i, 1); - } - } - - function reschedule(scheduledFn) { - self.scheduleFunction(scheduledFn.funcToCall, - scheduledFn.millis, - scheduledFn.params, - true, - scheduledFn.timeoutKey, - scheduledFn.runAtMillis + scheduledFn.millis); - } - - function forEachFunction(funcsToRun, callback) { - for (let i = 0; i < funcsToRun.length; ++i) { - callback(funcsToRun[i]); - } - } - - function runScheduledFunctions(endTime) { - if (scheduledLookup.length === 0 || scheduledLookup[0] > endTime) { - return; - } - - do { - currentTime = scheduledLookup.shift(); - - const funcsToRun = scheduledFunctions[currentTime]; - delete scheduledFunctions[currentTime]; - - forEachFunction(funcsToRun, function(funcToRun) { - if (funcToRun.recurring) { - reschedule(funcToRun); - } - }); - - forEachFunction(funcsToRun, function(funcToRun) { - funcToRun.funcToCall.apply(null, funcToRun.params || []); - }); - } while (scheduledLookup.length > 0 && - // checking first if we're out of time prevents setTimeout(0) - // scheduled in a funcToRun from forcing an extra iteration - currentTime !== endTime && - scheduledLookup[0] <= endTime); - } - } - - return DelayedFunctionScheduler; -}(); - -j$.MockDate = function() { - function MockDate(global) { - const self = this; - let currentTime = 0; - - if (!global || !global.Date) { - self.install = function() {}; - self.tick = function() {}; - self.uninstall = function() {}; - return self; - } - - const GlobalDate = global.Date; - - self.install = function(mockDate) { - if (mockDate instanceof GlobalDate) { - currentTime = mockDate.getTime(); - } else { - currentTime = new GlobalDate().getTime(); - } - - global.Date = FakeDate; - }; - - self.tick = function(millis) { - millis = millis || 0; - currentTime = currentTime + millis; - }; - - self.uninstall = function() { - currentTime = 0; - global.Date = GlobalDate; - }; - - createDateProperties(); - - return self; - - function FakeDate() { - switch (arguments.length) { - case 0: - return new GlobalDate(currentTime); - case 1: - return new GlobalDate(arguments[0]); - case 2: - return new GlobalDate(arguments[0], arguments[1]); - case 3: - return new GlobalDate(arguments[0], arguments[1], arguments[2]); - case 4: - return new GlobalDate(arguments[0], arguments[1], arguments[2], arguments[3]); - case 5: - return new GlobalDate(arguments[0], arguments[1], arguments[2], arguments[3], - arguments[4]); - case 6: - return new GlobalDate(arguments[0], arguments[1], arguments[2], arguments[3], - arguments[4], arguments[5]); - default: - return new GlobalDate(arguments[0], arguments[1], arguments[2], arguments[3], - arguments[4], arguments[5], arguments[6]); - } - } - - function createDateProperties() { - FakeDate.prototype = GlobalDate.prototype; - - FakeDate.now = function() { - if (GlobalDate.now) { - return currentTime; - } else { - throw new Error('Browser does not support Date.now()'); - } - }; - - FakeDate.toSource = GlobalDate.toSource; - FakeDate.toString = GlobalDate.toString; - FakeDate.parse = GlobalDate.parse; - FakeDate.UTC = GlobalDate.UTC; - } - } - - return MockDate; -}(); - -const _clock = new j$.Clock(global, function() { return new j$.DelayedFunctionScheduler(); }, new j$.MockDate(global)); - -export function clock() { - return _clock; -} - From c0fab6647d2468143186a903ed3c8923fa385ef8 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 1 Jul 2021 22:50:06 +0100 Subject: [PATCH 121/221] Use defer from js-sdk as it has a better ts definition --- src/Modal.tsx | 2 +- src/components/structures/MatrixChat.tsx | 4 +++- src/utils/MultiInviter.ts | 2 +- src/utils/promise.ts | 19 ------------------- 4 files changed, 5 insertions(+), 22 deletions(-) diff --git a/src/Modal.tsx b/src/Modal.tsx index 0b0e621e89..55fc871d67 100644 --- a/src/Modal.tsx +++ b/src/Modal.tsx @@ -18,10 +18,10 @@ limitations under the License. import React from 'react'; import ReactDOM from 'react-dom'; import classNames from 'classnames'; +import { defer } from "matrix-js-sdk/src/utils"; import Analytics from './Analytics'; import dis from './dispatcher/dispatcher'; -import { defer } from './utils/promise'; import AsyncWrapper from './AsyncWrapper'; const DIALOG_CONTAINER_ID = "mx_Dialog_Container"; diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index c1e0b8d7cb..da06e06442 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -19,6 +19,8 @@ import { createClient } from "matrix-js-sdk/src/matrix"; import { InvalidStoreError } from "matrix-js-sdk/src/errors"; import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { defer, IDeferred } from "matrix-js-sdk/src/utils"; + // focus-visible is a Polyfill for the :focus-visible CSS pseudo-attribute used by _AccessibleButton.scss import 'focus-visible'; // what-input helps improve keyboard accessibility @@ -55,7 +57,7 @@ import DMRoomMap from '../../utils/DMRoomMap'; import ThemeWatcher from "../../settings/watchers/ThemeWatcher"; import { FontWatcher } from '../../settings/watchers/FontWatcher'; import { storeRoomAliasInCache } from '../../RoomAliasCache'; -import { defer, IDeferred, sleep } from "../../utils/promise"; +import { sleep } from "../../utils/promise"; import ToastStore from "../../stores/ToastStore"; import * as StorageManager from "../../utils/StorageManager"; import type LoggedInViewType from "./LoggedInView"; diff --git a/src/utils/MultiInviter.ts b/src/utils/MultiInviter.ts index 4e66d4a99f..a7d1accde1 100644 --- a/src/utils/MultiInviter.ts +++ b/src/utils/MultiInviter.ts @@ -15,6 +15,7 @@ limitations under the License. */ import { MatrixError } from "matrix-js-sdk/src/http-api"; +import { defer, IDeferred } from "matrix-js-sdk/src/utils"; import { MatrixClientPeg } from '../MatrixClientPeg'; import { AddressType, getAddressType } from '../UserAddress'; @@ -22,7 +23,6 @@ import GroupStore from '../stores/GroupStore'; import { _t } from "../languageHandler"; import Modal from "../Modal"; import SettingsStore from "../settings/SettingsStore"; -import { defer, IDeferred } from "./promise"; import AskInviteAnywayDialog from "../components/views/dialogs/AskInviteAnywayDialog"; export enum InviteState { diff --git a/src/utils/promise.ts b/src/utils/promise.ts index 902d3c7e31..003da874fb 100644 --- a/src/utils/promise.ts +++ b/src/utils/promise.ts @@ -32,25 +32,6 @@ export async function timeout(promise: Promise, timeoutValue: T, ms: numbe return Promise.race([promise, timeoutPromise]); } -export interface IDeferred { - resolve: (value: T) => void; - reject: (any) => void; - promise: Promise; -} - -// Returns a Deferred -export function defer(): IDeferred { - let resolve; - let reject; - - const promise = new Promise((_resolve, _reject) => { - resolve = _resolve; - reject = _reject; - }); - - return { resolve, reject, promise }; -} - // Helper method to retry a Promise a given number of times or until a predicate fails export async function retry(fn: () => Promise, num: number, predicate?: (e: E) => boolean) { let lastErr: E; From 88f691776f113ee3cfdfe5520cc691a878339d7b Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 1 Jul 2021 22:55:27 +0100 Subject: [PATCH 122/221] Use sleep from js-sdk as no point duplicating it --- src/CountlyAnalytics.ts | 2 +- src/components/structures/GroupView.js | 2 +- src/components/structures/MatrixChat.tsx | 3 +-- src/components/views/dialogs/AddExistingToSpaceDialog.tsx | 2 +- src/components/views/dialogs/AddressPickerDialog.js | 2 +- .../views/settings/tabs/user/SecurityUserSettingsTab.js | 3 ++- src/components/views/spaces/SpacePublicShare.tsx | 2 +- src/indexing/EventIndex.ts | 2 +- src/utils/promise.ts | 5 ----- test/components/views/dialogs/InteractiveAuthDialog-test.js | 2 +- test/components/views/rooms/SendMessageComposer-test.js | 3 ++- 11 files changed, 12 insertions(+), 16 deletions(-) diff --git a/src/CountlyAnalytics.ts b/src/CountlyAnalytics.ts index 3ad56fe3bf..0711a60e11 100644 --- a/src/CountlyAnalytics.ts +++ b/src/CountlyAnalytics.ts @@ -16,12 +16,12 @@ limitations under the License. import { randomString } from "matrix-js-sdk/src/randomstring"; import { IContent } from "matrix-js-sdk/src/models/event"; +import { sleep } from "matrix-js-sdk/src/utils"; import { getCurrentLanguage } from './languageHandler'; import PlatformPeg from './PlatformPeg'; import SdkConfig from './SdkConfig'; import { MatrixClientPeg } from "./MatrixClientPeg"; -import { sleep } from "./utils/promise"; import RoomViewStore from "./stores/RoomViewStore"; import { Action } from "./dispatcher/actions"; diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index 93c44c4e50..f31f302b29 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -36,7 +36,7 @@ import FlairStore from '../../stores/FlairStore'; import { showGroupAddRoomDialog } from '../../GroupAddressPicker'; import { makeGroupPermalink, makeUserPermalink } from "../../utils/permalinks/Permalinks"; import { Group } from "matrix-js-sdk/src/models/group"; -import { sleep } from "../../utils/promise"; +import { sleep } from "matrix-js-sdk/src/utils"; import RightPanelStore from "../../stores/RightPanelStore"; import AutoHideScrollbar from "./AutoHideScrollbar"; import { mediaFromMxc } from "../../customisations/Media"; diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index da06e06442..cf2cea1ec1 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -19,7 +19,7 @@ import { createClient } from "matrix-js-sdk/src/matrix"; import { InvalidStoreError } from "matrix-js-sdk/src/errors"; import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; -import { defer, IDeferred } from "matrix-js-sdk/src/utils"; +import { sleep, defer, IDeferred } from "matrix-js-sdk/src/utils"; // focus-visible is a Polyfill for the :focus-visible CSS pseudo-attribute used by _AccessibleButton.scss import 'focus-visible'; @@ -57,7 +57,6 @@ import DMRoomMap from '../../utils/DMRoomMap'; import ThemeWatcher from "../../settings/watchers/ThemeWatcher"; import { FontWatcher } from '../../settings/watchers/FontWatcher'; import { storeRoomAliasInCache } from '../../RoomAliasCache'; -import { sleep } from "../../utils/promise"; import ToastStore from "../../stores/ToastStore"; import * as StorageManager from "../../utils/StorageManager"; import type LoggedInViewType from "./LoggedInView"; diff --git a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx index c09097c4b4..5024b98def 100644 --- a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx +++ b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx @@ -18,6 +18,7 @@ import React, { ReactNode, useContext, useMemo, useState } from "react"; import classNames from "classnames"; import { Room } from "matrix-js-sdk/src/models/room"; import { MatrixClient } from "matrix-js-sdk/src/client"; +import { sleep } from "matrix-js-sdk/src/utils"; import { _t } from '../../../languageHandler'; import { IDialogProps } from "./IDialogProps"; @@ -29,7 +30,6 @@ import RoomAvatar from "../avatars/RoomAvatar"; import { getDisplayAliasForRoom } from "../../../Rooms"; import AccessibleButton from "../elements/AccessibleButton"; import AutoHideScrollbar from "../../structures/AutoHideScrollbar"; -import { sleep } from "../../../utils/promise"; import DMRoomMap from "../../../utils/DMRoomMap"; import { calculateRoomVia } from "../../../utils/permalinks/Permalinks"; import StyledCheckbox from "../elements/StyledCheckbox"; diff --git a/src/components/views/dialogs/AddressPickerDialog.js b/src/components/views/dialogs/AddressPickerDialog.js index ad4fc8d672..09714e24e3 100644 --- a/src/components/views/dialogs/AddressPickerDialog.js +++ b/src/components/views/dialogs/AddressPickerDialog.js @@ -19,6 +19,7 @@ limitations under the License. import React, { createRef } from 'react'; import PropTypes from 'prop-types'; +import { sleep } from "matrix-js-sdk/src/utils"; import { _t, _td } from '../../../languageHandler'; import * as sdk from '../../../index'; @@ -30,7 +31,6 @@ import * as Email from '../../../email'; import IdentityAuthClient from '../../../IdentityAuthClient'; import { getDefaultIdentityServerUrl, useDefaultIdentityServer } from '../../../utils/IdentityServerUtils'; import { abbreviateUrl } from '../../../utils/UrlUtils'; -import { sleep } from "../../../utils/promise"; import { Key } from "../../../Keyboard"; import { Action } from "../../../dispatcher/actions"; import { replaceableComponent } from "../../../utils/replaceableComponent"; diff --git a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js index 79d8c9224e..a03598b21f 100644 --- a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js @@ -17,6 +17,8 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; +import { sleep } from "matrix-js-sdk/src/utils"; + import { _t } from "../../../../../languageHandler"; import SdkConfig from "../../../../../SdkConfig"; import { MatrixClientPeg } from "../../../../../MatrixClientPeg"; @@ -25,7 +27,6 @@ import AccessibleButton from "../../../elements/AccessibleButton"; import Analytics from "../../../../../Analytics"; import Modal from "../../../../../Modal"; import * as sdk from "../../../../.."; -import { sleep } from "../../../../../utils/promise"; import dis from "../../../../../dispatcher/dispatcher"; import { privateShouldBeEncrypted } from "../../../../../createRoom"; import { SettingLevel } from "../../../../../settings/SettingLevel"; diff --git a/src/components/views/spaces/SpacePublicShare.tsx b/src/components/views/spaces/SpacePublicShare.tsx index 4169e40269..39e5115e55 100644 --- a/src/components/views/spaces/SpacePublicShare.tsx +++ b/src/components/views/spaces/SpacePublicShare.tsx @@ -16,11 +16,11 @@ limitations under the License. import React, { useState } from "react"; import { Room } from "matrix-js-sdk/src/models/room"; +import { sleep } from "matrix-js-sdk/src/utils"; import { _t } from "../../../languageHandler"; import AccessibleButton from "../elements/AccessibleButton"; import { copyPlaintext } from "../../../utils/strings"; -import { sleep } from "../../../utils/promise"; import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks"; import { showRoomInviteDialog } from "../../../RoomInvite"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; diff --git a/src/indexing/EventIndex.ts b/src/indexing/EventIndex.ts index c84d446197..76104455f7 100644 --- a/src/indexing/EventIndex.ts +++ b/src/indexing/EventIndex.ts @@ -22,10 +22,10 @@ import { MatrixEvent } from 'matrix-js-sdk/src/models/event'; import { EventTimelineSet } from 'matrix-js-sdk/src/models/event-timeline-set'; import { RoomState } from 'matrix-js-sdk/src/models/room-state'; import { TimelineWindow } from 'matrix-js-sdk/src/timeline-window'; +import { sleep } from "matrix-js-sdk/src/utils"; import PlatformPeg from "../PlatformPeg"; import { MatrixClientPeg } from "../MatrixClientPeg"; -import { sleep } from "../utils/promise"; import SettingsStore from "../settings/SettingsStore"; import { SettingLevel } from "../settings/SettingLevel"; import { ICrawlerCheckpoint, ILoadArgs, ISearchArgs } from "./BaseEventIndexManager"; diff --git a/src/utils/promise.ts b/src/utils/promise.ts index 003da874fb..853c172269 100644 --- a/src/utils/promise.ts +++ b/src/utils/promise.ts @@ -14,11 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -// Returns a promise which resolves with a given value after the given number of ms -export function sleep(ms: number, value?: T): Promise { - return new Promise((resolve => { setTimeout(resolve, ms, value); })); -} - // Returns a promise which resolves when the input promise resolves with its value // or when the timeout of ms is reached with the value of given timeoutValue export async function timeout(promise: Promise, timeoutValue: T, ms: number): Promise { diff --git a/test/components/views/dialogs/InteractiveAuthDialog-test.js b/test/components/views/dialogs/InteractiveAuthDialog-test.js index fa44fc8d92..23b767f72e 100644 --- a/test/components/views/dialogs/InteractiveAuthDialog-test.js +++ b/test/components/views/dialogs/InteractiveAuthDialog-test.js @@ -18,12 +18,12 @@ import React from 'react'; import ReactDOM from 'react-dom'; import ReactTestUtils from 'react-dom/test-utils'; import MatrixReactTestUtils from 'matrix-react-test-utils'; +import { sleep } from "matrix-js-sdk/src/utils"; import sdk from '../../../skinned-sdk'; import {MatrixClientPeg} from '../../../../src/MatrixClientPeg'; import * as test_utils from '../../../test-utils'; -import {sleep} from "../../../../src/utils/promise"; const InteractiveAuthDialog = sdk.getComponent( 'views.dialogs.InteractiveAuthDialog', diff --git a/test/components/views/rooms/SendMessageComposer-test.js b/test/components/views/rooms/SendMessageComposer-test.js index 2947f0fe60..0c4bde76a8 100644 --- a/test/components/views/rooms/SendMessageComposer-test.js +++ b/test/components/views/rooms/SendMessageComposer-test.js @@ -19,6 +19,8 @@ import Adapter from "@wojtekmaj/enzyme-adapter-react-17"; import { configure, mount } from "enzyme"; import React from "react"; import { act } from "react-dom/test-utils"; +import { sleep } from "matrix-js-sdk/src/utils"; + import SendMessageComposer, { createMessageContent, isQuickReaction, @@ -29,7 +31,6 @@ import { createPartCreator, createRenderer } from "../../../editor/mock"; import { createTestClient, mkEvent, mkStubRoom } from "../../../test-utils"; import BasicMessageComposer from "../../../../src/components/views/rooms/BasicMessageComposer"; import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; -import { sleep } from "../../../../src/utils/promise"; import SpecPermalinkConstructor from "../../../../src/utils/permalinks/SpecPermalinkConstructor"; import defaultDispatcher from "../../../../src/dispatcher/dispatcher"; From fda17b4959b90de2db2884b126bcb7fc14dd4007 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 1 Jul 2021 23:02:51 +0100 Subject: [PATCH 123/221] Add types to the implementations too Co-authored-by: Michael Telatynski <7t3chguy@gmail.com> --- src/Markdown.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Markdown.ts b/src/Markdown.ts index bb6d6d8df0..96169d4011 100644 --- a/src/Markdown.ts +++ b/src/Markdown.ts @@ -123,7 +123,7 @@ export default class Markdown { const realParagraph = renderer.paragraph; - renderer.paragraph = function(node, entering) { + renderer.paragraph = function(node: commonmark.Node, entering: boolean) { // If there is only one top level node, just return the // bare text: it's a single line of text and so should be // 'inline', rather than unnecessarily wrapped in its own @@ -153,7 +153,7 @@ export default class Markdown { } }; - renderer.html_inline = function(node: any) { + renderer.html_inline = function(node: commonmark.Node) { if (isAllowedHtmlTag(node)) { this.lit(node.literal); return; @@ -162,7 +162,7 @@ export default class Markdown { } }; - renderer.html_block = function(node) { + renderer.html_block = function(node: commonmark.Node) { /* // as with `paragraph`, we only insert line breaks // if there are multiple lines in the markdown. @@ -190,7 +190,7 @@ export default class Markdown { toPlaintext(): string { const renderer = new commonmark.HtmlRenderer({ safe: false }) as CommonmarkHtmlRendererInternal; - renderer.paragraph = function(node, entering) { + renderer.paragraph = function(node: commonmark.Node, entering: boolean) { // as with toHTML, only append lines to paragraphs if there are // multiple paragraphs if (isMultiLine(node)) { @@ -200,7 +200,7 @@ export default class Markdown { } }; - renderer.html_block = function(node) { + renderer.html_block = function(node: commonmark.Node) { this.lit(node.literal); if (isMultiLine(node) && node.next) this.lit('\n\n'); }; From 80f52322176d988f38cc5f9319e2ce4aaf9eed4c Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 1 Jul 2021 23:23:03 +0100 Subject: [PATCH 124/221] Comply with new member-delimiter-style rule Just `eslint --fix` with rule from https://github.com/matrix-org/eslint-plugin-matrix-org/pull/9 in place --- src/CallHandler.tsx | 6 ++-- src/CountlyAnalytics.ts | 2 +- src/SecurityManager.ts | 4 +-- src/Terms.ts | 10 +++--- .../structures/AutoHideScrollbar.tsx | 4 +-- src/components/structures/FilePanel.tsx | 2 +- src/components/structures/LoggedInView.tsx | 4 +-- src/components/structures/MatrixChat.tsx | 2 +- .../structures/NonUrgentToastContainer.tsx | 2 +- src/components/structures/RoomView.tsx | 2 +- .../structures/SpaceRoomDirectory.tsx | 2 +- src/components/structures/TimelinePanel.tsx | 2 +- .../structures/auth/Registration.tsx | 2 +- src/components/structures/auth/SoftLogout.tsx | 2 +- src/components/views/auth/PasswordLogin.tsx | 4 +-- .../views/dialogs/ConfirmUserActionDialog.tsx | 2 +- src/components/views/dialogs/InviteDialog.tsx | 18 +++++----- src/components/views/dialogs/TermsDialog.tsx | 6 ++-- .../views/elements/EventListSummary.tsx | 2 +- src/components/views/elements/ImageView.tsx | 34 +++++++++---------- .../elements/SpellCheckLanguagesDropdown.tsx | 10 +++--- src/components/views/emojipicker/Header.tsx | 2 +- .../messages/MKeyVerificationRequest.tsx | 2 +- src/components/views/messages/TextualBody.tsx | 2 +- src/components/views/rooms/AuxPanel.tsx | 26 +++++++------- src/components/views/rooms/EventTile.tsx | 4 +-- .../views/rooms/RoomListNumResults.tsx | 2 +- .../views/settings/SpellCheckSettings.tsx | 10 +++--- .../views/spaces/SpaceBasicSettings.tsx | 2 +- src/components/views/voip/AudioFeed.tsx | 2 +- src/components/views/voip/CallView.tsx | 22 ++++++------ src/components/views/voip/CallViewForRoom.tsx | 6 ++-- src/components/views/voip/VideoFeed.tsx | 6 ++-- src/customisations/Security.ts | 12 +++---- .../payloads/ComposerInsertPayload.ts | 2 +- src/effects/confetti/index.ts | 26 +++++++------- src/performance/index.ts | 8 ++--- src/stores/TypingStore.ts | 8 ++--- src/stores/WidgetEchoStore.ts | 4 +-- src/utils/objects.ts | 2 +- test/KeyBindingsManager-test.ts | 8 ++--- 41 files changed, 139 insertions(+), 139 deletions(-) diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx index cb54db3f8a..6e1e6ce83a 100644 --- a/src/CallHandler.tsx +++ b/src/CallHandler.tsx @@ -124,9 +124,9 @@ interface ThirdpartyLookupResponseFields { } interface ThirdpartyLookupResponse { - userid: string, - protocol: string, - fields: ThirdpartyLookupResponseFields, + userid: string; + protocol: string; + fields: ThirdpartyLookupResponseFields; } // Unlike 'CallType' in js-sdk, this one includes screen sharing diff --git a/src/CountlyAnalytics.ts b/src/CountlyAnalytics.ts index 3ad56fe3bf..d4a340ddaf 100644 --- a/src/CountlyAnalytics.ts +++ b/src/CountlyAnalytics.ts @@ -256,7 +256,7 @@ interface ICreateRoomEvent extends IEvent { num_users: number; is_encrypted: boolean; is_public: boolean; - } + }; } interface IJoinRoomEvent extends IEvent { diff --git a/src/SecurityManager.ts b/src/SecurityManager.ts index 6b372bba28..214047c4fa 100644 --- a/src/SecurityManager.ts +++ b/src/SecurityManager.ts @@ -42,8 +42,8 @@ let secretStorageBeingAccessed = false; let nonInteractive = false; let dehydrationCache: { - key?: Uint8Array, - keyInfo?: ISecretStorageKeyInfo, + key?: Uint8Array; + keyInfo?: ISecretStorageKeyInfo; } = {}; function isCachingAllowed(): boolean { diff --git a/src/Terms.ts b/src/Terms.ts index 3859cc1c73..0189810e7c 100644 --- a/src/Terms.ts +++ b/src/Terms.ts @@ -48,13 +48,13 @@ export interface Policy { } export type Policies = { - [policy: string]: Policy, + [policy: string]: Policy; }; export type TermsInteractionCallback = ( policiesAndServicePairs: { - service: Service, - policies: Policies, + service: Service; + policies: Policies; }[], agreedUrls: string[], extraClassNames?: string, @@ -180,8 +180,8 @@ export async function startTermsFlow( export function dialogTermsInteractionCallback( policiesAndServicePairs: { - service: Service, - policies: { [policy: string]: Policy }, + service: Service; + policies: { [policy: string]: Policy }; }[], agreedUrls: string[], extraClassNames?: string, diff --git a/src/components/structures/AutoHideScrollbar.tsx b/src/components/structures/AutoHideScrollbar.tsx index e8a9872b48..184d883dda 100644 --- a/src/components/structures/AutoHideScrollbar.tsx +++ b/src/components/structures/AutoHideScrollbar.tsx @@ -21,8 +21,8 @@ interface IProps extends Omit, "onScroll"> { className?: string; onScroll?: (event: Event) => void; onWheel?: (event: WheelEvent) => void; - style?: React.CSSProperties - tabIndex?: number, + style?: React.CSSProperties; + tabIndex?: number; wrappedRef?: (ref: HTMLDivElement) => void; } diff --git a/src/components/structures/FilePanel.tsx b/src/components/structures/FilePanel.tsx index 21ef0c4f31..22a0e1283c 100644 --- a/src/components/structures/FilePanel.tsx +++ b/src/components/structures/FilePanel.tsx @@ -37,7 +37,7 @@ import ResizeNotifier from '../../utils/ResizeNotifier'; interface IProps { roomId: string; onClose: () => void; - resizeNotifier: ResizeNotifier + resizeNotifier: ResizeNotifier; } interface IState { diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx index 65b26cb505..1f870da900 100644 --- a/src/components/structures/LoggedInView.tsx +++ b/src/components/structures/LoggedInView.tsx @@ -87,8 +87,8 @@ interface IProps { config: { piwik: { policyUrl: string; - }, - [key: string]: any, + }; + [key: string]: any; }; currentUserId?: string; currentGroupId?: string; diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index c1e0b8d7cb..a63767108f 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -203,7 +203,7 @@ interface IState { resizeNotifier: ResizeNotifier; serverConfig?: ValidatedServerConfig; ready: boolean; - threepidInvite?: IThreepidInvite, + threepidInvite?: IThreepidInvite; roomOobData?: object; pendingInitialSync?: boolean; justRegistered?: boolean; diff --git a/src/components/structures/NonUrgentToastContainer.tsx b/src/components/structures/NonUrgentToastContainer.tsx index b1424a2974..a2d419b4ba 100644 --- a/src/components/structures/NonUrgentToastContainer.tsx +++ b/src/components/structures/NonUrgentToastContainer.tsx @@ -24,7 +24,7 @@ interface IProps { } interface IState { - toasts: ComponentClass[], + toasts: ComponentClass[]; } @replaceableComponent("structures.NonUrgentToastContainer") diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 6428b7cdc2..2c8fc08dac 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -94,7 +94,7 @@ if (DEBUG) { } interface IProps { - threepidInvite: IThreepidInvite, + threepidInvite: IThreepidInvite; oobData?: IOOBData; resizeNotifier: ResizeNotifier; diff --git a/src/components/structures/SpaceRoomDirectory.tsx b/src/components/structures/SpaceRoomDirectory.tsx index e5c4372ab6..2ee0327420 100644 --- a/src/components/structures/SpaceRoomDirectory.tsx +++ b/src/components/structures/SpaceRoomDirectory.tsx @@ -58,7 +58,7 @@ export interface ISpaceSummaryRoom { avatar_url?: string; guest_can_join: boolean; name?: string; - num_joined_members: number + num_joined_members: number; room_id: string; topic?: string; world_readable: boolean; diff --git a/src/components/structures/TimelinePanel.tsx b/src/components/structures/TimelinePanel.tsx index e4c7d15596..8d5d733082 100644 --- a/src/components/structures/TimelinePanel.tsx +++ b/src/components/structures/TimelinePanel.tsx @@ -125,7 +125,7 @@ interface IProps { onReadMarkerUpdated?(): void; // callback which is called when we wish to paginate the timeline window. - onPaginationRequest?(timelineWindow: TimelineWindow, direction: string, size: number): Promise, + onPaginationRequest?(timelineWindow: TimelineWindow, direction: string, size: number): Promise; } interface IState { diff --git a/src/components/structures/auth/Registration.tsx b/src/components/structures/auth/Registration.tsx index f27bed2cc3..57b758091a 100644 --- a/src/components/structures/auth/Registration.tsx +++ b/src/components/structures/auth/Registration.tsx @@ -49,7 +49,7 @@ interface IProps { // for operations like uploading cross-signing keys). onLoggedIn(params: { userId: string; - deviceId: string + deviceId: string; homeserverUrl: string; identityServerUrl?: string; accessToken: string; diff --git a/src/components/structures/auth/SoftLogout.tsx b/src/components/structures/auth/SoftLogout.tsx index 7fb60a7b5d..3790028fea 100644 --- a/src/components/structures/auth/SoftLogout.tsx +++ b/src/components/structures/auth/SoftLogout.tsx @@ -49,7 +49,7 @@ interface IProps { fragmentAfterLogin?: string; // Called when the SSO login completes - onTokenLoginCompleted: () => void, + onTokenLoginCompleted: () => void; } interface IState { diff --git a/src/components/views/auth/PasswordLogin.tsx b/src/components/views/auth/PasswordLogin.tsx index 12f55a112c..a77dd0b683 100644 --- a/src/components/views/auth/PasswordLogin.tsx +++ b/src/components/views/auth/PasswordLogin.tsx @@ -52,8 +52,8 @@ interface IProps { interface IState { fieldValid: Partial>; - loginType: LoginField.Email | LoginField.MatrixId | LoginField.Phone, - password: "", + loginType: LoginField.Email | LoginField.MatrixId | LoginField.Phone; + password: ""; } enum LoginField { diff --git a/src/components/views/dialogs/ConfirmUserActionDialog.tsx b/src/components/views/dialogs/ConfirmUserActionDialog.tsx index 5cdb4c664b..78fae390b5 100644 --- a/src/components/views/dialogs/ConfirmUserActionDialog.tsx +++ b/src/components/views/dialogs/ConfirmUserActionDialog.tsx @@ -29,7 +29,7 @@ interface IProps { // group member object. Supply either this or 'member' groupMember: GroupMemberType; // needed if a group member is specified - matrixClient?: MatrixClient, + matrixClient?: MatrixClient; action: string; // eg. 'Ban' title: string; // eg. 'Ban this user?' diff --git a/src/components/views/dialogs/InviteDialog.tsx b/src/components/views/dialogs/InviteDialog.tsx index d9dcb8fe00..bbb5f24162 100644 --- a/src/components/views/dialogs/InviteDialog.tsx +++ b/src/components/views/dialogs/InviteDialog.tsx @@ -70,9 +70,9 @@ import GenericTextContextMenu from "../context_menus/GenericTextContextMenu"; /* eslint-disable camelcase */ interface IRecentUser { - userId: string, - user: RoomMember, - lastActive: number, + userId: string; + user: RoomMember; + lastActive: number; } export const KIND_DM = "dm"; @@ -330,16 +330,16 @@ interface IInviteDialogProps { // The kind of invite being performed. Assumed to be KIND_DM if // not provided. - kind: string, + kind: string; // The room ID this dialog is for. Only required for KIND_INVITE. - roomId: string, + roomId: string; // The call to transfer. Only required for KIND_CALL_TRANSFER. - call: MatrixCall, + call: MatrixCall; // Initial value to populate the filter with - initialText: string, + initialText: string; } interface IInviteDialogState { @@ -356,8 +356,8 @@ interface IInviteDialogState { consultFirst: boolean; // These two flags are used for the 'Go' button to communicate what is going on. - busy: boolean, - errorText: string, + busy: boolean; + errorText: string; } @replaceableComponent("views.dialogs.InviteDialog") diff --git a/src/components/views/dialogs/TermsDialog.tsx b/src/components/views/dialogs/TermsDialog.tsx index 818ac4b9e4..02a779743b 100644 --- a/src/components/views/dialogs/TermsDialog.tsx +++ b/src/components/views/dialogs/TermsDialog.tsx @@ -46,19 +46,19 @@ interface ITermsDialogProps { * Array of [Service, policies] pairs, where policies is the response from the * /terms endpoint for that service */ - policiesAndServicePairs: any[], + policiesAndServicePairs: any[]; /** * urls that the user has already agreed to */ - agreedUrls?: string[], + agreedUrls?: string[]; /** * Called with: * * success {bool} True if the user accepted any douments, false if cancelled * * agreedUrls {string[]} List of agreed URLs */ - onFinished: (success: boolean, agreedUrls?: string[]) => void, + onFinished: (success: boolean, agreedUrls?: string[]) => void; } interface IState { diff --git a/src/components/views/elements/EventListSummary.tsx b/src/components/views/elements/EventListSummary.tsx index ab647db9ed..681817ca86 100644 --- a/src/components/views/elements/EventListSummary.tsx +++ b/src/components/views/elements/EventListSummary.tsx @@ -29,7 +29,7 @@ interface IProps { // The minimum number of events needed to trigger summarisation threshold?: number; // Whether or not to begin with state.expanded=true - startExpanded?: boolean, + startExpanded?: boolean; // The list of room members for which to show avatars next to the summary summaryMembers?: RoomMember[]; // The text to show as the summary of this event list diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index 74538d2fa9..2628170f9c 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -44,31 +44,31 @@ const ZOOM_COEFFICIENT = 0.0025; const ZOOM_DISTANCE = 10; interface IProps { - src: string, // the source of the image being displayed - name?: string, // the main title ('name') for the image - link?: string, // the link (if any) applied to the name of the image - width?: number, // width of the image src in pixels - height?: number, // height of the image src in pixels - fileSize?: number, // size of the image src in bytes - onFinished(): void, // callback when the lightbox is dismissed + src: string; // the source of the image being displayed + name?: string; // the main title ('name') for the image + link?: string; // the link (if any) applied to the name of the image + width?: number; // width of the image src in pixels + height?: number; // height of the image src in pixels + fileSize?: number; // size of the image src in bytes + onFinished(): void; // callback when the lightbox is dismissed // the event (if any) that the Image is displaying. Used for event-specific stuff like // redactions, senders, timestamps etc. Other descriptors are taken from the explicit // properties above, which let us use lightboxes to display images which aren't associated // with events. - mxEvent: MatrixEvent, - permalinkCreator: RoomPermalinkCreator, + mxEvent: MatrixEvent; + permalinkCreator: RoomPermalinkCreator; } interface IState { - zoom: number, - minZoom: number, - maxZoom: number, - rotation: number, - translationX: number, - translationY: number, - moving: boolean, - contextMenuDisplayed: boolean, + zoom: number; + minZoom: number; + maxZoom: number; + rotation: number; + translationX: number; + translationY: number; + moving: boolean; + contextMenuDisplayed: boolean; } @replaceableComponent("views.elements.ImageView") diff --git a/src/components/views/elements/SpellCheckLanguagesDropdown.tsx b/src/components/views/elements/SpellCheckLanguagesDropdown.tsx index d10a599d95..1678bdb33a 100644 --- a/src/components/views/elements/SpellCheckLanguagesDropdown.tsx +++ b/src/components/views/elements/SpellCheckLanguagesDropdown.tsx @@ -30,14 +30,14 @@ function languageMatchesSearchQuery(query, language) { } interface SpellCheckLanguagesDropdownIProps { - className: string, - value: string, - onOptionChange(language: string), + className: string; + value: string; + onOptionChange(language: string); } interface SpellCheckLanguagesDropdownIState { - searchQuery: string, - languages: any, + searchQuery: string; + languages: any; } @replaceableComponent("views.elements.SpellCheckLanguagesDropdown") diff --git a/src/components/views/emojipicker/Header.tsx b/src/components/views/emojipicker/Header.tsx index 010801141a..ac39affdd9 100644 --- a/src/components/views/emojipicker/Header.tsx +++ b/src/components/views/emojipicker/Header.tsx @@ -25,7 +25,7 @@ import { replaceableComponent } from "../../../utils/replaceableComponent"; interface IProps { categories: ICategory[]; - onAnchorClick(id: CategoryKey): void + onAnchorClick(id: CategoryKey): void; } @replaceableComponent("views.emojipicker.Header") diff --git a/src/components/views/messages/MKeyVerificationRequest.tsx b/src/components/views/messages/MKeyVerificationRequest.tsx index 8b8eb25f6b..fc550845e2 100644 --- a/src/components/views/messages/MKeyVerificationRequest.tsx +++ b/src/components/views/messages/MKeyVerificationRequest.tsx @@ -28,7 +28,7 @@ import EventTileBubble from "./EventTileBubble"; import { replaceableComponent } from "../../../utils/replaceableComponent"; interface IProps { - mxEvent: MatrixEvent + mxEvent: MatrixEvent; } @replaceableComponent("views.messages.MKeyVerificationRequest") diff --git a/src/components/views/messages/TextualBody.tsx b/src/components/views/messages/TextualBody.tsx index f9e0f695ab..6ba018c512 100644 --- a/src/components/views/messages/TextualBody.tsx +++ b/src/components/views/messages/TextualBody.tsx @@ -67,7 +67,7 @@ interface IProps { replacingEventId?: string; /* callback for when our widget has loaded */ - onHeightChanged(): void, + onHeightChanged(): void; } interface IState { diff --git a/src/components/views/rooms/AuxPanel.tsx b/src/components/views/rooms/AuxPanel.tsx index 99286dfa07..f142328895 100644 --- a/src/components/views/rooms/AuxPanel.tsx +++ b/src/components/views/rooms/AuxPanel.tsx @@ -32,32 +32,32 @@ import { throttle } from 'lodash'; interface IProps { // js-sdk room object - room: Room, - userId: string, - showApps: boolean, // Render apps + room: Room; + userId: string; + showApps: boolean; // Render apps // maxHeight attribute for the aux panel and the video // therein - maxHeight: number, + maxHeight: number; // a callback which is called when the content of the aux panel changes // content in a way that is likely to make it change size. - onResize: () => void, - fullHeight: boolean, + onResize: () => void; + fullHeight: boolean; - resizeNotifier: ResizeNotifier, + resizeNotifier: ResizeNotifier; } interface Counter { - title: string, - value: number, - link: string, - severity: string, - stateKey: string, + title: string; + value: number; + link: string; + severity: string; + stateKey: string; } interface IState { - counters: Counter[], + counters: Counter[]; } @replaceableComponent("views.rooms.AuxPanel") diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index cebb631708..c9d1040433 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -287,10 +287,10 @@ interface IProps { permalinkCreator?: RoomPermalinkCreator; // Symbol of the root node - as?: string + as?: string; // whether or not to always show timestamps - alwaysShowTimestamps?: boolean + alwaysShowTimestamps?: boolean; } interface IState { diff --git a/src/components/views/rooms/RoomListNumResults.tsx b/src/components/views/rooms/RoomListNumResults.tsx index a05db89f61..95c8c6590f 100644 --- a/src/components/views/rooms/RoomListNumResults.tsx +++ b/src/components/views/rooms/RoomListNumResults.tsx @@ -22,7 +22,7 @@ import { useEventEmitter } from "../../../hooks/useEventEmitter"; import SpaceStore from "../../../stores/SpaceStore"; interface IProps { - onVisibilityChange?: () => void + onVisibilityChange?: () => void; } const RoomListNumResults: React.FC = ({ onVisibilityChange }) => { diff --git a/src/components/views/settings/SpellCheckSettings.tsx b/src/components/views/settings/SpellCheckSettings.tsx index 0876f07142..1858412dac 100644 --- a/src/components/views/settings/SpellCheckSettings.tsx +++ b/src/components/views/settings/SpellCheckSettings.tsx @@ -21,17 +21,17 @@ import { _t } from "../../../languageHandler"; import { replaceableComponent } from "../../../utils/replaceableComponent"; interface ExistingSpellCheckLanguageIProps { - language: string, - onRemoved(language: string), + language: string; + onRemoved(language: string); } interface SpellCheckLanguagesIProps { - languages: Array, - onLanguagesChange(languages: Array), + languages: Array; + onLanguagesChange(languages: Array); } interface SpellCheckLanguagesIState { - newLanguage: string, + newLanguage: string; } export class ExistingSpellCheckLanguage extends React.Component { diff --git a/src/components/views/spaces/SpaceBasicSettings.tsx b/src/components/views/spaces/SpaceBasicSettings.tsx index f6b98eedec..6d2cc1f5db 100644 --- a/src/components/views/spaces/SpaceBasicSettings.tsx +++ b/src/components/views/spaces/SpaceBasicSettings.tsx @@ -23,7 +23,7 @@ import Field from "../elements/Field"; interface IProps { avatarUrl?: string; avatarDisabled?: boolean; - name?: string, + name?: string; nameDisabled?: boolean; topic?: string; topicDisabled?: boolean; diff --git a/src/components/views/voip/AudioFeed.tsx b/src/components/views/voip/AudioFeed.tsx index 272d8a06a3..a2ab760c86 100644 --- a/src/components/views/voip/AudioFeed.tsx +++ b/src/components/views/voip/AudioFeed.tsx @@ -20,7 +20,7 @@ import { logger } from 'matrix-js-sdk/src/logger'; import MediaDeviceHandler, { MediaDeviceHandlerEvent } from "../../../MediaDeviceHandler"; interface IProps { - feed: CallFeed, + feed: CallFeed; } export default class AudioFeed extends React.Component { diff --git a/src/components/views/voip/CallView.tsx b/src/components/views/voip/CallView.tsx index c522116e0a..dd0e8cb138 100644 --- a/src/components/views/voip/CallView.tsx +++ b/src/components/views/voip/CallView.tsx @@ -35,10 +35,10 @@ import { replaceableComponent } from "../../../utils/replaceableComponent"; interface IProps { // The call for us to display - call: MatrixCall, + call: MatrixCall; // Another ongoing call to display information about - secondaryCall?: MatrixCall, + secondaryCall?: MatrixCall; // a callback which is called when the content in the CallView changes // in a way that is likely to cause a resize. @@ -52,15 +52,15 @@ interface IProps { } interface IState { - isLocalOnHold: boolean, - isRemoteOnHold: boolean, - micMuted: boolean, - vidMuted: boolean, - callState: CallState, - controlsVisible: boolean, - showMoreMenu: boolean, - showDialpad: boolean, - feeds: CallFeed[], + isLocalOnHold: boolean; + isRemoteOnHold: boolean; + micMuted: boolean; + vidMuted: boolean; + callState: CallState; + controlsVisible: boolean; + showMoreMenu: boolean; + showDialpad: boolean; + feeds: CallFeed[]; } function getFullScreenElement() { diff --git a/src/components/views/voip/CallViewForRoom.tsx b/src/components/views/voip/CallViewForRoom.tsx index 9557fe0e7e..a5aa3e7734 100644 --- a/src/components/views/voip/CallViewForRoom.tsx +++ b/src/components/views/voip/CallViewForRoom.tsx @@ -25,16 +25,16 @@ import { replaceableComponent } from "../../../utils/replaceableComponent"; interface IProps { // What room we should display the call for - roomId: string, + roomId: string; // maxHeight style attribute for the video panel maxVideoHeight?: number; - resizeNotifier: ResizeNotifier, + resizeNotifier: ResizeNotifier; } interface IState { - call: MatrixCall, + call: MatrixCall; } /* diff --git a/src/components/views/voip/VideoFeed.tsx b/src/components/views/voip/VideoFeed.tsx index 2f88abe6fb..e5461eb1b4 100644 --- a/src/components/views/voip/VideoFeed.tsx +++ b/src/components/views/voip/VideoFeed.tsx @@ -24,9 +24,9 @@ import MemberAvatar from "../avatars/MemberAvatar"; import { replaceableComponent } from "../../../utils/replaceableComponent"; interface IProps { - call: MatrixCall, + call: MatrixCall; - feed: CallFeed, + feed: CallFeed; // Whether this call view is for picture-in-picture mode // otherwise, it's the larger call view when viewing the room the call is in. @@ -36,7 +36,7 @@ interface IProps { // a callback which is called when the video element is resized // due to a change in video metadata - onResize?: (e: Event) => void, + onResize?: (e: Event) => void; } interface IState { diff --git a/src/customisations/Security.ts b/src/customisations/Security.ts index e215c5cb24..c2262e5f71 100644 --- a/src/customisations/Security.ts +++ b/src/customisations/Security.ts @@ -69,11 +69,11 @@ function setupEncryptionNeeded(kind: SetupEncryptionKind): boolean { export interface ISecurityCustomisations { examineLoginResponse?: typeof examineLoginResponse; persistCredentials?: typeof persistCredentials; - createSecretStorageKey?: typeof createSecretStorageKey, - getSecretStorageKey?: typeof getSecretStorageKey, - catchAccessSecretStorageError?: typeof catchAccessSecretStorageError, - setupEncryptionNeeded?: typeof setupEncryptionNeeded, - getDehydrationKey?: typeof getDehydrationKey, + createSecretStorageKey?: typeof createSecretStorageKey; + getSecretStorageKey?: typeof getSecretStorageKey; + catchAccessSecretStorageError?: typeof catchAccessSecretStorageError; + setupEncryptionNeeded?: typeof setupEncryptionNeeded; + getDehydrationKey?: typeof getDehydrationKey; /** * When false, disables the post-login UI from showing. If there's @@ -83,7 +83,7 @@ export interface ISecurityCustomisations { * encryption is set up some other way which would circumvent the default * UI, such as by presenting alternative UI. */ - SHOW_ENCRYPTION_SETUP_UI?: boolean, // default true + SHOW_ENCRYPTION_SETUP_UI?: boolean; // default true } // A real customisation module will define and export one or more of the diff --git a/src/dispatcher/payloads/ComposerInsertPayload.ts b/src/dispatcher/payloads/ComposerInsertPayload.ts index 9702855432..ea5d8a0c53 100644 --- a/src/dispatcher/payloads/ComposerInsertPayload.ts +++ b/src/dispatcher/payloads/ComposerInsertPayload.ts @@ -20,7 +20,7 @@ import { ActionPayload } from "../payloads"; import { Action } from "../actions"; interface IBaseComposerInsertPayload extends ActionPayload { - action: Action.ComposerInsert, + action: Action.ComposerInsert; } interface IComposerInsertMentionPayload extends IBaseComposerInsertPayload { diff --git a/src/effects/confetti/index.ts b/src/effects/confetti/index.ts index 53e5dda5d2..ae2bb822c2 100644 --- a/src/effects/confetti/index.ts +++ b/src/effects/confetti/index.ts @@ -20,34 +20,34 @@ export type ConfettiOptions = { /** * max confetti count */ - maxCount: number, + maxCount: number; /** * particle animation speed */ - speed: number, + speed: number; /** * the confetti animation frame interval in milliseconds */ - frameInterval: number, + frameInterval: number; /** * the alpha opacity of the confetti (between 0 and 1, where 1 is opaque and 0 is invisible) */ - alpha: number, + alpha: number; /** * use gradient instead of solid particle color */ - gradient: boolean, + gradient: boolean; }; type ConfettiParticle = { - color: string, - color2: string, - x: number, - y: number, - diameter: number, - tilt: number, - tiltAngleIncrement: number, - tiltAngle: number, + color: string; + color2: string; + x: number; + y: number; + diameter: number; + tilt: number; + tiltAngleIncrement: number; + tiltAngle: number; }; export const DefaultOptions: ConfettiOptions = { diff --git a/src/performance/index.ts b/src/performance/index.ts index 1e24839370..cb808f9173 100644 --- a/src/performance/index.ts +++ b/src/performance/index.ts @@ -17,15 +17,15 @@ limitations under the License. import { PerformanceEntryNames } from "./entry-names"; interface GetEntriesOptions { - name?: string, - type?: string, + name?: string; + type?: string; } type PerformanceCallbackFunction = (entry: PerformanceEntry[]) => void; interface PerformanceDataListener { - entryNames?: string[], - callback: PerformanceCallbackFunction + entryNames?: string[]; + callback: PerformanceCallbackFunction; } export default class PerformanceMonitor { diff --git a/src/stores/TypingStore.ts b/src/stores/TypingStore.ts index 447f41c7ae..9781c93eb4 100644 --- a/src/stores/TypingStore.ts +++ b/src/stores/TypingStore.ts @@ -27,10 +27,10 @@ const TYPING_SERVER_TIMEOUT = 30000; export default class TypingStore { private typingStates: { [roomId: string]: { - isTyping: boolean, - userTimer: Timer, - serverTimer: Timer, - }, + isTyping: boolean; + userTimer: Timer; + serverTimer: Timer; + }; }; constructor() { diff --git a/src/stores/WidgetEchoStore.ts b/src/stores/WidgetEchoStore.ts index 0b0be50541..d3ef9df023 100644 --- a/src/stores/WidgetEchoStore.ts +++ b/src/stores/WidgetEchoStore.ts @@ -26,8 +26,8 @@ import { WidgetType } from "../widgets/WidgetType"; class WidgetEchoStore extends EventEmitter { private roomWidgetEcho: { [roomId: string]: { - [widgetId: string]: IWidget, - }, + [widgetId: string]: IWidget; + }; }; constructor() { diff --git a/src/utils/objects.ts b/src/utils/objects.ts index 561e68e8c5..c2ee6ce100 100644 --- a/src/utils/objects.ts +++ b/src/utils/objects.ts @@ -152,7 +152,7 @@ export function objectClone(obj: O): O { export function objectFromEntries(entries: Iterable<[K, V]>): {[k: K]: V} { const obj: { // @ts-ignore - same as return type - [k: K]: V} = {}; + [k: K]: V;} = {}; for (const e of entries) { // @ts-ignore - same as return type obj[e[0]] = e[1]; diff --git a/test/KeyBindingsManager-test.ts b/test/KeyBindingsManager-test.ts index 694efac7b5..eab1bea2b0 100644 --- a/test/KeyBindingsManager-test.ts +++ b/test/KeyBindingsManager-test.ts @@ -17,10 +17,10 @@ limitations under the License. import { isKeyComboMatch, KeyCombo } from '../src/KeyBindingsManager'; function mockKeyEvent(key: string, modifiers?: { - ctrlKey?: boolean, - altKey?: boolean, - shiftKey?: boolean, - metaKey?: boolean + ctrlKey?: boolean; + altKey?: boolean; + shiftKey?: boolean; + metaKey?: boolean; }): KeyboardEvent { return { key, From 1fe0c2ab881f579453c576b19ee017a8433837fb Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 1 Jul 2021 23:45:08 +0100 Subject: [PATCH 125/221] Bump eslint plugin version --- yarn.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yarn.lock b/yarn.lock index fcb9a46b37..c8c3315855 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3232,7 +3232,7 @@ eslint-config-google@^0.14.0: "eslint-plugin-matrix-org@github:matrix-org/eslint-plugin-matrix-org#main": version "0.3.2" - resolved "https://codeload.github.com/matrix-org/eslint-plugin-matrix-org/tar.gz/28d392822533a7468be0dd806d0a4ba573a45d74" + resolved "https://codeload.github.com/matrix-org/eslint-plugin-matrix-org/tar.gz/e8197938dca66849ffdac4baca7c05275df12835" eslint-plugin-react-hooks@^4.2.0: version "4.2.0" From 5d1a1b46f3d080c2c836e8c9603cc14f8ff1d7c3 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 1 Jul 2021 23:48:52 +0100 Subject: [PATCH 126/221] Fix instances of the Edit Message Composer's save button being wrongly disabled --- .../views/rooms/BasicMessageComposer.tsx | 3 ++ .../views/rooms/EditMessageComposer.tsx | 39 +++++++------------ 2 files changed, 18 insertions(+), 24 deletions(-) diff --git a/src/components/views/rooms/BasicMessageComposer.tsx b/src/components/views/rooms/BasicMessageComposer.tsx index d317aa409b..3258674cf6 100644 --- a/src/components/views/rooms/BasicMessageComposer.tsx +++ b/src/components/views/rooms/BasicMessageComposer.tsx @@ -711,6 +711,7 @@ export default class BasicMessageEditor extends React.Component } public insertMention(userId: string): void { + this.modifiedFlag = true; const { model } = this.props; const { partCreator } = model; const member = this.props.room.getMember(userId); @@ -729,6 +730,7 @@ export default class BasicMessageEditor extends React.Component } public insertQuotedMessage(event: MatrixEvent): void { + this.modifiedFlag = true; const { model } = this.props; const { partCreator } = model; const quoteParts = parseEvent(event, partCreator, { isQuotedMessage: true }); @@ -744,6 +746,7 @@ export default class BasicMessageEditor extends React.Component } public insertPlaintext(text: string): void { + this.modifiedFlag = true; const { model } = this.props; const { partCreator } = model; const caret = this.getCaret(); diff --git a/src/components/views/rooms/EditMessageComposer.tsx b/src/components/views/rooms/EditMessageComposer.tsx index 4e51a0105b..fea6499dd8 100644 --- a/src/components/views/rooms/EditMessageComposer.tsx +++ b/src/components/views/rooms/EditMessageComposer.tsx @@ -131,11 +131,12 @@ export default class EditMessageComposer extends React.Component super(props); this.context = context; // otherwise React will only set it prior to render due to type def above + const isRestored = this.createEditorModel(); + const ev = this.props.editState.getEvent(); this.state = { - saveDisabled: true, + saveDisabled: !isRestored || !this.isContentModified(createEditContent(this.model, ev)["m.new_content"]), }; - this.createEditorModel(); window.addEventListener("beforeunload", this.saveStoredEditorState); this.dispatcherRef = dis.register(this.onAction); } @@ -230,12 +231,12 @@ export default class EditMessageComposer extends React.Component } } - private saveStoredEditorState(): void { + private saveStoredEditorState = (): void => { const item = SendHistoryManager.createItem(this.model); this.clearPreviousEdit(); localStorage.setItem(this.editorRoomKey, this.props.editState.getEvent().getId()); localStorage.setItem(this.editorStateKey, JSON.stringify(item)); - } + }; private isSlashCommand(): boolean { const parts = this.model.parts; @@ -256,10 +257,9 @@ export default class EditMessageComposer extends React.Component private isContentModified(newContent: IContent): boolean { // if nothing has changed then bail const oldContent = this.props.editState.getEvent().getContent(); - if (!this.editorRef.current?.isModified() || - (oldContent["msgtype"] === newContent["msgtype"] && oldContent["body"] === newContent["body"] && + if (oldContent["msgtype"] === newContent["msgtype"] && oldContent["body"] === newContent["body"] && oldContent["format"] === newContent["format"] && - oldContent["formatted_body"] === newContent["formatted_body"])) { + oldContent["formatted_body"] === newContent["formatted_body"]) { return false; } return true; @@ -410,36 +410,27 @@ export default class EditMessageComposer extends React.Component dis.unregister(this.dispatcherRef); } - private createEditorModel(): void { + private createEditorModel(): boolean { const { editState } = this.props; const room = this.getRoom(); const partCreator = new CommandPartCreator(room, this.context); + let parts; + let isRestored = false; if (editState.hasEditorState()) { // if restoring state from a previous editor, // restore serialized parts from the state parts = editState.getSerializedParts().map(p => partCreator.deserializePart(p)); } else { - //otherwise, either restore serialized parts from localStorage or parse the body of the event - parts = this.restoreStoredEditorState(partCreator) || parseEvent(editState.getEvent(), partCreator); + // otherwise, either restore serialized parts from localStorage or parse the body of the event + const restoredParts = this.restoreStoredEditorState(partCreator); + parts = restoredParts || parseEvent(editState.getEvent(), partCreator); + isRestored = !!restoredParts; } this.model = new EditorModel(parts, partCreator); this.saveStoredEditorState(); - } - private getInitialCaretPosition(): CaretPosition { - const { editState } = this.props; - let caretPosition; - if (editState.hasEditorState() && editState.getCaret()) { - // if restoring state from a previous editor, - // restore caret position from the state - const caret = editState.getCaret(); - caretPosition = this.model.positionForOffset(caret.offset, caret.atNodeEnd); - } else { - // otherwise, set it at the end - caretPosition = this.model.getPositionAtEnd(); - } - return caretPosition; + return isRestored; } private onChange = (): void => { From a4fe2f143f84ff7502cc57b2c7db82a879fa21a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 2 Jul 2021 12:12:41 +0200 Subject: [PATCH 127/221] First batch of burning MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/Analytics.tsx | 3 +-- src/AsyncWrapper.tsx | 7 +++---- src/ContentMessages.tsx | 9 ++++----- src/Lifecycle.ts | 13 ++++--------- src/MatrixClientPeg.ts | 4 +--- src/Notifier.ts | 3 +-- src/RoomInvite.tsx | 6 +----- src/SecurityManager.ts | 4 +--- src/Terms.ts | 3 +-- src/accessibility/KeyboardShortcuts.tsx | 3 +-- src/createRoom.ts | 8 +++----- src/verification.ts | 4 +--- 12 files changed, 22 insertions(+), 45 deletions(-) diff --git a/src/Analytics.tsx b/src/Analytics.tsx index 8c82639b5f..6a1608b63f 100644 --- a/src/Analytics.tsx +++ b/src/Analytics.tsx @@ -21,7 +21,7 @@ import { getCurrentLanguage, _t, _td, IVariables } from './languageHandler'; import PlatformPeg from './PlatformPeg'; import SdkConfig from './SdkConfig'; import Modal from './Modal'; -import * as sdk from './index'; +import ErrorDialog from './components/views/dialogs/ErrorDialog'; const hashRegex = /#\/(groups?|room|user|settings|register|login|forgot_password|home|directory)/; const hashVarRegex = /#\/(group|room|user)\/.*$/; @@ -390,7 +390,6 @@ export class Analytics { { expl: _td('Your device resolution'), value: resolution }, ]; - const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog'); Modal.createTrackedDialog('Analytics Details', '', ErrorDialog, { title: _t('Analytics'), description:

diff --git a/src/AsyncWrapper.tsx b/src/AsyncWrapper.tsx index 3bbef71093..5d3531be62 100644 --- a/src/AsyncWrapper.tsx +++ b/src/AsyncWrapper.tsx @@ -16,9 +16,11 @@ limitations under the License. import React, { ComponentType } from "react"; -import * as sdk from './index'; import { _t } from './languageHandler'; import { IDialogProps } from "./components/views/dialogs/IDialogProps"; +import BaseDialog from "./components/views/dialogs/BaseDialog"; +import Spinner from "./components/views/elements/Spinner"; +import DialogButtons from "./components/views/elements/DialogButtons"; type AsyncImport = { default: T }; @@ -77,8 +79,6 @@ export default class AsyncWrapper extends React.Component { const Component = this.state.component; return ; } else if (this.state.error) { - const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); - const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); return { _t("Unable to load! Check your network connectivity and try again.") } { ; } else { // show a spinner until the component is loaded. - const Spinner = sdk.getComponent("elements.Spinner"); return ; } } diff --git a/src/ContentMessages.tsx b/src/ContentMessages.tsx index 1b8a3be37e..dd2002dd9a 100644 --- a/src/ContentMessages.tsx +++ b/src/ContentMessages.tsx @@ -21,7 +21,6 @@ import { encode } from "blurhash"; import { MatrixClient } from "matrix-js-sdk/src/client"; import dis from './dispatcher/dispatcher'; -import * as sdk from './index'; import { _t } from './languageHandler'; import Modal from './Modal'; import RoomViewStore from './stores/RoomViewStore'; @@ -40,6 +39,10 @@ import { } from "./dispatcher/payloads/UploadPayload"; import { IUpload } from "./models/IUpload"; import { IImageInfo } from "matrix-js-sdk/src/@types/partials"; +import QuestionDialog from "./components/views/dialogs/QuestionDialog"; +import ErrorDialog from "./components/views/dialogs/ErrorDialog"; +import UploadConfirmDialog from "./components/views/dialogs/UploadConfirmDialog"; +import UploadFailureDialog from "./components/views/dialogs/UploadFailureDialog"; const MAX_WIDTH = 800; const MAX_HEIGHT = 600; @@ -419,7 +422,6 @@ export default class ContentMessages { const isQuoting = Boolean(RoomViewStore.getQuotingEvent()); if (isQuoting) { - const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); const { finished } = Modal.createTrackedDialog<[boolean]>('Upload Reply Warning', '', QuestionDialog, { title: _t('Replying With Files'), description: ( @@ -453,7 +455,6 @@ export default class ContentMessages { } if (tooBigFiles.length > 0) { - const UploadFailureDialog = sdk.getComponent("dialogs.UploadFailureDialog"); const { finished } = Modal.createTrackedDialog<[boolean]>('Upload Failure', '', UploadFailureDialog, { badFiles: tooBigFiles, totalFiles: files.length, @@ -463,7 +464,6 @@ export default class ContentMessages { if (!shouldContinue) return; } - const UploadConfirmDialog = sdk.getComponent("dialogs.UploadConfirmDialog"); let uploadAll = false; // Promise to complete before sending next file into room, used for synchronisation of file-sending // to match the order the files were specified in @@ -606,7 +606,6 @@ export default class ContentMessages { { fileName: upload.fileName }, ); } - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createTrackedDialog('Upload failed', '', ErrorDialog, { title: _t('Upload Failed'), description: desc, diff --git a/src/Lifecycle.ts b/src/Lifecycle.ts index 76dee5ab55..61ded93833 100644 --- a/src/Lifecycle.ts +++ b/src/Lifecycle.ts @@ -33,7 +33,6 @@ import Presence from './Presence'; import dis from './dispatcher/dispatcher'; import DMRoomMap from './utils/DMRoomMap'; import Modal from './Modal'; -import * as sdk from './index'; import ActiveWidgetStore from './stores/ActiveWidgetStore'; import PlatformPeg from "./PlatformPeg"; import { sendLoginRequest } from "./Login"; @@ -52,6 +51,10 @@ import CallHandler from './CallHandler'; import LifecycleCustomisations from "./customisations/Lifecycle"; import ErrorDialog from "./components/views/dialogs/ErrorDialog"; import { _t } from "./languageHandler"; +import LazyLoadingResyncDialog from "./components/views/dialogs/LazyLoadingResyncDialog"; +import LazyLoadingDisabledDialog from "./components/views/dialogs/LazyLoadingDisabledDialog"; +import SessionRestoreErrorDialog from "./components/views/dialogs/SessionRestoreErrorDialog"; +import StorageEvictedDialog from "./components/views/dialogs/StorageEvictedDialog"; const HOMESERVER_URL_KEY = "mx_hs_url"; const ID_SERVER_URL_KEY = "mx_is_url"; @@ -238,8 +241,6 @@ export function handleInvalidStoreError(e: InvalidStoreError): Promise { return Promise.resolve().then(() => { const lazyLoadEnabled = e.value; if (lazyLoadEnabled) { - const LazyLoadingResyncDialog = - sdk.getComponent("views.dialogs.LazyLoadingResyncDialog"); return new Promise((resolve) => { Modal.createDialog(LazyLoadingResyncDialog, { onFinished: resolve, @@ -250,8 +251,6 @@ export function handleInvalidStoreError(e: InvalidStoreError): Promise { // between LL/non-LL version on same host. // as disabling LL when previously enabled // is a strong indicator of this (/develop & /app) - const LazyLoadingDisabledDialog = - sdk.getComponent("views.dialogs.LazyLoadingDisabledDialog"); return new Promise((resolve) => { Modal.createDialog(LazyLoadingDisabledDialog, { onFinished: resolve, @@ -451,9 +450,6 @@ export async function restoreFromLocalStorage(opts?: { ignoreGuest?: boolean }): async function handleLoadSessionFailure(e: Error): Promise { console.error("Unable to load session", e); - const SessionRestoreErrorDialog = - sdk.getComponent('views.dialogs.SessionRestoreErrorDialog'); - const modal = Modal.createTrackedDialog('Session Restore Error', '', SessionRestoreErrorDialog, { error: e.message, }); @@ -612,7 +608,6 @@ async function doSetLoggedIn( } function showStorageEvictedDialog(): Promise { - const StorageEvictedDialog = sdk.getComponent('views.dialogs.StorageEvictedDialog'); return new Promise(resolve => { Modal.createTrackedDialog('Storage evicted', '', StorageEvictedDialog, { onFinished: resolve, diff --git a/src/MatrixClientPeg.ts b/src/MatrixClientPeg.ts index cb6cb5c65c..db8401baba 100644 --- a/src/MatrixClientPeg.ts +++ b/src/MatrixClientPeg.ts @@ -23,7 +23,6 @@ import { MemoryStore } from 'matrix-js-sdk/src/store/memory'; import * as utils from 'matrix-js-sdk/src/utils'; import { EventTimeline } from 'matrix-js-sdk/src/models/event-timeline'; import { EventTimelineSet } from 'matrix-js-sdk/src/models/event-timeline-set'; -import * as sdk from './index'; import createMatrixClient from './utils/createMatrixClient'; import SettingsStore from './settings/SettingsStore'; import MatrixActionCreators from './actions/MatrixActionCreators'; @@ -35,6 +34,7 @@ import IdentityAuthClient from './IdentityAuthClient'; import { crossSigningCallbacks, tryToUnlockSecretStorageWithDehydrationKey } from './SecurityManager'; import { SHOW_QR_CODE_METHOD } from "matrix-js-sdk/src/crypto/verification/QRCode"; import SecurityCustomisations from "./customisations/Security"; +import CryptoStoreTooNewDialog from ".components/views/dialogs/CryptoStoreTooNewDialog"; export interface IMatrixClientCreds { homeserverUrl: string; @@ -219,8 +219,6 @@ class _MatrixClientPeg implements IMatrixClientPeg { } catch (e) { if (e && e.name === 'InvalidCryptoStoreError') { // The js-sdk found a crypto DB too new for it to use - const CryptoStoreTooNewDialog = - sdk.getComponent("views.dialogs.CryptoStoreTooNewDialog"); Modal.createDialog(CryptoStoreTooNewDialog); } // this can happen for a number of reasons, the most likely being diff --git a/src/Notifier.ts b/src/Notifier.ts index 2335dc59ac..415adcafc8 100644 --- a/src/Notifier.ts +++ b/src/Notifier.ts @@ -27,7 +27,6 @@ import * as TextForEvent from './TextForEvent'; import Analytics from './Analytics'; import * as Avatar from './Avatar'; import dis from './dispatcher/dispatcher'; -import * as sdk from './index'; import { _t } from './languageHandler'; import Modal from './Modal'; import SettingsStore from "./settings/SettingsStore"; @@ -37,6 +36,7 @@ import { isPushNotifyDisabled } from "./settings/controllers/NotificationControl import RoomViewStore from "./stores/RoomViewStore"; import UserActivity from "./UserActivity"; import { mediaFromMxc } from "./customisations/Media"; +import ErrorDialog from "./components/views/dialogs/ErrorDialog"; /* * Dispatches: @@ -240,7 +240,6 @@ export const Notifier = { ? _t('%(brand)s does not have permission to send you notifications - ' + 'please check your browser settings', { brand }) : _t('%(brand)s was not given permission to send notifications - please try again', { brand }); - const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog'); Modal.createTrackedDialog('Unable to enable Notifications', result, ErrorDialog, { title: _t('Unable to enable Notifications'), description, diff --git a/src/RoomInvite.tsx b/src/RoomInvite.tsx index c7f377b6e8..7d093f4092 100644 --- a/src/RoomInvite.tsx +++ b/src/RoomInvite.tsx @@ -22,13 +22,13 @@ import { User } from "matrix-js-sdk/src/models/user"; import { MatrixClientPeg } from './MatrixClientPeg'; import MultiInviter, { CompletionStates } from './utils/MultiInviter'; import Modal from './Modal'; -import * as sdk from './'; import { _t } from './languageHandler'; import InviteDialog, { KIND_DM, KIND_INVITE, Member } from "./components/views/dialogs/InviteDialog"; import CommunityPrototypeInviteDialog from "./components/views/dialogs/CommunityPrototypeInviteDialog"; import { CommunityPrototypeStore } from "./stores/CommunityPrototypeStore"; import BaseAvatar from "./components/views/avatars/BaseAvatar"; import { mediaFromMxc } from "./customisations/Media"; +import ErrorDialog from "./components/views/dialogs/ErrorDialog"; export interface IInviteResult { states: CompletionStates; @@ -51,7 +51,6 @@ export function inviteMultipleToRoom(roomId: string, addresses: string[]): Promi export function showStartChatInviteDialog(initialText = ""): void { // This dialog handles the room creation internally - we don't need to worry about it. - const InviteDialog = sdk.getComponent("dialogs.InviteDialog"); Modal.createTrackedDialog( 'Start DM', '', InviteDialog, { kind: KIND_DM, initialText }, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true, @@ -111,7 +110,6 @@ export function inviteUsersToRoom(roomId: string, userIds: string[]): Promise { console.error(err.stack); - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createTrackedDialog('Failed to invite', '', ErrorDialog, { title: _t("Failed to invite"), description: ((err && err.message) ? err.message : _t("Operation failed")), @@ -131,7 +129,6 @@ export function showAnyInviteErrors( // Just get the first message because there was a fatal problem on the first // user. This usually means that no other users were attempted, making it // pointless for us to list who failed exactly. - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createTrackedDialog('Failed to invite users to the room', '', ErrorDialog, { title: _t("Failed to invite users to the room:", { roomName: room.name }), description: inviter.getErrorText(failedUsers[0]), @@ -178,7 +175,6 @@ export function showAnyInviteErrors(
; - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createTrackedDialog("Some invites could not be sent", "", ErrorDialog, { title: _t("Some invites couldn't be sent"), description, diff --git a/src/SecurityManager.ts b/src/SecurityManager.ts index 214047c4fa..8479eadddb 100644 --- a/src/SecurityManager.ts +++ b/src/SecurityManager.ts @@ -17,7 +17,6 @@ limitations under the License. import { ICryptoCallbacks, ISecretStorageKeyInfo } from 'matrix-js-sdk/src/matrix'; import { MatrixClient } from 'matrix-js-sdk/src/client'; import Modal from './Modal'; -import * as sdk from './index'; import { MatrixClientPeg } from './MatrixClientPeg'; import { deriveKey } from 'matrix-js-sdk/src/crypto/key_passphrase'; import { decodeRecoveryKey } from 'matrix-js-sdk/src/crypto/recoverykey'; @@ -29,6 +28,7 @@ import RestoreKeyBackupDialog from './components/views/dialogs/security/RestoreK import SettingsStore from "./settings/SettingsStore"; import SecurityCustomisations from "./customisations/Security"; import { DeviceTrustLevel } from 'matrix-js-sdk/src/crypto/CrossSigning'; +import InteractiveAuthDialog from "./components/views/dialogs/InteractiveAuthDialog"; // This stores the secret storage private keys in memory for the JS SDK. This is // only meant to act as a cache to avoid prompting the user multiple times @@ -68,7 +68,6 @@ export class AccessCancelledError extends Error { } async function confirmToDismiss(): Promise { - const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); const [sure] = await Modal.createDialog(QuestionDialog, { title: _t("Cancel entering passphrase?"), description: _t("Are you sure you want to cancel entering passphrase?"), @@ -354,7 +353,6 @@ export async function accessSecretStorage(func = async () => { }, forceReset = f throw new Error("Secret storage creation canceled"); } } else { - const InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog"); await cli.bootstrapCrossSigning({ authUploadDeviceSigningKeys: async (makeRequest) => { const { finished } = Modal.createTrackedDialog( diff --git a/src/Terms.ts b/src/Terms.ts index 0189810e7c..5a6d96b798 100644 --- a/src/Terms.ts +++ b/src/Terms.ts @@ -17,8 +17,8 @@ limitations under the License. import classNames from 'classnames'; import { MatrixClientPeg } from './MatrixClientPeg'; -import * as sdk from '.'; import Modal from './Modal'; +import TermsDialog from './components/views/dialogs/TermsDialog'; export class TermsNotSignedError extends Error {} @@ -188,7 +188,6 @@ export function dialogTermsInteractionCallback( ): Promise { return new Promise((resolve, reject) => { console.log("Terms that need agreement", policiesAndServicePairs); - const TermsDialog = sdk.getComponent("views.dialogs.TermsDialog"); Modal.createTrackedDialog('Terms of Service', '', TermsDialog, { policiesAndServicePairs, diff --git a/src/accessibility/KeyboardShortcuts.tsx b/src/accessibility/KeyboardShortcuts.tsx index 25c41f9db5..c5cf85facd 100644 --- a/src/accessibility/KeyboardShortcuts.tsx +++ b/src/accessibility/KeyboardShortcuts.tsx @@ -17,10 +17,10 @@ limitations under the License. import * as React from "react"; import classNames from "classnames"; -import * as sdk from "../index"; import Modal from "../Modal"; import { _t, _td } from "../languageHandler"; import { isMac, Key } from "../Keyboard"; +import InfoDialog from "../components/views/dialogs/InfoDialog"; // TS: once languageHandler is TS we can probably inline this into the enum _td("Navigation"); @@ -375,7 +375,6 @@ export const toggleDialog = () => {
; }); - const InfoDialog = sdk.getComponent('dialogs.InfoDialog'); activeModal = Modal.createTrackedDialog("Keyboard Shortcuts", "", InfoDialog, { className: "mx_KeyboardShortcutsDialog", title: _t("Keyboard Shortcuts"), diff --git a/src/createRoom.ts b/src/createRoom.ts index 94efecaa6c..afbeb7fdb9 100644 --- a/src/createRoom.ts +++ b/src/createRoom.ts @@ -21,7 +21,6 @@ import { EventType } from "matrix-js-sdk/src/@types/event"; import { MatrixClientPeg } from './MatrixClientPeg'; import Modal from './Modal'; -import * as sdk from './index'; import { _t } from './languageHandler'; import dis from "./dispatcher/dispatcher"; import * as Rooms from "./Rooms"; @@ -37,6 +36,8 @@ import { makeSpaceParentEvent } from "./utils/space"; import { Action } from "./dispatcher/actions"; import { ICreateRoomOpts } from "matrix-js-sdk/src/@types/requests"; import { Preset, Visibility } from "matrix-js-sdk/src/@types/partials"; +import ErrorDialog from "./components/views/dialogs/ErrorDialog"; +import Spinner from "./components/views/elements/Spinner"; // we define a number of interfaces which take their names from the js-sdk /* eslint-disable camelcase */ @@ -80,9 +81,6 @@ export default function createRoom(opts: IOpts): Promise { const startTime = CountlyAnalytics.getTimestamp(); - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - const Loader = sdk.getComponent("elements.Spinner"); - const client = MatrixClientPeg.get(); if (client.isGuest()) { dis.dispatch({ action: 'require_registration' }); @@ -153,7 +151,7 @@ export default function createRoom(opts: IOpts): Promise { } let modal; - if (opts.spinner) modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner'); + if (opts.spinner) modal = Modal.createDialog(Spinner, null, 'mx_Dialog_spinner'); let roomId; return client.createRoom(createOpts).finally(function() { diff --git a/src/verification.ts b/src/verification.ts index 22b62461a1..719c0ec5b3 100644 --- a/src/verification.ts +++ b/src/verification.ts @@ -19,7 +19,6 @@ import { User } from "matrix-js-sdk/src/models/user"; import { MatrixClientPeg } from './MatrixClientPeg'; import dis from "./dispatcher/dispatcher"; import Modal from './Modal'; -import * as sdk from './index'; import { RightPanelPhases } from "./stores/RightPanelStorePhases"; import { findDMForUser } from './createRoom'; import { accessSecretStorage } from './SecurityManager'; @@ -27,6 +26,7 @@ import { verificationMethods } from 'matrix-js-sdk/src/crypto'; import { Action } from './dispatcher/actions'; import UntrustedDeviceDialog from "./components/views/dialogs/UntrustedDeviceDialog"; import { IDevice } from "./components/views/right_panel/UserInfo"; +import ManualDeviceKeyVerificationDialog from "./components/views/dialogs/ManualDeviceKeyVerificationDialog"; async function enable4SIfNeeded() { const cli = MatrixClientPeg.get(); @@ -71,8 +71,6 @@ export async function verifyDevice(user: User, device: IDevice) { refireParams: { member: user, verificationRequestPromise }, }); } else if (action === "legacy") { - const ManualDeviceKeyVerificationDialog = - sdk.getComponent("dialogs.ManualDeviceKeyVerificationDialog"); Modal.createTrackedDialog("Legacy verify session", "legacy verify session", ManualDeviceKeyVerificationDialog, { From b17d72c399ddde3df4ece5a0f8f23cece0a5fc0e Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 2 Jul 2021 14:54:10 +0100 Subject: [PATCH 128/221] fix duplicate import --- test/components/views/dialogs/InteractiveAuthDialog-test.js | 1 - 1 file changed, 1 deletion(-) diff --git a/test/components/views/dialogs/InteractiveAuthDialog-test.js b/test/components/views/dialogs/InteractiveAuthDialog-test.js index e89b9db24e..dcf89afb4d 100644 --- a/test/components/views/dialogs/InteractiveAuthDialog-test.js +++ b/test/components/views/dialogs/InteractiveAuthDialog-test.js @@ -24,7 +24,6 @@ import sdk from '../../../skinned-sdk'; import { MatrixClientPeg } from '../../../../src/MatrixClientPeg'; import * as TestUtilsMatrix from '../../../test-utils'; -import { sleep } from "../../../../src/utils/promise"; const InteractiveAuthDialog = sdk.getComponent( 'views.dialogs.InteractiveAuthDialog', From aac098900581b99ea0a53b510621e7cbd8f00388 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Fri, 2 Jul 2021 14:58:59 +0100 Subject: [PATCH 129/221] Make beta dot animation run on the compositor --- res/css/views/beta/_BetaCard.scss | 32 ++++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/res/css/views/beta/_BetaCard.scss b/res/css/views/beta/_BetaCard.scss index 1a8241b65f..218c3dc3b2 100644 --- a/res/css/views/beta/_BetaCard.scss +++ b/res/css/views/beta/_BetaCard.scss @@ -113,21 +113,47 @@ $dot-size: 12px; box-shadow: 0 0 0 0 rgba($pulse-color, 1); animation: mx_Beta_bluePulse 2s infinite; animation-iteration-count: 20; + position: relative; + + &::after { + content: ""; + position: absolute; + inset: 0; + transform: scale(1); + transform-origin: center center; + animation-name: mx_Beta_bluePulse_shadow; + animation-duration: inherit; + animation-iteration-count: inherit; + border-radius: 50%; + background: rgba($pulse-color, 1); + } } @keyframes mx_Beta_bluePulse { 0% { transform: scale(0.95); - box-shadow: 0 0 0 0 rgba($pulse-color, 0.7); } 70% { transform: scale(1); - box-shadow: 0 0 0 10px rgba($pulse-color, 0); } 100% { transform: scale(0.95); - box-shadow: 0 0 0 0 rgba($pulse-color, 0); + } +} + +@keyframes mx_Beta_bluePulse_shadow { + 0% { + opacity: 0.7; + } + + 70% { + transform: scale(2.2); + opacity: 0; + } + + 100% { + opacity: 0; } } From a093ea6357ea9116f8650bc91532b8df970fbe2b Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Fri, 2 Jul 2021 15:23:03 +0100 Subject: [PATCH 130/221] Move RightPanel animation to compositor --- res/css/structures/_RightPanel.scss | 34 ++++++++++++++++++++++++++--- res/css/views/beta/_BetaCard.scss | 6 +++-- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/res/css/structures/_RightPanel.scss b/res/css/structures/_RightPanel.scss index 52a2a68b6a..3222fe936c 100644 --- a/res/css/structures/_RightPanel.scss +++ b/res/css/structures/_RightPanel.scss @@ -121,23 +121,51 @@ $pulse-color: $pinned-unread-color; box-shadow: 0 0 0 0 rgba($pulse-color, 1); animation: mx_RightPanel_indicator_pulse 2s infinite; animation-iteration-count: 1; + + &::after { + content: ""; + position: absolute; + width: inherit; + height: inherit; + top: 0; + left: 0; + transform: scale(1); + transform-origin: center center; + animation-name: mx_RightPanel_indicator_pulse_shadow; + animation-duration: inherit; + animation-iteration-count: inherit; + border-radius: 50%; + background: rgba($pulse-color, 1); + } } } @keyframes mx_RightPanel_indicator_pulse { 0% { transform: scale(0.95); - box-shadow: 0 0 0 0 rgba($pulse-color, 0.7); } 70% { transform: scale(1); - box-shadow: 0 0 0 10px rgba($pulse-color, 0); } 100% { transform: scale(0.95); - box-shadow: 0 0 0 0 rgba($pulse-color, 0); + } +} + +@keyframes mx_RightPanel_indicator_pulse_shadow { + 0% { + opacity: 0.7; + } + + 70% { + transform: scale(2.2); + opacity: 0; + } + + 100% { + opacity: 0; } } diff --git a/res/css/views/beta/_BetaCard.scss b/res/css/views/beta/_BetaCard.scss index 218c3dc3b2..2af4e79ecd 100644 --- a/res/css/views/beta/_BetaCard.scss +++ b/res/css/views/beta/_BetaCard.scss @@ -110,7 +110,6 @@ $dot-size: 12px; width: $dot-size; transform: scale(1); background: rgba($pulse-color, 1); - box-shadow: 0 0 0 0 rgba($pulse-color, 1); animation: mx_Beta_bluePulse 2s infinite; animation-iteration-count: 20; position: relative; @@ -118,7 +117,10 @@ $dot-size: 12px; &::after { content: ""; position: absolute; - inset: 0; + width: inherit; + height: inherit; + top: 0; + left: 0; transform: scale(1); transform-origin: center center; animation-name: mx_Beta_bluePulse_shadow; From a22baa1b2b8a1ca7dfd1d75683ed3ae0aba50e00 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Fri, 2 Jul 2021 15:23:27 +0100 Subject: [PATCH 131/221] move file drop image animation to compositor --- res/css/structures/_RoomView.scss | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/res/css/structures/_RoomView.scss b/res/css/structures/_RoomView.scss index 0efa2d01a1..831f186ed4 100644 --- a/res/css/structures/_RoomView.scss +++ b/res/css/structures/_RoomView.scss @@ -57,14 +57,15 @@ limitations under the License. @keyframes mx_RoomView_fileDropTarget_image_animation { from { - width: 0px; + transform: scaleX(0); } to { - width: 32px; + transform: scaleX(1); } } .mx_RoomView_fileDropTarget_image { + width: 32px; animation: mx_RoomView_fileDropTarget_image_animation; animation-duration: 0.5s; margin-bottom: 16px; From df64a076d9a8b5b727c87c1f1a322f06b48dcd1e Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Fri, 2 Jul 2021 15:24:30 +0100 Subject: [PATCH 132/221] Deprecate unused PulsedAvatar --- res/css/_components.scss | 1 - res/css/views/avatars/_PulsedAvatar.scss | 30 ------------------------ 2 files changed, 31 deletions(-) delete mode 100644 res/css/views/avatars/_PulsedAvatar.scss diff --git a/res/css/_components.scss b/res/css/_components.scss index 1517527034..389be11c60 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -57,7 +57,6 @@ @import "./views/avatars/_BaseAvatar.scss"; @import "./views/avatars/_DecoratedRoomAvatar.scss"; @import "./views/avatars/_MemberStatusMessageAvatar.scss"; -@import "./views/avatars/_PulsedAvatar.scss"; @import "./views/avatars/_WidgetAvatar.scss"; @import "./views/beta/_BetaCard.scss"; @import "./views/context_menus/_CallContextMenu.scss"; diff --git a/res/css/views/avatars/_PulsedAvatar.scss b/res/css/views/avatars/_PulsedAvatar.scss deleted file mode 100644 index ce9e3382ab..0000000000 --- a/res/css/views/avatars/_PulsedAvatar.scss +++ /dev/null @@ -1,30 +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. -*/ - -.mx_PulsedAvatar { - @keyframes shadow-pulse { - 0% { - box-shadow: 0 0 0 0px rgba($accent-color, 0.2); - } - 100% { - box-shadow: 0 0 0 6px rgba($accent-color, 0); - } - } - - img { - animation: shadow-pulse 1s infinite; - } -} From 9d569c378e5240554780fb52336184136eb10204 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 2 Jul 2021 17:08:27 +0200 Subject: [PATCH 133/221] Second batch of burning MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/actions/RoomListActions.ts | 5 +---- .../dialogs/eventindex/ManageEventIndexDialog.tsx | 8 +++----- src/autocomplete/CommunityProvider.tsx | 4 +--- src/autocomplete/NotifProvider.tsx | 4 +--- src/autocomplete/UserProvider.tsx | 4 +--- src/components/structures/FilePanel.tsx | 10 +++++----- src/components/structures/HomePage.tsx | 3 +-- src/components/structures/TabbedView.tsx | 4 +--- src/components/structures/TimelinePanel.tsx | 3 +-- src/components/structures/auth/Login.tsx | 5 ++--- src/components/views/auth/RegistrationForm.tsx | 3 +-- src/components/views/dialogs/AskInviteAnywayDialog.tsx | 4 +--- src/components/views/dialogs/ChangelogDialog.tsx | 6 ++---- src/components/views/dialogs/ConfirmRedactDialog.tsx | 3 +-- .../views/dialogs/DeactivateAccountDialog.tsx | 4 +--- src/components/views/dialogs/ErrorDialog.tsx | 3 +-- 16 files changed, 24 insertions(+), 49 deletions(-) diff --git a/src/actions/RoomListActions.ts b/src/actions/RoomListActions.ts index 81e05f8678..a7f629c40d 100644 --- a/src/actions/RoomListActions.ts +++ b/src/actions/RoomListActions.ts @@ -19,13 +19,13 @@ import { asyncAction } from './actionCreators'; import Modal from '../Modal'; import * as Rooms from '../Rooms'; import { _t } from '../languageHandler'; -import * as sdk from '../index'; import { MatrixClient } from "matrix-js-sdk/src/client"; import { Room } from "matrix-js-sdk/src/models/room"; import { AsyncActionPayload } from "../dispatcher/payloads"; import RoomListStore from "../stores/room-list/RoomListStore"; import { SortAlgorithm } from "../stores/room-list/algorithms/models"; import { DefaultTagID } from "../stores/room-list/models"; +import ErrorDialog from '../components/views/dialogs/ErrorDialog'; export default class RoomListActions { /** @@ -88,7 +88,6 @@ export default class RoomListActions { return Rooms.guessAndSetDMRoom( room, newTag === DefaultTagID.DM, ).catch((err) => { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); console.error("Failed to set direct chat tag " + err); Modal.createTrackedDialog('Failed to set direct chat tag', '', ErrorDialog, { title: _t('Failed to set direct chat tag'), @@ -109,7 +108,6 @@ export default class RoomListActions { const promiseToDelete = matrixClient.deleteRoomTag( roomId, oldTag, ).catch(function(err) { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); console.error("Failed to remove tag " + oldTag + " from room: " + err); Modal.createTrackedDialog('Failed to remove tag from room', '', ErrorDialog, { title: _t('Failed to remove tag %(tagName)s from room', { tagName: oldTag }), @@ -129,7 +127,6 @@ export default class RoomListActions { metaData = metaData || {}; const promiseToAdd = matrixClient.setRoomTag(roomId, newTag, metaData).catch(function(err) { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); console.error("Failed to add tag " + newTag + " to room: " + err); Modal.createTrackedDialog('Failed to add tag to room', '', ErrorDialog, { title: _t('Failed to add tag %(tagName)s to room', { tagName: newTag }), diff --git a/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.tsx b/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.tsx index 76c3373ba4..9da724b535 100644 --- a/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.tsx +++ b/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.tsx @@ -15,7 +15,6 @@ limitations under the License. */ import React from 'react'; -import * as sdk from '../../../../index'; import { _t } from '../../../../languageHandler'; import SdkConfig from '../../../../SdkConfig'; import SettingsStore from "../../../../settings/SettingsStore"; @@ -24,6 +23,9 @@ import Modal from '../../../../Modal'; import { formatBytes, formatCountLong } from "../../../../utils/FormattingUtils"; import EventIndexPeg from "../../../../indexing/EventIndexPeg"; import { SettingLevel } from "../../../../settings/SettingLevel"; +import Field from '../../../../components/views/elements/Field'; +import BaseDialog from "../../../../components/views/dialogs/BaseDialog"; +import DialogButtons from "../../../../components/views/elements/DialogButtons"; interface IProps { onFinished: (confirmed: boolean) => void; @@ -145,7 +147,6 @@ export default class ManageEventIndexDialog extends React.Component ); - const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); - const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); - return ( { - const BaseAvatar = sdk.getComponent('views.avatars.BaseAvatar'); - // Disable autocompletions when composing commands because of various issues // (see https://github.com/vector-im/element-web/issues/4762) if (/^(\/join|\/leave)/.test(query)) { diff --git a/src/autocomplete/NotifProvider.tsx b/src/autocomplete/NotifProvider.tsx index 1d42915ec9..31b834ccfe 100644 --- a/src/autocomplete/NotifProvider.tsx +++ b/src/autocomplete/NotifProvider.tsx @@ -21,8 +21,8 @@ import AutocompleteProvider from './AutocompleteProvider'; import { _t } from '../languageHandler'; import { MatrixClientPeg } from '../MatrixClientPeg'; import { PillCompletion } from './Components'; -import * as sdk from '../index'; import { ICompletion, ISelectionRange } from "./Autocompleter"; +import RoomAvatar from '../components/views/avatars/RoomAvatar'; const AT_ROOM_REGEX = /@\S*/g; @@ -40,8 +40,6 @@ export default class NotifProvider extends AutocompleteProvider { force = false, limit = -1, ): Promise { - const RoomAvatar = sdk.getComponent('views.avatars.RoomAvatar'); - const client = MatrixClientPeg.get(); if (!this.room.currentState.mayTriggerNotifOfType('room', client.credentials.userId)) return []; diff --git a/src/autocomplete/UserProvider.tsx b/src/autocomplete/UserProvider.tsx index 470e018e22..d8f17c54d0 100644 --- a/src/autocomplete/UserProvider.tsx +++ b/src/autocomplete/UserProvider.tsx @@ -21,7 +21,6 @@ import React from 'react'; import { _t } from '../languageHandler'; import AutocompleteProvider from './AutocompleteProvider'; import { PillCompletion } from './Components'; -import * as sdk from '../index'; import QueryMatcher from './QueryMatcher'; import { sortBy } from 'lodash'; import { MatrixClientPeg } from '../MatrixClientPeg'; @@ -33,6 +32,7 @@ import { RoomState } from "matrix-js-sdk/src/models/room-state"; import { EventTimeline } from "matrix-js-sdk/src/models/event-timeline"; import { makeUserPermalink } from "../utils/permalinks/Permalinks"; import { ICompletion, ISelectionRange } from "./Autocompleter"; +import MemberAvatar from '../components/views/avatars/MemberAvatar'; const USER_REGEX = /\B@\S*/g; @@ -108,8 +108,6 @@ export default class UserProvider extends AutocompleteProvider { force = false, limit = -1, ): Promise { - const MemberAvatar = sdk.getComponent('views.avatars.MemberAvatar'); - // lazy-load user list into matcher if (!this.users) this._makeUsers(); diff --git a/src/components/structures/FilePanel.tsx b/src/components/structures/FilePanel.tsx index 5865201069..36f774a130 100644 --- a/src/components/structures/FilePanel.tsx +++ b/src/components/structures/FilePanel.tsx @@ -24,7 +24,6 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { Room } from 'matrix-js-sdk/src/models/room'; import { TimelineWindow } from 'matrix-js-sdk/src/timeline-window'; -import * as sdk from '../../index'; import { MatrixClientPeg } from '../../MatrixClientPeg'; import EventIndexPeg from "../../indexing/EventIndexPeg"; import { _t } from '../../languageHandler'; @@ -34,6 +33,9 @@ import DesktopBuildsNotice, { WarningKind } from "../views/elements/DesktopBuild import { replaceableComponent } from "../../utils/replaceableComponent"; import ResizeNotifier from '../../utils/ResizeNotifier'; +import TimelinePanel from "./TimelinePanel"; +import Spinner from "../views/elements/Spinner"; +import { TileShape } from '../views/rooms/EventTile'; interface IProps { roomId: string; @@ -237,8 +239,6 @@ class FilePanel extends React.Component { } // wrap a TimelinePanel with the jump-to-event bits turned off. - const TimelinePanel = sdk.getComponent("structures.TimelinePanel"); - const Loader = sdk.getComponent("elements.Spinner"); const emptyState = (

{_t('No files visible in this room')}

@@ -264,7 +264,7 @@ class FilePanel extends React.Component { timelineSet={this.state.timelineSet} showUrlPreview = {false} onPaginationRequest={this.onPaginationRequest} - tileShape="file_grid" + tileShape={TileShape.FileGrid} resizeNotifier={this.props.resizeNotifier} empty={emptyState} /> @@ -277,7 +277,7 @@ class FilePanel extends React.Component { onClose={this.props.onClose} previousPhase={RightPanelPhases.RoomSummary} > - + ); } diff --git a/src/components/structures/HomePage.tsx b/src/components/structures/HomePage.tsx index 046e07f455..94239ea603 100644 --- a/src/components/structures/HomePage.tsx +++ b/src/components/structures/HomePage.tsx @@ -21,7 +21,6 @@ import AutoHideScrollbar from './AutoHideScrollbar'; import { getHomePageUrl } from "../../utils/pages"; import { _t } from "../../languageHandler"; import SdkConfig from "../../SdkConfig"; -import * as sdk from "../../index"; import dis from "../../dispatcher/dispatcher"; import { Action } from "../../dispatcher/actions"; import BaseAvatar from "../views/avatars/BaseAvatar"; @@ -33,6 +32,7 @@ import MatrixClientContext from "../../contexts/MatrixClientContext"; import MiniAvatarUploader, { AVATAR_SIZE } from "../views/elements/MiniAvatarUploader"; import Analytics from "../../Analytics"; import CountlyAnalytics from "../../CountlyAnalytics"; +import EmbeddedPage from "./EmbeddedPage"; const onClickSendDm = () => { Analytics.trackEvent('home_page', 'button', 'dm'); @@ -96,7 +96,6 @@ const HomePage: React.FC = ({ justRegistered = false }) => { const pageUrl = getHomePageUrl(config); if (pageUrl) { - const EmbeddedPage = sdk.getComponent('structures.EmbeddedPage'); return ; } diff --git a/src/components/structures/TabbedView.tsx b/src/components/structures/TabbedView.tsx index 3d77eaeac1..dcfde94811 100644 --- a/src/components/structures/TabbedView.tsx +++ b/src/components/structures/TabbedView.tsx @@ -18,9 +18,9 @@ limitations under the License. import * as React from "react"; import { _t } from '../../languageHandler'; -import * as sdk from "../../index"; import AutoHideScrollbar from './AutoHideScrollbar'; import { replaceableComponent } from "../../utils/replaceableComponent"; +import AccessibleButton from "../views/elements/AccessibleButton"; /** * Represents a tab for the TabbedView. @@ -82,8 +82,6 @@ export default class TabbedView extends React.Component { } private _renderTabLabel(tab: Tab) { - const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); - let classes = "mx_TabbedView_tabLabel "; const idx = this.props.tabs.indexOf(tab); diff --git a/src/components/structures/TimelinePanel.tsx b/src/components/structures/TimelinePanel.tsx index 0b6d33b409..85a048e9b8 100644 --- a/src/components/structures/TimelinePanel.tsx +++ b/src/components/structures/TimelinePanel.tsx @@ -32,7 +32,6 @@ import RoomContext from "../../contexts/RoomContext"; import UserActivity from "../../UserActivity"; import Modal from "../../Modal"; import dis from "../../dispatcher/dispatcher"; -import * as sdk from "../../index"; import { Key } from '../../Keyboard'; import Timer from '../../utils/Timer'; import shouldHideEvent from '../../shouldHideEvent'; @@ -47,6 +46,7 @@ import ResizeNotifier from "../../utils/ResizeNotifier"; import { RoomPermalinkCreator } from "../../utils/permalinks/Permalinks"; import Spinner from "../views/elements/Spinner"; import EditorStateTransfer from '../../utils/EditorStateTransfer'; +import ErrorDialog from '../views/dialogs/ErrorDialog'; const PAGINATE_SIZE = 20; const INITIAL_SIZE = 20; @@ -1096,7 +1096,6 @@ class TimelinePanel extends React.Component { console.error( `Error loading timeline panel at ${eventId}: ${error}`, ); - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); let onFinished; diff --git a/src/components/structures/auth/Login.tsx b/src/components/structures/auth/Login.tsx index 61d3759dee..9f12521a34 100644 --- a/src/components/structures/auth/Login.tsx +++ b/src/components/structures/auth/Login.tsx @@ -18,7 +18,6 @@ import React, { ReactNode } from 'react'; import { MatrixError } from "matrix-js-sdk/src/http-api"; import { _t, _td } from '../../../languageHandler'; -import * as sdk from '../../../index'; import Login, { ISSOFlow, LoginFlow } from '../../../Login'; import SdkConfig from '../../../SdkConfig'; import { messageForResourceLimitError } from '../../../utils/ErrorUtils'; @@ -36,6 +35,8 @@ import Spinner from "../../views/elements/Spinner"; import SSOButtons from "../../views/elements/SSOButtons"; import ServerPicker from "../../views/elements/ServerPicker"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import AuthBody from "../../views/auth/AuthBody"; +import AuthHeader from "../../views/auth/AuthHeader"; // These are used in several places, and come from the js-sdk's autodiscovery // stuff. We define them here so that they'll be picked up by i18n. @@ -541,8 +542,6 @@ export default class LoginComponent extends React.PureComponent }; render() { - const AuthHeader = sdk.getComponent("auth.AuthHeader"); - const AuthBody = sdk.getComponent("auth.AuthBody"); const loader = this.isBusy() && !this.state.busyLoggingIn ?
: null; diff --git a/src/components/views/auth/RegistrationForm.tsx b/src/components/views/auth/RegistrationForm.tsx index be6e29a493..25ea347d24 100644 --- a/src/components/views/auth/RegistrationForm.tsx +++ b/src/components/views/auth/RegistrationForm.tsx @@ -17,7 +17,6 @@ limitations under the License. import React from 'react'; -import * as sdk from '../../../index'; import * as Email from '../../../email'; import { looksValid as phoneNumberLooksValid } from '../../../phonenumber'; import Modal from '../../../Modal'; @@ -31,6 +30,7 @@ import CountlyAnalytics from "../../../CountlyAnalytics"; import Field from '../elements/Field'; import RegistrationEmailPromptDialog from '../dialogs/RegistrationEmailPromptDialog'; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import CountryDropdown from "./CountryDropdown"; enum RegistrationField { Email = "field_email", @@ -471,7 +471,6 @@ export default class RegistrationForm extends React.PureComponent { }; public render() { - const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); - const errorList = this.props.unknownProfileUsers .map(address =>
  • {address.userId}: {address.errorText}
  • ); diff --git a/src/components/views/dialogs/ChangelogDialog.tsx b/src/components/views/dialogs/ChangelogDialog.tsx index 8acacd8e73..d484d94249 100644 --- a/src/components/views/dialogs/ChangelogDialog.tsx +++ b/src/components/views/dialogs/ChangelogDialog.tsx @@ -16,9 +16,10 @@ Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> */ import React from 'react'; -import * as sdk from '../../../index'; import request from 'browser-request'; import { _t } from '../../../languageHandler'; +import QuestionDialog from "./QuestionDialog"; +import Spinner from "../elements/Spinner"; interface IProps { newVersion: string; @@ -65,9 +66,6 @@ export default class ChangelogDialog extends React.Component { } public render() { - const Spinner = sdk.getComponent('views.elements.Spinner'); - const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog'); - const logs = REPOS.map(repo => { let content; if (this.state[repo] == null) { diff --git a/src/components/views/dialogs/ConfirmRedactDialog.tsx b/src/components/views/dialogs/ConfirmRedactDialog.tsx index 94f29a71fc..a2f2b10144 100644 --- a/src/components/views/dialogs/ConfirmRedactDialog.tsx +++ b/src/components/views/dialogs/ConfirmRedactDialog.tsx @@ -15,9 +15,9 @@ limitations under the License. */ import React from 'react'; -import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import TextInputDialog from "./TextInputDialog"; interface IProps { onFinished: (success: boolean) => void; @@ -29,7 +29,6 @@ interface IProps { @replaceableComponent("views.dialogs.ConfirmRedactDialog") export default class ConfirmRedactDialog extends React.Component { render() { - const TextInputDialog = sdk.getComponent('views.dialogs.TextInputDialog'); return ( void; @@ -165,8 +165,6 @@ export default class DeactivateAccountDialog extends React.Component diff --git a/src/components/views/dialogs/ErrorDialog.tsx b/src/components/views/dialogs/ErrorDialog.tsx index 0f675f0df7..56cd76237f 100644 --- a/src/components/views/dialogs/ErrorDialog.tsx +++ b/src/components/views/dialogs/ErrorDialog.tsx @@ -26,9 +26,9 @@ limitations under the License. */ import React from 'react'; -import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import BaseDialog from "./BaseDialog"; interface IProps { onFinished: (success: boolean) => void; @@ -57,7 +57,6 @@ export default class ErrorDialog extends React.Component { }; public render() { - const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); return ( Date: Fri, 2 Jul 2021 17:19:01 +0200 Subject: [PATCH 134/221] Third batch of burning MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- .../views/dialogs/ConfirmWipeDeviceDialog.tsx | 6 ++---- src/components/views/dialogs/CreateGroupDialog.tsx | 6 ++---- .../views/dialogs/CryptoStoreTooNewDialog.tsx | 7 +++---- src/components/views/dialogs/DevtoolsDialog.tsx | 5 ++--- src/components/views/dialogs/ReportEventDialog.tsx | 12 +++++------- src/components/views/dialogs/RoomSettingsDialog.tsx | 4 +--- src/components/views/dialogs/UploadConfirmDialog.tsx | 6 ++---- 7 files changed, 17 insertions(+), 29 deletions(-) diff --git a/src/components/views/dialogs/ConfirmWipeDeviceDialog.tsx b/src/components/views/dialogs/ConfirmWipeDeviceDialog.tsx index 2978179817..544d0df1c9 100644 --- a/src/components/views/dialogs/ConfirmWipeDeviceDialog.tsx +++ b/src/components/views/dialogs/ConfirmWipeDeviceDialog.tsx @@ -16,8 +16,9 @@ limitations under the License. import React from 'react'; import { _t } from "../../../languageHandler"; -import * as sdk from "../../../index"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import BaseDialog from "./BaseDialog"; +import DialogButtons from "../elements/DialogButtons"; interface IProps { onFinished: (success: boolean) => void; @@ -34,9 +35,6 @@ export default class ConfirmWipeDeviceDialog extends React.Component { }; render() { - const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); - const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); - return ( void; @@ -106,9 +107,6 @@ export default class CreateGroupDialog extends React.Component { }; render() { - const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); - const Spinner = sdk.getComponent('elements.Spinner'); - if (this.state.creating) { return ; } diff --git a/src/components/views/dialogs/CryptoStoreTooNewDialog.tsx b/src/components/views/dialogs/CryptoStoreTooNewDialog.tsx index 2cdaf9cf4f..b1c3ff229a 100644 --- a/src/components/views/dialogs/CryptoStoreTooNewDialog.tsx +++ b/src/components/views/dialogs/CryptoStoreTooNewDialog.tsx @@ -16,11 +16,13 @@ limitations under the License. */ import React from 'react'; -import * as sdk from '../../../index'; import dis from '../../../dispatcher/dispatcher'; import { _t } from '../../../languageHandler'; import SdkConfig from '../../../SdkConfig'; import Modal from '../../../Modal'; +import BaseDialog from "./BaseDialog"; +import DialogButtons from "../elements/DialogButtons"; +import QuestionDialog from "./QuestionDialog"; interface IProps { onFinished: (success: boolean) => void; @@ -30,7 +32,6 @@ export default (props: IProps) => { const brand = SdkConfig.get().brand; const _onLogoutClicked = () => { - const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); Modal.createTrackedDialog('Logout e2e db too new', '', QuestionDialog, { title: _t("Sign out"), description: _t( @@ -58,8 +59,6 @@ export default (props: IProps) => { { brand }, ); - const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); - const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); return ( void; @@ -369,7 +370,6 @@ class FilteredList extends React.PureComponent ; } - const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); return ( { body } diff --git a/src/components/views/dialogs/ReportEventDialog.tsx b/src/components/views/dialogs/ReportEventDialog.tsx index 81a7d569fe..494fd59082 100644 --- a/src/components/views/dialogs/ReportEventDialog.tsx +++ b/src/components/views/dialogs/ReportEventDialog.tsx @@ -15,7 +15,6 @@ limitations under the License. */ import React from 'react'; -import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; import { ensureDMExists } from "../../../createRoom"; import { IDialogProps } from "./IDialogProps"; @@ -26,6 +25,10 @@ import Markdown from '../../../Markdown'; import { replaceableComponent } from "../../../utils/replaceableComponent"; import SettingsStore from "../../../settings/SettingsStore"; import StyledRadioButton from "../elements/StyledRadioButton"; +import BaseDialog from "./BaseDialog"; +import DialogButtons from "../elements/DialogButtons"; +import Field from "../elements/Field"; +import Spinner from "../elements/Spinner"; interface IProps extends IDialogProps { mxEvent: MatrixEvent; @@ -239,11 +242,6 @@ export default class ReportEventDialog extends React.Component { }; render() { - const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); - const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); - const Loader = sdk.getComponent('elements.Spinner'); - const Field = sdk.getComponent('elements.Field'); - let error = null; if (this.state.err) { error =
    @@ -255,7 +253,7 @@ export default class ReportEventDialog extends React.Component { if (this.state.busy) { progress = (
    - +
    ); } diff --git a/src/components/views/dialogs/RoomSettingsDialog.tsx b/src/components/views/dialogs/RoomSettingsDialog.tsx index 005222a94e..a426dce5c7 100644 --- a/src/components/views/dialogs/RoomSettingsDialog.tsx +++ b/src/components/views/dialogs/RoomSettingsDialog.tsx @@ -24,12 +24,12 @@ import GeneralRoomSettingsTab from "../settings/tabs/room/GeneralRoomSettingsTab import SecurityRoomSettingsTab from "../settings/tabs/room/SecurityRoomSettingsTab"; import NotificationSettingsTab from "../settings/tabs/room/NotificationSettingsTab"; import BridgeSettingsTab from "../settings/tabs/room/BridgeSettingsTab"; -import * as sdk from "../../../index"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import dis from "../../../dispatcher/dispatcher"; import SettingsStore from "../../../settings/SettingsStore"; import { UIFeature } from "../../../settings/UIFeature"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import BaseDialog from "./BaseDialog"; export const ROOM_GENERAL_TAB = "ROOM_GENERAL_TAB"; export const ROOM_SECURITY_TAB = "ROOM_SECURITY_TAB"; @@ -119,8 +119,6 @@ export default class RoomSettingsDialog extends React.Component { } render() { - const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); - const roomName = MatrixClientPeg.get().getRoom(this.props.roomId).name; return ( { }; render() { - const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); - const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); - let title; if (this.props.totalFiles > 1 && this.props.currentIndex !== undefined) { title = _t( From 426c79f47ddc4ea749913cd6be8b09b44fe9720a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 2 Jul 2021 17:25:30 +0200 Subject: [PATCH 135/221] Delint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/MatrixClientPeg.ts | 2 +- src/SecurityManager.ts | 1 + src/components/views/dialogs/CryptoStoreTooNewDialog.tsx | 4 +++- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/MatrixClientPeg.ts b/src/MatrixClientPeg.ts index db8401baba..7b3c069412 100644 --- a/src/MatrixClientPeg.ts +++ b/src/MatrixClientPeg.ts @@ -34,7 +34,7 @@ import IdentityAuthClient from './IdentityAuthClient'; import { crossSigningCallbacks, tryToUnlockSecretStorageWithDehydrationKey } from './SecurityManager'; import { SHOW_QR_CODE_METHOD } from "matrix-js-sdk/src/crypto/verification/QRCode"; import SecurityCustomisations from "./customisations/Security"; -import CryptoStoreTooNewDialog from ".components/views/dialogs/CryptoStoreTooNewDialog"; +import CryptoStoreTooNewDialog from "./components/views/dialogs/CryptoStoreTooNewDialog"; export interface IMatrixClientCreds { homeserverUrl: string; diff --git a/src/SecurityManager.ts b/src/SecurityManager.ts index 8479eadddb..b6736cb69c 100644 --- a/src/SecurityManager.ts +++ b/src/SecurityManager.ts @@ -29,6 +29,7 @@ import SettingsStore from "./settings/SettingsStore"; import SecurityCustomisations from "./customisations/Security"; import { DeviceTrustLevel } from 'matrix-js-sdk/src/crypto/CrossSigning'; import InteractiveAuthDialog from "./components/views/dialogs/InteractiveAuthDialog"; +import QuestionDialog from "./components/views/dialogs/QuestionDialog"; // This stores the secret storage private keys in memory for the JS SDK. This is // only meant to act as a cache to avoid prompting the user multiple times diff --git a/src/components/views/dialogs/CryptoStoreTooNewDialog.tsx b/src/components/views/dialogs/CryptoStoreTooNewDialog.tsx index b1c3ff229a..134c4ab79e 100644 --- a/src/components/views/dialogs/CryptoStoreTooNewDialog.tsx +++ b/src/components/views/dialogs/CryptoStoreTooNewDialog.tsx @@ -28,7 +28,7 @@ interface IProps { onFinished: (success: boolean) => void; } -export default (props: IProps) => { +const CryptoStoreTooNewDialog: React.FC = (props: IProps) => { const brand = SdkConfig.get().brand; const _onLogoutClicked = () => { @@ -78,3 +78,5 @@ export default (props: IProps) => { ); }; + +export default CryptoStoreTooNewDialog; From dcb555784864e7d9b9f901c7f2ffa66cabca520a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 2 Jul 2021 18:00:07 +0200 Subject: [PATCH 136/221] Revert some changes due to failing tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/Analytics.tsx | 4 +++- src/AsyncWrapper.tsx | 8 +++++--- src/MatrixClientPeg.ts | 5 ++++- src/SecurityManager.ts | 6 ++++-- src/Terms.ts | 4 +++- 5 files changed, 19 insertions(+), 8 deletions(-) diff --git a/src/Analytics.tsx b/src/Analytics.tsx index 6a1608b63f..ce8287de56 100644 --- a/src/Analytics.tsx +++ b/src/Analytics.tsx @@ -21,7 +21,7 @@ import { getCurrentLanguage, _t, _td, IVariables } from './languageHandler'; import PlatformPeg from './PlatformPeg'; import SdkConfig from './SdkConfig'; import Modal from './Modal'; -import ErrorDialog from './components/views/dialogs/ErrorDialog'; +import * as sdk from './index'; const hashRegex = /#\/(groups?|room|user|settings|register|login|forgot_password|home|directory)/; const hashVarRegex = /#\/(group|room|user)\/.*$/; @@ -390,6 +390,8 @@ export class Analytics { { expl: _td('Your device resolution'), value: resolution }, ]; + // FIXME: Using an import will result in test failures + const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog'); Modal.createTrackedDialog('Analytics Details', '', ErrorDialog, { title: _t('Analytics'), description:
    diff --git a/src/AsyncWrapper.tsx b/src/AsyncWrapper.tsx index 5d3531be62..ef8924add8 100644 --- a/src/AsyncWrapper.tsx +++ b/src/AsyncWrapper.tsx @@ -16,11 +16,9 @@ limitations under the License. import React, { ComponentType } from "react"; +import * as sdk from './index'; import { _t } from './languageHandler'; import { IDialogProps } from "./components/views/dialogs/IDialogProps"; -import BaseDialog from "./components/views/dialogs/BaseDialog"; -import Spinner from "./components/views/elements/Spinner"; -import DialogButtons from "./components/views/elements/DialogButtons"; type AsyncImport = { default: T }; @@ -79,6 +77,9 @@ export default class AsyncWrapper extends React.Component { const Component = this.state.component; return ; } else if (this.state.error) { + // FIXME: Using an import will result in test failures + const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); return { _t("Unable to load! Check your network connectivity and try again.") } { ; } else { // show a spinner until the component is loaded. + const Spinner = sdk.getComponent("elements.Spinner"); return ; } } diff --git a/src/MatrixClientPeg.ts b/src/MatrixClientPeg.ts index 7b3c069412..063c5f4cad 100644 --- a/src/MatrixClientPeg.ts +++ b/src/MatrixClientPeg.ts @@ -23,6 +23,7 @@ import { MemoryStore } from 'matrix-js-sdk/src/store/memory'; import * as utils from 'matrix-js-sdk/src/utils'; import { EventTimeline } from 'matrix-js-sdk/src/models/event-timeline'; import { EventTimelineSet } from 'matrix-js-sdk/src/models/event-timeline-set'; +import * as sdk from './index'; import createMatrixClient from './utils/createMatrixClient'; import SettingsStore from './settings/SettingsStore'; import MatrixActionCreators from './actions/MatrixActionCreators'; @@ -34,7 +35,6 @@ import IdentityAuthClient from './IdentityAuthClient'; import { crossSigningCallbacks, tryToUnlockSecretStorageWithDehydrationKey } from './SecurityManager'; import { SHOW_QR_CODE_METHOD } from "matrix-js-sdk/src/crypto/verification/QRCode"; import SecurityCustomisations from "./customisations/Security"; -import CryptoStoreTooNewDialog from "./components/views/dialogs/CryptoStoreTooNewDialog"; export interface IMatrixClientCreds { homeserverUrl: string; @@ -219,6 +219,9 @@ class _MatrixClientPeg implements IMatrixClientPeg { } catch (e) { if (e && e.name === 'InvalidCryptoStoreError') { // The js-sdk found a crypto DB too new for it to use + // FIXME: Using an import will result in test failures + const CryptoStoreTooNewDialog = + sdk.getComponent("views.dialogs.CryptoStoreTooNewDialog"); Modal.createDialog(CryptoStoreTooNewDialog); } // this can happen for a number of reasons, the most likely being diff --git a/src/SecurityManager.ts b/src/SecurityManager.ts index b6736cb69c..6805dfde19 100644 --- a/src/SecurityManager.ts +++ b/src/SecurityManager.ts @@ -17,6 +17,7 @@ limitations under the License. import { ICryptoCallbacks, ISecretStorageKeyInfo } from 'matrix-js-sdk/src/matrix'; import { MatrixClient } from 'matrix-js-sdk/src/client'; import Modal from './Modal'; +import * as sdk from './index'; import { MatrixClientPeg } from './MatrixClientPeg'; import { deriveKey } from 'matrix-js-sdk/src/crypto/key_passphrase'; import { decodeRecoveryKey } from 'matrix-js-sdk/src/crypto/recoverykey'; @@ -28,8 +29,6 @@ import RestoreKeyBackupDialog from './components/views/dialogs/security/RestoreK import SettingsStore from "./settings/SettingsStore"; import SecurityCustomisations from "./customisations/Security"; import { DeviceTrustLevel } from 'matrix-js-sdk/src/crypto/CrossSigning'; -import InteractiveAuthDialog from "./components/views/dialogs/InteractiveAuthDialog"; -import QuestionDialog from "./components/views/dialogs/QuestionDialog"; // This stores the secret storage private keys in memory for the JS SDK. This is // only meant to act as a cache to avoid prompting the user multiple times @@ -69,6 +68,7 @@ export class AccessCancelledError extends Error { } async function confirmToDismiss(): Promise { + const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); const [sure] = await Modal.createDialog(QuestionDialog, { title: _t("Cancel entering passphrase?"), description: _t("Are you sure you want to cancel entering passphrase?"), @@ -354,6 +354,8 @@ export async function accessSecretStorage(func = async () => { }, forceReset = f throw new Error("Secret storage creation canceled"); } } else { + // FIXME: Using an import will result in test failures + const InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog"); await cli.bootstrapCrossSigning({ authUploadDeviceSigningKeys: async (makeRequest) => { const { finished } = Modal.createTrackedDialog( diff --git a/src/Terms.ts b/src/Terms.ts index d07555e567..351d1c0951 100644 --- a/src/Terms.ts +++ b/src/Terms.ts @@ -18,8 +18,8 @@ import classNames from 'classnames'; import { SERVICE_TYPES } from 'matrix-js-sdk/src/service-types'; import { MatrixClientPeg } from './MatrixClientPeg'; +import * as sdk from '.'; import Modal from './Modal'; -import TermsDialog from './components/views/dialogs/TermsDialog'; export class TermsNotSignedError extends Error {} @@ -189,6 +189,8 @@ export function dialogTermsInteractionCallback( ): Promise { return new Promise((resolve, reject) => { console.log("Terms that need agreement", policiesAndServicePairs); + // FIXME: Using an import will result in test failures + const TermsDialog = sdk.getComponent("views.dialogs.TermsDialog"); Modal.createTrackedDialog('Terms of Service', '', TermsDialog, { policiesAndServicePairs, From b40027a19303886b533182ebc53214375c706882 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 2 Jul 2021 18:02:41 +0200 Subject: [PATCH 137/221] Delint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- .../views/dialogs/eventindex/ManageEventIndexDialog.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.tsx b/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.tsx index 9da724b535..c5c8022346 100644 --- a/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.tsx +++ b/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.tsx @@ -177,7 +177,7 @@ export default class ManageEventIndexDialog extends React.Component
    From b36a727a09931e17174232ef205069ec82f3954d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 2 Jul 2021 18:15:05 +0200 Subject: [PATCH 138/221] Fourth batch of burning MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- .../views/settings/tabs/room/SecurityRoomSettingsTab.tsx | 4 +--- .../views/settings/tabs/user/HelpUserSettingsTab.tsx | 8 ++------ .../settings/tabs/user/PreferencesUserSettingsTab.tsx | 3 +-- src/toasts/SetupEncryptionToast.ts | 3 +-- test/components/views/rooms/MemberList-test.tsx | 5 +++-- 5 files changed, 8 insertions(+), 15 deletions(-) diff --git a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx index 4868ffd4fc..312d7f21a0 100644 --- a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx @@ -18,7 +18,6 @@ import React from 'react'; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { _t } from "../../../../../languageHandler"; import { MatrixClientPeg } from "../../../../../MatrixClientPeg"; -import * as sdk from "../../../../.."; import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch"; import Modal from "../../../../../Modal"; import QuestionDialog from "../../../dialogs/QuestionDialog"; @@ -27,6 +26,7 @@ import { SettingLevel } from "../../../../../settings/SettingLevel"; import SettingsStore from "../../../../../settings/SettingsStore"; import { UIFeature } from "../../../../../settings/UIFeature"; import { replaceableComponent } from "../../../../../utils/replaceableComponent"; +import SettingsFlag from '../../../elements/SettingsFlag'; // Knock and private are reserved keywords which are not yet implemented. export enum JoinRule { @@ -385,8 +385,6 @@ export default class SecurityRoomSettingsTab extends React.Component void; @@ -81,10 +82,6 @@ export default class HelpUserSettingsTab extends React.Component }; private onBugReport = (e) => { - const BugReportDialog = sdk.getComponent("dialogs.BugReportDialog"); - if (!BugReportDialog) { - return; - } Modal.createTrackedDialog('Bug Report Dialog', '', BugReportDialog, {}); }; @@ -171,7 +168,6 @@ export default class HelpUserSettingsTab extends React.Component const successful = await copyPlaintext(MatrixClientPeg.get().getAccessToken()); 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'), diff --git a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx index 1988ec50ac..ebef2b96bf 100644 --- a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx @@ -20,10 +20,10 @@ import { _t } from "../../../../../languageHandler"; import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch"; import SettingsStore from "../../../../../settings/SettingsStore"; import Field from "../../../elements/Field"; -import * as sdk from "../../../../.."; import PlatformPeg from "../../../../../PlatformPeg"; import { SettingLevel } from "../../../../../settings/SettingLevel"; import { replaceableComponent } from "../../../../../utils/replaceableComponent"; +import SettingsFlag from '../../../elements/SettingsFlag'; interface IState { autoLaunch: boolean; @@ -174,7 +174,6 @@ export default class PreferencesUserSettingsTab extends React.Component<{}, ISta }; private renderGroup(settingIds: string[]): React.ReactNodeArray { - const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag"); return settingIds.filter(SettingsStore.isEnabled).map(i => { return ; }); diff --git a/src/toasts/SetupEncryptionToast.ts b/src/toasts/SetupEncryptionToast.ts index ade7dfe3f0..bdaeb5142f 100644 --- a/src/toasts/SetupEncryptionToast.ts +++ b/src/toasts/SetupEncryptionToast.ts @@ -15,7 +15,6 @@ limitations under the License. */ import Modal from "../Modal"; -import * as sdk from "../index"; import { _t } from "../languageHandler"; import DeviceListener from "../DeviceListener"; import SetupEncryptionDialog from "../components/views/dialogs/security/SetupEncryptionDialog"; @@ -23,6 +22,7 @@ import { accessSecretStorage } from "../SecurityManager"; import ToastStore from "../stores/ToastStore"; import GenericToast from "../components/views/toasts/GenericToast"; import SecurityCustomisations from "../customisations/Security"; +import Spinner from "../components/views/elements/Spinner"; const TOAST_KEY = "setupencryption"; @@ -88,7 +88,6 @@ export const showToast = (kind: Kind) => { Modal.createTrackedDialog("Verify session", "Verify session", SetupEncryptionDialog, {}, null, /* priority = */ false, /* static = */ true); } else { - const Spinner = sdk.getComponent("elements.Spinner"); const modal = Modal.createDialog( Spinner, null, "mx_Dialog_spinner", /* priority */ false, /* static */ true, ); diff --git a/test/components/views/rooms/MemberList-test.tsx b/test/components/views/rooms/MemberList-test.tsx index a169cd08e6..f720bc7a6d 100644 --- a/test/components/views/rooms/MemberList-test.tsx +++ b/test/components/views/rooms/MemberList-test.tsx @@ -14,18 +14,20 @@ See the License for the specific language governing permissions and limitations under the License. */ +import "../../../skinned-sdk"; + import React from 'react'; import ReactTestUtils from 'react-dom/test-utils'; import ReactDOM from 'react-dom'; import * as TestUtils from '../../../test-utils'; -import sdk from '../../../skinned-sdk'; import { MatrixClientPeg } from '../../../../src/MatrixClientPeg'; import { Room } from 'matrix-js-sdk/src/models/room'; import { RoomMember } from 'matrix-js-sdk/src/models/room-member'; import { User } from "matrix-js-sdk/src/models/user"; import { compare } from "../../../../src/utils/strings"; import MemberList from "../../../../src/components/views/rooms/MemberList"; +import MemberTile from '../../../../src/components/views/rooms/MemberTile'; function generateRoomId() { return '!' + Math.random().toString().slice(2, 10) + ':domain'; @@ -206,7 +208,6 @@ describe('MemberList', () => { } function itDoesOrderMembersCorrectly(enablePresence) { - const MemberTile = sdk.getComponent("rooms.MemberTile"); describe('does order members correctly', () => { // Note: even if presence is disabled, we still expect that the presence // tests will pass. All expectOrderedByPresenceAndPowerLevel does is ensure From 0cee0db9dfae3629266d6e1e6cf3be553386495a Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 2 Jul 2021 20:38:07 +0100 Subject: [PATCH 139/221] Fix icon size in passphrase prompt --- res/css/views/dialogs/security/_AccessSecretStorageDialog.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/res/css/views/dialogs/security/_AccessSecretStorageDialog.scss b/res/css/views/dialogs/security/_AccessSecretStorageDialog.scss index 30b79c1a9a..ec3bea0ef7 100644 --- a/res/css/views/dialogs/security/_AccessSecretStorageDialog.scss +++ b/res/css/views/dialogs/security/_AccessSecretStorageDialog.scss @@ -28,6 +28,7 @@ limitations under the License. left: 0; top: 2px; // alignment background-image: url("$(res)/img/element-icons/warning-badge.svg"); + background-size: contain; } .mx_AccessSecretStorageDialog_reset_link { From be4c29b27cb3416e72d90cf91bbe05b90223e0e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 3 Jul 2021 08:50:45 +0200 Subject: [PATCH 140/221] Revert some changes due to them breaking Element MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/ContentMessages.tsx | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/ContentMessages.tsx b/src/ContentMessages.tsx index dd2002dd9a..66ca8a559f 100644 --- a/src/ContentMessages.tsx +++ b/src/ContentMessages.tsx @@ -21,6 +21,7 @@ import { encode } from "blurhash"; import { MatrixClient } from "matrix-js-sdk/src/client"; import dis from './dispatcher/dispatcher'; +import * as sdk from './index'; import { _t } from './languageHandler'; import Modal from './Modal'; import RoomViewStore from './stores/RoomViewStore'; @@ -39,10 +40,6 @@ import { } from "./dispatcher/payloads/UploadPayload"; import { IUpload } from "./models/IUpload"; import { IImageInfo } from "matrix-js-sdk/src/@types/partials"; -import QuestionDialog from "./components/views/dialogs/QuestionDialog"; -import ErrorDialog from "./components/views/dialogs/ErrorDialog"; -import UploadConfirmDialog from "./components/views/dialogs/UploadConfirmDialog"; -import UploadFailureDialog from "./components/views/dialogs/UploadFailureDialog"; const MAX_WIDTH = 800; const MAX_HEIGHT = 600; @@ -422,6 +419,8 @@ export default class ContentMessages { const isQuoting = Boolean(RoomViewStore.getQuotingEvent()); if (isQuoting) { + // FIXME: Using an import will result in Element crashing + const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); const { finished } = Modal.createTrackedDialog<[boolean]>('Upload Reply Warning', '', QuestionDialog, { title: _t('Replying With Files'), description: ( @@ -455,6 +454,8 @@ export default class ContentMessages { } if (tooBigFiles.length > 0) { + // FIXME: Using an import will result in Element crashing + const UploadFailureDialog = sdk.getComponent("dialogs.UploadFailureDialog"); const { finished } = Modal.createTrackedDialog<[boolean]>('Upload Failure', '', UploadFailureDialog, { badFiles: tooBigFiles, totalFiles: files.length, @@ -471,6 +472,8 @@ export default class ContentMessages { for (let i = 0; i < okFiles.length; ++i) { const file = okFiles[i]; if (!uploadAll) { + // FIXME: Using an import will result in Element crashing + const UploadConfirmDialog = sdk.getComponent("dialogs.UploadConfirmDialog"); const { finished } = Modal.createTrackedDialog<[boolean, boolean]>('Upload Files confirmation', '', UploadConfirmDialog, { file, @@ -606,6 +609,8 @@ export default class ContentMessages { { fileName: upload.fileName }, ); } + // FIXME: Using an import will result in Element crashing + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createTrackedDialog('Upload failed', '', ErrorDialog, { title: _t('Upload Failed'), description: desc, From 6702e68778639fa2be3c4e9879bce39b6ab37ac6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 3 Jul 2021 09:36:18 +0200 Subject: [PATCH 141/221] Revert some changes due to them breaking tests (and hope it will work now) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/structures/HomePage.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/structures/HomePage.tsx b/src/components/structures/HomePage.tsx index 94239ea603..4ed160d493 100644 --- a/src/components/structures/HomePage.tsx +++ b/src/components/structures/HomePage.tsx @@ -21,6 +21,7 @@ import AutoHideScrollbar from './AutoHideScrollbar'; import { getHomePageUrl } from "../../utils/pages"; import { _t } from "../../languageHandler"; import SdkConfig from "../../SdkConfig"; +import * as sdk from "../../index"; import dis from "../../dispatcher/dispatcher"; import { Action } from "../../dispatcher/actions"; import BaseAvatar from "../views/avatars/BaseAvatar"; @@ -32,7 +33,6 @@ import MatrixClientContext from "../../contexts/MatrixClientContext"; import MiniAvatarUploader, { AVATAR_SIZE } from "../views/elements/MiniAvatarUploader"; import Analytics from "../../Analytics"; import CountlyAnalytics from "../../CountlyAnalytics"; -import EmbeddedPage from "./EmbeddedPage"; const onClickSendDm = () => { Analytics.trackEvent('home_page', 'button', 'dm'); @@ -96,6 +96,8 @@ const HomePage: React.FC = ({ justRegistered = false }) => { const pageUrl = getHomePageUrl(config); if (pageUrl) { + // FIXME: Using an import will result in wrench-element-tests failures + const EmbeddedPage = sdk.getComponent('structures.EmbeddedPage'); return ; } From b7ef7d2a47ae43af14b1f6a7f301aef518cacb33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 3 Jul 2021 10:06:42 +0200 Subject: [PATCH 142/221] Fifth batch of burning MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- .../views/emojipicker/EmojiPicker.tsx | 2 +- src/components/views/rooms/MessageComposer.tsx | 7 ++----- .../views/rooms/ThirdPartyMemberInfo.tsx | 6 ++---- .../views/settings/E2eAdvancedPanel.tsx | 3 +-- .../views/settings/EventIndexPanel.tsx | 3 +-- src/components/views/settings/SetIdServer.tsx | 11 ++++------- .../settings/tabs/room/RolesRoomSettingsTab.tsx | 8 ++------ .../tabs/user/MjolnirUserSettingsTab.tsx | 17 ++++------------- src/stores/RoomViewStore.tsx | 2 ++ 9 files changed, 19 insertions(+), 40 deletions(-) diff --git a/src/components/views/emojipicker/EmojiPicker.tsx b/src/components/views/emojipicker/EmojiPicker.tsx index 47e3823116..9b2e771e64 100644 --- a/src/components/views/emojipicker/EmojiPicker.tsx +++ b/src/components/views/emojipicker/EmojiPicker.tsx @@ -33,7 +33,7 @@ export const EMOJI_HEIGHT = 37; export const EMOJIS_PER_ROW = 8; interface IProps { - selectedEmojis: Set; + selectedEmojis?: Set; showQuickReactions?: boolean; onChoose(unicode: string): boolean; } diff --git a/src/components/views/rooms/MessageComposer.tsx b/src/components/views/rooms/MessageComposer.tsx index 7d61ba5ec6..2ce753cb8d 100644 --- a/src/components/views/rooms/MessageComposer.tsx +++ b/src/components/views/rooms/MessageComposer.tsx @@ -17,7 +17,6 @@ import React from 'react'; import classNames from 'classnames'; import { _t } from '../../../languageHandler'; import { MatrixClientPeg } from '../../../MatrixClientPeg'; -import * as sdk from '../../../index'; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { Room } from "matrix-js-sdk/src/models/room"; import { RoomMember } from "matrix-js-sdk/src/models/room-member"; @@ -44,13 +43,14 @@ import SendMessageComposer from "./SendMessageComposer"; import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload"; import { Action } from "../../../dispatcher/actions"; import EditorModel from "../../../editor/model"; +import EmojiPicker from '../emojipicker/EmojiPicker'; +import MemberStatusMessageAvatar from "../avatars/MemberStatusMessageAvatar"; interface IComposerAvatarProps { me: object; } function ComposerAvatar(props: IComposerAvatarProps) { - const MemberStatusMessageAvatar = sdk.getComponent('avatars.MemberStatusMessageAvatar'); return
    ; @@ -76,7 +76,6 @@ const EmojiButton = ({ addEmoji }) => { let contextMenu; if (menuDisplayed) { const buttonRect = button.current.getBoundingClientRect(); - const EmojiPicker = sdk.getComponent('emojipicker.EmojiPicker'); contextMenu = ; @@ -366,8 +365,6 @@ export default class MessageComposer extends React.Component { ]; if (!this.state.tombstone && this.state.canSendMessages) { - const SendMessageComposer = sdk.getComponent("rooms.SendMessageComposer"); - controls.push( this.messageComposerInput = c} diff --git a/src/components/views/rooms/ThirdPartyMemberInfo.tsx b/src/components/views/rooms/ThirdPartyMemberInfo.tsx index 4cdabede2f..2bcc3ead57 100644 --- a/src/components/views/rooms/ThirdPartyMemberInfo.tsx +++ b/src/components/views/rooms/ThirdPartyMemberInfo.tsx @@ -20,13 +20,14 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { Room } from "matrix-js-sdk/src/models/room"; import { _t } from "../../../languageHandler"; import dis from "../../../dispatcher/dispatcher"; -import * as sdk from "../../../index"; import Modal from "../../../Modal"; import { isValid3pidInvite } from "../../../RoomInvite"; import RoomAvatar from "../avatars/RoomAvatar"; import RoomName from "../elements/RoomName"; import { replaceableComponent } from "../../../utils/replaceableComponent"; import SettingsStore from "../../../settings/SettingsStore"; +import ErrorDialog from '../dialogs/ErrorDialog'; +import AccessibleButton from '../elements/AccessibleButton'; interface IProps { event: MatrixEvent; @@ -104,7 +105,6 @@ export default class ThirdPartyMemberInfo extends React.Component { - const SettingsFlag = sdk.getComponent('views.elements.SettingsFlag'); return
    {_t("Encryption")} diff --git a/src/components/views/settings/EventIndexPanel.tsx b/src/components/views/settings/EventIndexPanel.tsx index 59b4a166c0..73b324b739 100644 --- a/src/components/views/settings/EventIndexPanel.tsx +++ b/src/components/views/settings/EventIndexPanel.tsx @@ -18,7 +18,6 @@ import React from 'react'; import { _t } from '../../../languageHandler'; import SdkConfig from "../../../SdkConfig"; -import * as sdk from '../../../index'; import Modal from '../../../Modal'; import SettingsStore from "../../../settings/SettingsStore"; import AccessibleButton from "../elements/AccessibleButton"; @@ -27,6 +26,7 @@ import EventIndexPeg from "../../../indexing/EventIndexPeg"; import { SettingLevel } from "../../../settings/SettingLevel"; import { replaceableComponent } from "../../../utils/replaceableComponent"; import SeshatResetDialog from '../dialogs/SeshatResetDialog'; +import InlineSpinner from '../elements/InlineSpinner'; interface IState { enabling: boolean; @@ -147,7 +147,6 @@ export default class EventIndexPanel extends React.Component<{}, IState> { render() { let eventIndexingSettings = null; - const InlineSpinner = sdk.getComponent('elements.InlineSpinner'); const brand = SdkConfig.get().brand; if (EventIndexPeg.get() !== null) { diff --git a/src/components/views/settings/SetIdServer.tsx b/src/components/views/settings/SetIdServer.tsx index 4a73c82d9b..9180c98101 100644 --- a/src/components/views/settings/SetIdServer.tsx +++ b/src/components/views/settings/SetIdServer.tsx @@ -17,7 +17,6 @@ limitations under the License. import url from 'url'; import React from 'react'; import { _t } from "../../../languageHandler"; -import * as sdk from '../../../index'; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import Modal from '../../../Modal'; import dis from "../../../dispatcher/dispatcher"; @@ -28,6 +27,10 @@ import { getDefaultIdentityServerUrl, doesIdentityServerHaveTerms } from '../../ import { timeout } from "../../../utils/promise"; import { replaceableComponent } from "../../../utils/replaceableComponent"; import { ActionPayload } from '../../../dispatcher/payloads'; +import InlineSpinner from '../elements/InlineSpinner'; +import AccessibleButton from '../elements/AccessibleButton'; +import Field from '../elements/Field'; +import QuestionDialog from "../dialogs/QuestionDialog"; // We'll wait up to this long when checking for 3PID bindings on the IS. const REACHABILITY_TIMEOUT = 10000; // ms @@ -126,7 +129,6 @@ export default class SetIdServer extends React.Component { private getTooltip = () => { if (this.state.checking) { - const InlineSpinner = sdk.getComponent('views.elements.InlineSpinner'); return
    { _t("Checking server") } @@ -217,7 +219,6 @@ export default class SetIdServer extends React.Component { }; private showNoTermsWarning(fullUrl) { - const QuestionDialog = sdk.getComponent("views.dialogs.QuestionDialog"); const { finished } = Modal.createTrackedDialog('No Terms Warning', '', QuestionDialog, { title: _t("Identity server has no terms of service"), description: ( @@ -319,7 +320,6 @@ export default class SetIdServer extends React.Component { message = unboundMessage; } - const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); const { finished } = Modal.createTrackedDialog('Identity Server Bound Warning', '', QuestionDialog, { title, description: message, @@ -352,8 +352,6 @@ export default class SetIdServer extends React.Component { }; render() { - const AccessibleButton = sdk.getComponent('views.elements.AccessibleButton'); - const Field = sdk.getComponent('elements.Field'); const idServerUrl = this.state.currentClientIdServer; let sectionTitle; let bodyText; @@ -398,7 +396,6 @@ export default class SetIdServer extends React.Component { discoButtonContent = _t("Do not use an identity server"); } if (this.state.disconnectBusy) { - const InlineSpinner = sdk.getComponent('views.elements.InlineSpinner'); discoButtonContent = ; } discoSection =
    diff --git a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx index 67c7998751..f12499e7f9 100644 --- a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx @@ -17,7 +17,6 @@ limitations under the License. import React from 'react'; import { _t, _td } from "../../../../../languageHandler"; import { MatrixClientPeg } from "../../../../../MatrixClientPeg"; -import * as sdk from "../../../../.."; import AccessibleButton from "../../../elements/AccessibleButton"; import Modal from "../../../../../Modal"; import { replaceableComponent } from "../../../../../utils/replaceableComponent"; @@ -26,6 +25,8 @@ import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { RoomState } from "matrix-js-sdk/src/models/room-state"; import { compare } from "../../../../../utils/strings"; +import ErrorDialog from '../../../dialogs/ErrorDialog'; +import PowerSelector from "../../../elements/PowerSelector"; const plEventsToLabels = { // These will be translated for us later. @@ -76,7 +77,6 @@ interface IBannedUserProps { export class BannedUser extends React.Component { private onUnbanClick = (e) => { MatrixClientPeg.get().unban(this.props.member.roomId, this.props.member.userId).catch((err) => { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); console.error("Failed to unban: " + err); Modal.createTrackedDialog('Failed to unban', '', ErrorDialog, { title: _t('Error'), @@ -176,7 +176,6 @@ export default class RolesRoomSettingsTab extends React.Component { client.sendStateEvent(this.props.roomId, "m.room.power_levels", plContent).catch(e => { console.error(e); - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createTrackedDialog('Power level requirement change failed', '', ErrorDialog, { title: _t('Error changing power level requirement'), description: _t( @@ -203,7 +202,6 @@ export default class RolesRoomSettingsTab extends React.Component { client.sendStateEvent(this.props.roomId, "m.room.power_levels", plContent).catch(e => { console.error(e); - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createTrackedDialog('Power level change failed', '', ErrorDialog, { title: _t('Error changing power level'), description: _t( @@ -215,8 +213,6 @@ export default class RolesRoomSettingsTab extends React.Component { }; render() { - const PowerSelector = sdk.getComponent('elements.PowerSelector'); - const client = MatrixClientPeg.get(); const room = client.getRoom(this.props.roomId); const plEvent = room.currentState.getStateEvents('m.room.power_levels', ''); diff --git a/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.tsx b/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.tsx index 0a2b9f3212..41c44e65a0 100644 --- a/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.tsx @@ -22,8 +22,11 @@ import { ListRule } from "../../../../../mjolnir/ListRule"; import { BanList, RULE_SERVER, RULE_USER } from "../../../../../mjolnir/BanList"; import Modal from "../../../../../Modal"; import { MatrixClientPeg } from "../../../../../MatrixClientPeg"; -import * as sdk from "../../../../../index"; import { replaceableComponent } from "../../../../../utils/replaceableComponent"; +import ErrorDialog from "../../../dialogs/ErrorDialog"; +import QuestionDialog from "../../../dialogs/QuestionDialog"; +import AccessibleButton from "../../../elements/AccessibleButton"; +import Field from "../../../elements/Field"; interface IState { busy: boolean; @@ -68,7 +71,6 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState> } catch (e) { console.error(e); - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createTrackedDialog('Failed to add Mjolnir rule', '', ErrorDialog, { title: _t('Error adding ignored user/server'), description: _t('Something went wrong. Please try again or view your console for hints.'), @@ -90,7 +92,6 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState> } catch (e) { console.error(e); - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createTrackedDialog('Failed to subscribe to Mjolnir list', '', ErrorDialog, { title: _t('Error subscribing to list'), description: _t('Please verify the room ID or address and try again.'), @@ -108,7 +109,6 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState> } catch (e) { console.error(e); - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createTrackedDialog('Failed to remove Mjolnir rule', '', ErrorDialog, { title: _t('Error removing ignored user/server'), description: _t('Something went wrong. Please try again or view your console for hints.'), @@ -126,7 +126,6 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState> } catch (e) { console.error(e); - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createTrackedDialog('Failed to unsubscribe from Mjolnir list', '', ErrorDialog, { title: _t('Error unsubscribing from list'), description: _t('Please try again or view your console for hints.'), @@ -137,8 +136,6 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState> } private viewListRules(list: BanList) { - const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); - const room = MatrixClientPeg.get().getRoom(list.roomId); const name = room ? room.name : list.roomId; @@ -168,8 +165,6 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState> } private renderPersonalBanListRules() { - const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); - const list = Mjolnir.sharedInstance().getPersonalList(); const rules = list ? [...list.userRules, ...list.serverRules] : []; if (!list || rules.length <= 0) return {_t("You have not ignored anyone.")}; @@ -199,8 +194,6 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState> } private renderSubscribedBanLists() { - const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); - const personalList = Mjolnir.sharedInstance().getPersonalList(); const lists = Mjolnir.sharedInstance().lists.filter(b => { return personalList? personalList.roomId !== b.roomId : true; @@ -241,8 +234,6 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState> } render() { - const Field = sdk.getComponent('elements.Field'); - const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const brand = SdkConfig.get().brand; return ( diff --git a/src/stores/RoomViewStore.tsx b/src/stores/RoomViewStore.tsx index 87978df471..10f42f3166 100644 --- a/src/stores/RoomViewStore.tsx +++ b/src/stores/RoomViewStore.tsx @@ -164,6 +164,7 @@ class RoomViewStore extends Store { } break; case 'open_room_settings': { + // FIXME: Using an import will result in test failures const RoomSettingsDialog = sdk.getComponent("dialogs.RoomSettingsDialog"); Modal.createTrackedDialog('Room settings', '', RoomSettingsDialog, { roomId: payload.room_id || this.state.roomId, @@ -340,6 +341,7 @@ class RoomViewStore extends Store { } } + // FIXME: Using an import will result in test failures const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createTrackedDialog('Failed to join room', '', ErrorDialog, { title: _t("Failed to join room"), From ab4cd9d0fdd1a4960bd1026fb59745d1325008a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 3 Jul 2021 10:10:17 +0200 Subject: [PATCH 143/221] Delint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/rooms/MessageComposer.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/views/rooms/MessageComposer.tsx b/src/components/views/rooms/MessageComposer.tsx index 2ce753cb8d..b7015d2275 100644 --- a/src/components/views/rooms/MessageComposer.tsx +++ b/src/components/views/rooms/MessageComposer.tsx @@ -371,7 +371,6 @@ export default class MessageComposer extends React.Component { key="controls_input" room={this.props.room} placeholder={this.renderPlaceholderText()} - resizeNotifier={this.props.resizeNotifier} permalinkCreator={this.props.permalinkCreator} replyToEvent={this.props.replyToEvent} onChange={this.onChange} From 914de71e9c351e8617f8385f44faca35d75c8c3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 3 Jul 2021 10:44:03 +0200 Subject: [PATCH 144/221] Sixth batch of burning MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/dialogs/BugReportDialog.tsx | 14 ++++++-------- src/components/views/dialogs/InviteDialog.tsx | 10 +++------- src/components/views/dialogs/ShareDialog.tsx | 5 ++--- .../views/dialogs/UserSettingsDialog.tsx | 4 +--- src/components/views/elements/Field.tsx | 1 + .../views/elements/SpellCheckLanguagesDropdown.tsx | 3 +-- src/components/views/elements/ToggleSwitch.tsx | 3 +-- src/components/views/elements/TooltipButton.tsx | 3 +-- .../views/messages/MKeyVerificationRequest.tsx | 4 +--- .../views/right_panel/EncryptionInfo.tsx | 5 ++--- .../views/right_panel/EncryptionPanel.tsx | 1 + .../views/right_panel/VerificationPanel.tsx | 9 ++------- 12 files changed, 22 insertions(+), 40 deletions(-) diff --git a/src/components/views/dialogs/BugReportDialog.tsx b/src/components/views/dialogs/BugReportDialog.tsx index eeb3769bf9..6baf24f797 100644 --- a/src/components/views/dialogs/BugReportDialog.tsx +++ b/src/components/views/dialogs/BugReportDialog.tsx @@ -18,13 +18,17 @@ limitations under the License. */ import React from 'react'; -import * as sdk from '../../../index'; import SdkConfig from '../../../SdkConfig'; import Modal from '../../../Modal'; import { _t } from '../../../languageHandler'; import sendBugReport, { downloadBugReport } from '../../../rageshake/submit-rageshake'; import AccessibleButton from "../elements/AccessibleButton"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import QuestionDialog from "./QuestionDialog"; +import BaseDialog from "./BaseDialog"; +import Field from '../elements/Field'; +import Spinner from "../elements/Spinner"; +import DialogButtons from "../elements/DialogButtons"; interface IProps { onFinished: (success: boolean) => void; @@ -93,7 +97,6 @@ export default class BugReportDialog extends React.Component { }).then(() => { if (!this.unmounted) { this.props.onFinished(false); - const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); // N.B. first param is passed to piwik and so doesn't want i18n Modal.createTrackedDialog('Bug report sent', '', QuestionDialog, { title: _t('Logs sent'), @@ -160,11 +163,6 @@ export default class BugReportDialog extends React.Component { }; public render() { - const Loader = sdk.getComponent("elements.Spinner"); - const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); - const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); - const Field = sdk.getComponent('elements.Field'); - let error = null; if (this.state.err) { error =
    @@ -176,7 +174,7 @@ export default class BugReportDialog extends React.Component { if (this.state.busy) { progress = (
    - + {this.state.progress} ...
    ); diff --git a/src/components/views/dialogs/InviteDialog.tsx b/src/components/views/dialogs/InviteDialog.tsx index bbb5f24162..1df5f35ae9 100644 --- a/src/components/views/dialogs/InviteDialog.tsx +++ b/src/components/views/dialogs/InviteDialog.tsx @@ -18,7 +18,6 @@ import React, { createRef } from 'react'; import classNames from 'classnames'; import { _t, _td } from "../../../languageHandler"; -import * as sdk from "../../../index"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { makeRoomPermalink, makeUserPermalink } from "../../../utils/permalinks/Permalinks"; import DMRoomMap from "../../../utils/DMRoomMap"; @@ -65,6 +64,9 @@ import { copyPlaintext, selectText } from "../../../utils/strings"; import * as ContextMenu from "../../structures/ContextMenu"; import { toRightOf } from "../../structures/ContextMenu"; import GenericTextContextMenu from "../context_menus/GenericTextContextMenu"; +import QuestionDialog from "./QuestionDialog"; +import Spinner from "../elements/Spinner"; +import BaseDialog from "./BaseDialog"; // we have a number of types defined from the Matrix spec which can't reasonably be altered here. /* eslint-disable camelcase */ @@ -1046,7 +1048,6 @@ export default class InviteDialog extends React.PureComponent 0) { - const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog'); Modal.createTrackedDialog('Invite Paste Fail', '', QuestionDialog, { title: _t('Failed to find the following users'), description: _t( @@ -1158,7 +1159,6 @@ export default class InviteDialog extends React.PureComponent; diff --git a/src/components/views/dialogs/ShareDialog.tsx b/src/components/views/dialogs/ShareDialog.tsx index fb43db1a25..a3443ada02 100644 --- a/src/components/views/dialogs/ShareDialog.tsx +++ b/src/components/views/dialogs/ShareDialog.tsx @@ -22,7 +22,6 @@ 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 "../elements/QRCode"; import { RoomPermalinkCreator, makeGroupPermalink, makeUserPermalink } from "../../../utils/permalinks/Permalinks"; @@ -35,6 +34,8 @@ import { IDialogProps } from "./IDialogProps"; import SettingsStore from "../../../settings/SettingsStore"; import { UIFeature } from "../../../settings/UIFeature"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import BaseDialog from "./BaseDialog"; +import GenericTextContextMenu from "../context_menus/GenericTextContextMenu.js"; const socials = [ { @@ -119,7 +120,6 @@ export default class ShareDialog extends React.PureComponent { 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'), @@ -230,7 +230,6 @@ export default class ShareDialog extends React.PureComponent { ; } - const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); return } render() { - const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); - return ( { }); // Handle displaying feedback on validity + // FIXME: Using an import will result in test failures const Tooltip = sdk.getComponent("elements.Tooltip"); let fieldTooltip; if (tooltipContent || this.state.feedback) { diff --git a/src/components/views/elements/SpellCheckLanguagesDropdown.tsx b/src/components/views/elements/SpellCheckLanguagesDropdown.tsx index 1678bdb33a..5230042c38 100644 --- a/src/components/views/elements/SpellCheckLanguagesDropdown.tsx +++ b/src/components/views/elements/SpellCheckLanguagesDropdown.tsx @@ -17,11 +17,11 @@ limitations under the License. import React from 'react'; import Dropdown from "../../views/elements/Dropdown"; -import * as sdk from '../../../index'; import PlatformPeg from "../../../PlatformPeg"; import SettingsStore from "../../../settings/SettingsStore"; import { _t } from "../../../languageHandler"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import Spinner from "./Spinner"; function languageMatchesSearchQuery(query, language) { if (language.label.toUpperCase().includes(query.toUpperCase())) return true; @@ -84,7 +84,6 @@ export default class SpellCheckLanguagesDropdown extends React.Component; } diff --git a/src/components/views/elements/ToggleSwitch.tsx b/src/components/views/elements/ToggleSwitch.tsx index 7315cc6383..c439ef4f85 100644 --- a/src/components/views/elements/ToggleSwitch.tsx +++ b/src/components/views/elements/ToggleSwitch.tsx @@ -17,7 +17,7 @@ limitations under the License. import React from "react"; import classNames from "classnames"; -import * as sdk from "../../../index"; +import AccessibleButton from "./AccessibleButton"; interface IProps { // Whether or not this toggle is in the 'on' position. @@ -43,7 +43,6 @@ export default ({ checked, disabled = false, onChange, ...props }: IProps) => { "mx_ToggleSwitch_enabled": !disabled, }); - const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); return ( { }; render() { - const Tooltip = sdk.getComponent("elements.Tooltip"); const tip = this.state.hover ? { } public render() { - const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); - const { mxEvent } = this.props; const request = mxEvent.verificationRequest; diff --git a/src/components/views/right_panel/EncryptionInfo.tsx b/src/components/views/right_panel/EncryptionInfo.tsx index c34cf18710..e74caf8457 100644 --- a/src/components/views/right_panel/EncryptionInfo.tsx +++ b/src/components/views/right_panel/EncryptionInfo.tsx @@ -16,13 +16,13 @@ limitations under the License. import React from "react"; -import * as sdk from "../../../index"; import { _t } from "../../../languageHandler"; import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { User } from "matrix-js-sdk/src/models/user"; +import AccessibleButton from "../elements/AccessibleButton"; +import Spinner from "../elements/Spinner"; export const PendingActionSpinner = ({ text }) => { - const Spinner = sdk.getComponent('elements.Spinner'); return
    { text } @@ -64,7 +64,6 @@ const EncryptionInfo: React.FC = ({ } content = ; } else { - const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); content = ( {_t("Start Verification")} diff --git a/src/components/views/right_panel/EncryptionPanel.tsx b/src/components/views/right_panel/EncryptionPanel.tsx index 251c04d3cc..9ed791c229 100644 --- a/src/components/views/right_panel/EncryptionPanel.tsx +++ b/src/components/views/right_panel/EncryptionPanel.tsx @@ -81,6 +81,7 @@ const EncryptionPanel: React.FC = (props: IProps) => { const changeHandler = useCallback(() => { // handle transitions -> cancelled for mismatches which fire a modal instead of showing a card if (request && request.cancelled && MISMATCHES.includes(request.cancellationCode)) { + // FIXME: Using an import will result in test failures const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createTrackedDialog("Verification failed", "insecure", ErrorDialog, { headerImage: require("../../../../res/img/e2e/warning.svg"), diff --git a/src/components/views/right_panel/VerificationPanel.tsx b/src/components/views/right_panel/VerificationPanel.tsx index 6087923057..a4d4d2fa30 100644 --- a/src/components/views/right_panel/VerificationPanel.tsx +++ b/src/components/views/right_panel/VerificationPanel.tsx @@ -17,7 +17,6 @@ limitations under the License. import React from "react"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; -import * as sdk from '../../../index'; import { verificationMethods } from 'matrix-js-sdk/src/crypto'; import { SCAN_QR_CODE_METHOD } from "matrix-js-sdk/src/crypto/verification/QRCode"; import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; @@ -38,6 +37,8 @@ import { } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; import Spinner from "../elements/Spinner"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import AccessibleButton from "../elements/AccessibleButton"; +import VerificationShowSas from "../verification/VerificationShowSas"; // XXX: Should be defined in matrix-js-sdk enum VerificationPhase { @@ -81,7 +82,6 @@ export default class VerificationPanel extends React.PureComponent

    {_t("Verified")}

    @@ -282,8 +280,6 @@ export default class VerificationPanel extends React.PureComponent Date: Sat, 3 Jul 2021 10:59:57 +0200 Subject: [PATCH 145/221] Seventh batch of burning MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/elements/TooltipButton.tsx | 2 +- src/components/views/messages/SenderProfile.tsx | 2 +- src/components/views/rooms/EventTile.tsx | 17 ++++++++--------- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/components/views/elements/TooltipButton.tsx b/src/components/views/elements/TooltipButton.tsx index d60b8a341b..26e46c7da8 100644 --- a/src/components/views/elements/TooltipButton.tsx +++ b/src/components/views/elements/TooltipButton.tsx @@ -20,7 +20,7 @@ import { replaceableComponent } from "../../../utils/replaceableComponent"; import Tooltip from './Tooltip'; interface IProps { - helpText: string; + helpText: React.ReactNode | string; } interface IState { diff --git a/src/components/views/messages/SenderProfile.tsx b/src/components/views/messages/SenderProfile.tsx index 11c3ca4e3c..bdae9cec4a 100644 --- a/src/components/views/messages/SenderProfile.tsx +++ b/src/components/views/messages/SenderProfile.tsx @@ -24,7 +24,7 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event"; interface IProps { mxEvent: MatrixEvent; - onClick(): void; + onClick?(): void; enableFlair: boolean; } diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index c9d1040433..7cceef4a86 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -47,6 +47,13 @@ import { StaticNotificationState } from "../../../stores/notifications/StaticNot import NotificationBadge from "./NotificationBadge"; import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload"; import { Action } from '../../../dispatcher/actions'; +import MemberAvatar from '../avatars/MemberAvatar'; +import SenderProfile from '../messages/SenderProfile'; +import MessageTimestamp from '../messages/MessageTimestamp'; +import TooltipButton from '../elements/TooltipButton'; +import ReadReceiptMarker from "./ReadReceiptMarker"; +import MessageActionBar from "../messages/MessageActionBar"; +import ReactionsRow from '../messages/ReactionsRow'; const eventTileTypes = { [EventType.RoomMessage]: 'messages.MessageEvent', @@ -666,7 +673,6 @@ export default class EventTile extends React.Component { ); } - const ReadReceiptMarker = sdk.getComponent('rooms.ReadReceiptMarker'); const avatars = []; const receiptOffset = 15; let left = 0; @@ -733,7 +739,7 @@ export default class EventTile extends React.Component { ); } - onSenderProfileClick = event => { + onSenderProfileClick = () => { const mxEvent = this.props.mxEvent; dis.dispatch({ action: Action.ComposerInsert, @@ -841,10 +847,6 @@ export default class EventTile extends React.Component { }; render() { - const MessageTimestamp = sdk.getComponent('messages.MessageTimestamp'); - const SenderProfile = sdk.getComponent('messages.SenderProfile'); - const MemberAvatar = sdk.getComponent('avatars.MemberAvatar'); - //console.info("EventTile showUrlPreview for %s is %s", this.props.mxEvent.getId(), this.props.showUrlPreview); const content = this.props.mxEvent.getContent(); @@ -987,7 +989,6 @@ export default class EventTile extends React.Component { } } - const MessageActionBar = sdk.getComponent('messages.MessageActionBar'); const actionBar = !isEditing ? { { 'requestLink': (sub) => { sub } }, ); - const TooltipButton = sdk.getComponent('elements.TooltipButton'); const keyRequestInfo = isEncryptionFailure && !isRedacted ?
    @@ -1038,7 +1038,6 @@ export default class EventTile extends React.Component { let reactionsRow; if (!isRedacted) { - const ReactionsRow = sdk.getComponent('messages.ReactionsRow'); reactionsRow = Date: Sat, 3 Jul 2021 11:24:33 +0200 Subject: [PATCH 146/221] Eighth batch of burning MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/SlashCommands.tsx | 19 ++++++------------- src/components/structures/auth/SoftLogout.tsx | 16 ++++++---------- .../auth/InteractiveAuthEntryComponents.tsx | 18 ++++++------------ .../dialogs/ConfirmAndWaitRedactDialog.tsx | 9 ++++----- .../views/dialogs/ConfirmUserActionDialog.tsx | 10 ++++------ src/components/views/dialogs/TermsDialog.tsx | 6 ++---- .../ConfirmDestroyCrossSigningDialog.tsx | 6 ++---- 7 files changed, 30 insertions(+), 54 deletions(-) diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx index 128ca9e5e2..7753ff6f75 100644 --- a/src/SlashCommands.tsx +++ b/src/SlashCommands.tsx @@ -23,7 +23,6 @@ import { User } from "matrix-js-sdk/src/models/user"; import * as ContentHelpers from 'matrix-js-sdk/src/content-helpers'; import { MatrixClientPeg } from './MatrixClientPeg'; import dis from './dispatcher/dispatcher'; -import * as sdk from './index'; import { _t, _td } from './languageHandler'; import Modal from './Modal'; import MultiInviter from './utils/MultiInviter'; @@ -50,6 +49,12 @@ import { UIFeature } from "./settings/UIFeature"; import { CHAT_EFFECTS } from "./effects"; import CallHandler from "./CallHandler"; import { guessAndSetDMRoom } from "./Rooms"; +import UploadConfirmDialog from './components/views/dialogs/UploadConfirmDialog'; +import ErrorDialog from './components/views/dialogs/ErrorDialog'; +import DevtoolsDialog from './components/views/dialogs/DevtoolsDialog'; +import RoomUpgradeWarningDialog from "./components/views/dialogs/RoomUpgradeWarningDialog"; +import InfoDialog from "./components/views/dialogs/InfoDialog"; +import SlashCommandHelpDialog from "./components/views/dialogs/SlashCommandHelpDialog"; // XXX: workaround for https://github.com/microsoft/TypeScript/issues/31816 interface HTMLInputEvent extends Event { @@ -63,7 +68,6 @@ const singleMxcUpload = async (): Promise => { fileSelector.onchange = (ev: HTMLInputEvent) => { const file = ev.target.files[0]; - const UploadConfirmDialog = sdk.getComponent("dialogs.UploadConfirmDialog"); Modal.createTrackedDialog('Upload Files confirmation', '', UploadConfirmDialog, { file, onFinished: (shouldContinue) => { @@ -246,7 +250,6 @@ export const Commands = [ args: '', description: _td('Searches DuckDuckGo for results'), runFn: function() { - const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog'); // TODO Don't explain this away, actually show a search UI here. Modal.createTrackedDialog('Slash Commands', '/ddg is not a command', ErrorDialog, { title: _t('/ddg is not a command'), @@ -269,8 +272,6 @@ export const Commands = [ return reject(_t("You do not have the required permissions to use this command.")); } - const RoomUpgradeWarningDialog = sdk.getComponent("dialogs.RoomUpgradeWarningDialog"); - const { finished } = Modal.createTrackedDialog('Slash Commands', 'upgrade room confirmation', RoomUpgradeWarningDialog, { roomId: roomId, targetVersion: args }, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true); @@ -314,7 +315,6 @@ export const Commands = [ if (checkForUpgradeFn) cli.removeListener('Room', checkForUpgradeFn); - const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog'); Modal.createTrackedDialog('Slash Commands', 'room upgrade error', ErrorDialog, { title: _t('Error upgrading room'), description: _t( @@ -434,7 +434,6 @@ export const Commands = [ const topic = topicEvents && topicEvents.getContent().topic; const topicHtml = topic ? linkifyAndSanitizeHtml(topic) : _t('This room has no topic.'); - const InfoDialog = sdk.getComponent('dialogs.InfoDialog'); Modal.createTrackedDialog('Slash Commands', 'Topic', InfoDialog, { title: room.name, description:
    , @@ -737,7 +736,6 @@ export const Commands = [ ignoredUsers.push(userId); // de-duped internally in the js-sdk return success( cli.setIgnoredUsers(ignoredUsers).then(() => { - const InfoDialog = sdk.getComponent('dialogs.InfoDialog'); Modal.createTrackedDialog('Slash Commands', 'User ignored', InfoDialog, { title: _t('Ignored user'), description:
    @@ -768,7 +766,6 @@ export const Commands = [ if (index !== -1) ignoredUsers.splice(index, 1); return success( cli.setIgnoredUsers(ignoredUsers).then(() => { - const InfoDialog = sdk.getComponent('dialogs.InfoDialog'); Modal.createTrackedDialog('Slash Commands', 'User unignored', InfoDialog, { title: _t('Unignored user'), description:
    @@ -838,7 +835,6 @@ export const Commands = [ command: 'devtools', description: _td('Opens the Developer Tools dialog'), runFn: function(roomId) { - const DevtoolsDialog = sdk.getComponent('dialogs.DevtoolsDialog'); Modal.createDialog(DevtoolsDialog, { roomId }); return success(); }, @@ -943,7 +939,6 @@ export const Commands = [ await cli.setDeviceVerified(userId, deviceId, true); // Tell the user we verified everything - const InfoDialog = sdk.getComponent('dialogs.InfoDialog'); Modal.createTrackedDialog('Slash Commands', 'Verified key', InfoDialog, { title: _t('Verified key'), description:
    @@ -1000,8 +995,6 @@ export const Commands = [ command: "help", description: _td("Displays list of commands with usages and descriptions"), runFn: function() { - const SlashCommandHelpDialog = sdk.getComponent('dialogs.SlashCommandHelpDialog'); - Modal.createTrackedDialog('Slash Commands', 'Help', SlashCommandHelpDialog); return success(); }, diff --git a/src/components/structures/auth/SoftLogout.tsx b/src/components/structures/auth/SoftLogout.tsx index 3790028fea..d232f55dd1 100644 --- a/src/components/structures/auth/SoftLogout.tsx +++ b/src/components/structures/auth/SoftLogout.tsx @@ -16,7 +16,6 @@ limitations under the License. import React from 'react'; import { _t } from '../../../languageHandler'; -import * as sdk from '../../../index'; import dis from '../../../dispatcher/dispatcher'; import * as Lifecycle from '../../../Lifecycle'; import Modal from '../../../Modal'; @@ -26,6 +25,12 @@ import AuthPage from "../../views/auth/AuthPage"; import { SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY } from "../../../BasePlatform"; import SSOButtons from "../../views/elements/SSOButtons"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import ConfirmWipeDeviceDialog from '../../views/dialogs/ConfirmWipeDeviceDialog'; +import Field from '../../views/elements/Field'; +import AccessibleButton from '../../views/elements/AccessibleButton'; +import Spinner from "../../views/elements/Spinner"; +import AuthHeader from "../../views/auth/AuthHeader"; +import AuthBody from "../../views/auth/AuthBody"; const LOGIN_VIEW = { LOADING: 1, @@ -94,7 +99,6 @@ export default class SoftLogout extends React.Component { } onClearAll = () => { - const ConfirmWipeDeviceDialog = sdk.getComponent('dialogs.ConfirmWipeDeviceDialog'); Modal.createTrackedDialog('Clear Data', 'Soft Logout', ConfirmWipeDeviceDialog, { onFinished: (wipeData) => { if (!wipeData) return; @@ -202,7 +206,6 @@ export default class SoftLogout extends React.Component { private renderSignInSection() { if (this.state.loginView === LOGIN_VIEW.LOADING) { - const Spinner = sdk.getComponent("elements.Spinner"); return ; } @@ -214,9 +217,6 @@ export default class SoftLogout extends React.Component { } if (this.state.loginView === LOGIN_VIEW.PASSWORD) { - const Field = sdk.getComponent("elements.Field"); - const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); - let error = null; if (this.state.errorText) { error = {this.state.errorText}; @@ -286,10 +286,6 @@ export default class SoftLogout extends React.Component { } render() { - const AuthHeader = sdk.getComponent("auth.AuthHeader"); - const AuthBody = sdk.getComponent("auth.AuthBody"); - const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); - return ( diff --git a/src/components/views/auth/InteractiveAuthEntryComponents.tsx b/src/components/views/auth/InteractiveAuthEntryComponents.tsx index e002eb5717..4b1ecec740 100644 --- a/src/components/views/auth/InteractiveAuthEntryComponents.tsx +++ b/src/components/views/auth/InteractiveAuthEntryComponents.tsx @@ -18,7 +18,6 @@ import React, { ChangeEvent, createRef, FormEvent, MouseEvent } from 'react'; import classNames from 'classnames'; import { MatrixClient } from "matrix-js-sdk/src/client"; -import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; import SettingsStore from "../../../settings/SettingsStore"; import AccessibleButton from "../elements/AccessibleButton"; @@ -26,6 +25,8 @@ import Spinner from "../elements/Spinner"; import CountlyAnalytics from "../../../CountlyAnalytics"; import { replaceableComponent } from "../../../utils/replaceableComponent"; import { LocalisedPolicy, Policies } from '../../../Terms'; +import Field from '../elements/Field'; +import CaptchaForm from "./CaptchaForm"; /* This file contains a collection of components which are used by the * InteractiveAuth to prompt the user to enter the information needed @@ -164,8 +165,7 @@ export class PasswordAuthEntry extends React.Component; + submitButtonOrSpinner = ; } else { submitButtonOrSpinner = (

    { _t("Confirm your identity by entering your account password below.") }

    @@ -236,13 +234,11 @@ export class RecaptchaAuthEntry extends React.Component; + return ; } let errorText = this.props.errorText; - const CaptchaForm = sdk.getComponent("views.auth.CaptchaForm"); let sitePublicKey; if (!this.props.stageParams || !this.props.stageParams.public_key) { errorText = _t( @@ -390,8 +386,7 @@ export class TermsAuthEntry extends React.Component; + return ; } const checkboxes = []; @@ -590,8 +585,7 @@ export class MsisdnAuthEntry extends React.Component; + return ; } else { const enableSubmit = Boolean(this.state.token); const submitClasses = classNames({ diff --git a/src/components/views/dialogs/ConfirmAndWaitRedactDialog.tsx b/src/components/views/dialogs/ConfirmAndWaitRedactDialog.tsx index 90b749b959..d21fde329c 100644 --- a/src/components/views/dialogs/ConfirmAndWaitRedactDialog.tsx +++ b/src/components/views/dialogs/ConfirmAndWaitRedactDialog.tsx @@ -15,9 +15,12 @@ limitations under the License. */ import React from 'react'; -import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import ConfirmRedactDialog from './ConfirmRedactDialog'; +import ErrorDialog from './ErrorDialog'; +import BaseDialog from "./BaseDialog"; +import Spinner from "../elements/Spinner"; interface IProps { redact: () => Promise; @@ -73,7 +76,6 @@ export default class ConfirmAndWaitRedactDialog extends React.PureComponent ); } else { - const BaseDialog = sdk.getComponent("dialogs.BaseDialog"); - const Spinner = sdk.getComponent('elements.Spinner'); return ( ; } } diff --git a/src/components/views/dialogs/ConfirmUserActionDialog.tsx b/src/components/views/dialogs/ConfirmUserActionDialog.tsx index 78fae390b5..cbef474c69 100644 --- a/src/components/views/dialogs/ConfirmUserActionDialog.tsx +++ b/src/components/views/dialogs/ConfirmUserActionDialog.tsx @@ -17,11 +17,14 @@ limitations under the License. import React from 'react'; import { MatrixClient } from 'matrix-js-sdk/src/client'; import { RoomMember } from "matrix-js-sdk/src/models/room-member"; -import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; import { GroupMemberType } from '../../../groups'; import { replaceableComponent } from "../../../utils/replaceableComponent"; import { mediaFromMxc } from "../../../customisations/Media"; +import MemberAvatar from '../avatars/MemberAvatar'; +import BaseAvatar from '../avatars/BaseAvatar'; +import BaseDialog from "./BaseDialog"; +import DialogButtons from "../elements/DialogButtons"; interface IProps { // matrix-js-sdk (room) member object. Supply either this or 'groupMember' @@ -67,11 +70,6 @@ export default class ConfirmUserActionDialog extends React.Component { }; public render() { - const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); - const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); - const MemberAvatar = sdk.getComponent("views.avatars.MemberAvatar"); - const BaseAvatar = sdk.getComponent("views.avatars.BaseAvatar"); - const confirmButtonClass = this.props.danger ? 'danger' : ''; let reasonBox; diff --git a/src/components/views/dialogs/TermsDialog.tsx b/src/components/views/dialogs/TermsDialog.tsx index 02a779743b..afa732033f 100644 --- a/src/components/views/dialogs/TermsDialog.tsx +++ b/src/components/views/dialogs/TermsDialog.tsx @@ -16,11 +16,12 @@ limitations under the License. import url from 'url'; import React from 'react'; -import * as sdk from '../../../index'; import { _t, pickBestLanguage } from '../../../languageHandler'; import { replaceableComponent } from "../../../utils/replaceableComponent"; import { SERVICE_TYPES } from "matrix-js-sdk/src/service-types"; +import DialogButtons from "../elements/DialogButtons"; +import BaseDialog from "./BaseDialog"; interface ITermsCheckboxProps { onChange: (url: string, checked: boolean) => void; @@ -117,9 +118,6 @@ export default class TermsDialog extends React.PureComponent void; @@ -34,9 +35,6 @@ export default class ConfirmDestroyCrossSigningDialog extends React.Component Date: Sat, 3 Jul 2021 11:51:23 +0200 Subject: [PATCH 147/221] Ninth batch of burning MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/structures/LoggedInView.tsx | 12 +++++------- src/components/structures/RoomView.tsx | 22 ++++++++-------------- 2 files changed, 13 insertions(+), 21 deletions(-) diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx index 1f870da900..171de4e3bd 100644 --- a/src/components/structures/LoggedInView.tsx +++ b/src/components/structures/LoggedInView.tsx @@ -24,7 +24,6 @@ import { Key } from '../../Keyboard'; import PageTypes from '../../PageTypes'; import MediaDeviceHandler from '../../MediaDeviceHandler'; import { fixupColorFonts } from '../../utils/FontManager'; -import * as sdk from '../../index'; import dis from '../../dispatcher/dispatcher'; import { IMatrixClientCreds } from '../../MatrixClientPeg'; import SettingsStore from "../../settings/SettingsStore"; @@ -59,6 +58,11 @@ import { replaceableComponent } from "../../utils/replaceableComponent"; import CallHandler, { CallHandlerEvent } from '../../CallHandler'; import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call'; import AudioFeedArrayForCall from '../views/voip/AudioFeedArrayForCall'; +import RoomView from './RoomView'; +import ToastContainer from './ToastContainer'; +import MyGroups from "./MyGroups"; +import UserView from "./UserView"; +import GroupView from "./GroupView"; // We need to fetch each pinned message individually (if we don't already have it) // so each pinned message may trigger a request. Limit the number per room for sanity. @@ -567,12 +571,6 @@ class LoggedInView extends React.Component { }; render() { - const RoomView = sdk.getComponent('structures.RoomView'); - const UserView = sdk.getComponent('structures.UserView'); - const GroupView = sdk.getComponent('structures.GroupView'); - const MyGroups = sdk.getComponent('structures.MyGroups'); - const ToastContainer = sdk.getComponent('structures.ToastContainer'); - let pageElement; switch (this.props.page_type) { diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 2c8fc08dac..5c6d032596 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -34,7 +34,6 @@ import { RoomPermalinkCreator } from '../../utils/permalinks/Permalinks'; import ResizeNotifier from '../../utils/ResizeNotifier'; import ContentMessages from '../../ContentMessages'; import Modal from '../../Modal'; -import * as sdk from '../../index'; import CallHandler, { PlaceCallType } from '../../CallHandler'; import dis from '../../dispatcher/dispatcher'; import * as Rooms from '../../Rooms'; @@ -82,6 +81,14 @@ import { replaceableComponent } from "../../utils/replaceableComponent"; import UIStore from "../../stores/UIStore"; import EditorStateTransfer from "../../utils/EditorStateTransfer"; import { throttle } from "lodash"; +import ErrorDialog from '../views/dialogs/ErrorDialog'; +import SearchResultTile from '../views/rooms/SearchResultTile'; +import Spinner from "../views/elements/Spinner"; +import UploadBar from './UploadBar'; +import RoomStatusBar from "./RoomStatusBar"; +import MessageComposer from '../views/rooms/MessageComposer'; +import JumpToBottomButton from "../views/rooms/JumpToBottomButton"; +import TopUnreadMessagesBar from "../views/rooms/TopUnreadMessagesBar"; const DEBUG = false; let debuglog = function(msg: string) {}; @@ -1328,7 +1335,6 @@ export default class RoomView extends React.Component { searchResults: results, }); }, (error) => { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); console.error("Search failed", error); Modal.createTrackedDialog('Search failed', '', ErrorDialog, { title: _t("Search failed"), @@ -1344,9 +1350,6 @@ export default class RoomView extends React.Component { } private getSearchResultTiles() { - const SearchResultTile = sdk.getComponent('rooms.SearchResultTile'); - const Spinner = sdk.getComponent("elements.Spinner"); - // XXX: todo: merge overlapping results somehow? // XXX: why doesn't searching on name work? @@ -1466,7 +1469,6 @@ export default class RoomView extends React.Component { console.error("Failed to reject invite: %s", error); const msg = error.message ? error.message : JSON.stringify(error); - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createTrackedDialog('Failed to reject invite', '', ErrorDialog, { title: _t("Failed to reject invite"), description: msg, @@ -1500,7 +1502,6 @@ export default class RoomView extends React.Component { console.error("Failed to reject invite: %s", error); const msg = error.message ? error.message : JSON.stringify(error); - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createTrackedDialog('Failed to reject invite', '', ErrorDialog, { title: _t("Failed to reject invite"), description: msg, @@ -1834,10 +1835,8 @@ export default class RoomView extends React.Component { let isStatusAreaExpanded = true; if (ContentMessages.sharedInstance().getCurrentUploads().length > 0) { - const UploadBar = sdk.getComponent('structures.UploadBar'); statusBar = ; } else if (!this.state.searchResults) { - const RoomStatusBar = sdk.getComponent('structures.RoomStatusBar'); isStatusAreaExpanded = this.state.statusBarVisible; statusBar = { myMembership === 'join' && !this.state.searchResults ); if (canSpeak) { - const MessageComposer = sdk.getComponent('rooms.MessageComposer'); messageComposer = { let topUnreadMessagesBar = null; // Do not show TopUnreadMessagesBar if we have search results showing, it makes no sense if (this.state.showTopUnreadMessagesBar && !this.state.searchResults) { - const TopUnreadMessagesBar = sdk.getComponent('rooms.TopUnreadMessagesBar'); topUnreadMessagesBar = ( ); @@ -2042,7 +2037,6 @@ export default class RoomView extends React.Component { let jumpToBottom; // Do not show JumpToBottomButton if we have search results showing, it makes no sense if (!this.state.atEndOfLiveTimeline && !this.state.searchResults) { - const JumpToBottomButton = sdk.getComponent('rooms.JumpToBottomButton'); jumpToBottom = ( 0} numUnreadMessages={this.state.numUnreadMessages} From 5d054519dc1269baee1aba99dacb1195c544e2fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 3 Jul 2021 11:57:05 +0200 Subject: [PATCH 148/221] Tenth batch of burning MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/structures/LoggedInView.tsx | 4 +- src/components/structures/MatrixChat.tsx | 62 +++++++++---------- .../structures/auth/Registration.tsx | 10 +-- 3 files changed, 35 insertions(+), 41 deletions(-) diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx index 171de4e3bd..5a26967cb0 100644 --- a/src/components/structures/LoggedInView.tsx +++ b/src/components/structures/LoggedInView.tsx @@ -82,8 +82,8 @@ interface IProps { hideToSRUsers: boolean; resizeNotifier: ResizeNotifier; // eslint-disable-next-line camelcase - page_type: string; - autoJoin: boolean; + page_type?: string; + autoJoin?: boolean; threepidInvite?: IThreepidInvite; roomOobData?: IOOBData; currentRoomId: string; diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 06c7bfac8b..64d32bbe62 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -85,9 +85,26 @@ import RoomListStore from "../../stores/room-list/RoomListStore"; import { RoomUpdateCause } from "../../stores/room-list/models"; import defaultDispatcher from "../../dispatcher/dispatcher"; import SecurityCustomisations from "../../customisations/Security"; +import Spinner from "../views/elements/Spinner"; +import QuestionDialog from "../views/dialogs/QuestionDialog"; +import UserSettingsDialog from '../views/dialogs/UserSettingsDialog'; +import CreateGroupDialog from '../views/dialogs/CreateGroupDialog'; +import CreateRoomDialog from '../views/dialogs/CreateRoomDialog'; +import RoomDirectory from './RoomDirectory'; +import KeySignatureUploadFailedDialog from "../views/dialogs/KeySignatureUploadFailedDialog"; +import IncomingSasDialog from "../views/dialogs/IncomingSasDialog"; +import CompleteSecurity from "./auth/CompleteSecurity"; +import LoggedInView from './LoggedInView'; +import Welcome from "../views/auth/Welcome"; +import ForgotPassword from "./auth/ForgotPassword"; +import E2eSetup from "./auth/E2eSetup"; +import Registration from './auth/Registration'; +import Login from "./auth/Login"; +import ErrorBoundary from '../views/elements/ErrorBoundary'; import PerformanceMonitor, { PerformanceEntryNames } from "../../performance"; import UIStore, { UI_EVENTS } from "../../stores/UIStore"; +import SoftLogout from './auth/SoftLogout'; /** constants for MatrixChat.state.view */ export enum Views { @@ -156,7 +173,12 @@ interface IRoomInfo { /* eslint-enable camelcase */ interface IProps { // TODO type things better - config: Record; + config: { + piwik: { + policyUrl: string; + }; + [key: string]: any; + }; serverConfig?: ValidatedServerConfig; onNewScreen: (screen: string, replaceLast: boolean) => void; enableGuest?: boolean; @@ -519,7 +541,6 @@ export default class MatrixChat extends React.PureComponent { onAction = (payload) => { // console.log(`MatrixClientPeg.onAction: ${payload.action}`); - const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); // Start the onboarding process for certain actions if (MatrixClientPeg.get() && MatrixClientPeg.get().isGuest() && @@ -613,8 +634,7 @@ export default class MatrixChat extends React.PureComponent { onFinished: (confirm) => { if (confirm) { // FIXME: controller shouldn't be loading a view :( - const Loader = sdk.getComponent("elements.Spinner"); - const modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner'); + const modal = Modal.createDialog(Spinner, null, 'mx_Dialog_spinner'); MatrixClientPeg.get().leave(payload.room_id).then(() => { modal.close(); @@ -650,7 +670,6 @@ export default class MatrixChat extends React.PureComponent { } case Action.ViewUserSettings: { const tabPayload = payload as OpenToTabPayload; - const UserSettingsDialog = sdk.getComponent("dialogs.UserSettingsDialog"); Modal.createTrackedDialog('User settings', '', UserSettingsDialog, { initialTabId: tabPayload.initialTabId }, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true); @@ -663,11 +682,12 @@ export default class MatrixChat extends React.PureComponent { this.createRoom(payload.public, payload.defaultName); break; case 'view_create_group': { - let CreateGroupDialog = sdk.getComponent("dialogs.CreateGroupDialog"); - if (SettingsStore.getValue("feature_communities_v2_prototypes")) { - CreateGroupDialog = CreateCommunityPrototypeDialog; - } - Modal.createTrackedDialog('Create Community', '', CreateGroupDialog); + const prototype = SettingsStore.getValue("feature_communities_v2_prototypes"); + Modal.createTrackedDialog( + 'Create Community', + '', + prototype ? CreateCommunityPrototypeDialog : CreateGroupDialog, + ); break; } case Action.ViewRoomDirectory: { @@ -677,7 +697,6 @@ export default class MatrixChat extends React.PureComponent { room_id: SpaceStore.instance.activeSpace.roomId, }); } else { - const RoomDirectory = sdk.getComponent("structures.RoomDirectory"); Modal.createTrackedDialog('Room directory', '', RoomDirectory, { initialText: payload.initialText, }, 'mx_RoomDirectory_dialogWrapper', false, true); @@ -1019,7 +1038,6 @@ export default class MatrixChat extends React.PureComponent { } } - const CreateRoomDialog = sdk.getComponent('dialogs.CreateRoomDialog'); const modal = Modal.createTrackedDialog('Create Room', '', CreateRoomDialog, { defaultPublic, defaultName, @@ -1116,7 +1134,6 @@ export default class MatrixChat extends React.PureComponent { } private leaveRoom(roomId: string) { - const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); const roomToLeave = MatrixClientPeg.get().getRoom(roomId); const warnings = this.leaveRoomWarnings(roomId); @@ -1143,8 +1160,7 @@ export default class MatrixChat extends React.PureComponent { const d = leaveRoomBehaviour(roomId); // FIXME: controller shouldn't be loading a view :( - const Loader = sdk.getComponent("elements.Spinner"); - const modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner'); + const modal = Modal.createDialog(Spinner, null, 'mx_Dialog_spinner'); d.finally(() => modal.close()); dis.dispatch({ @@ -1439,7 +1455,6 @@ export default class MatrixChat extends React.PureComponent { }); }); cli.on('no_consent', function(message, consentUri) { - const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); Modal.createTrackedDialog('No Consent Dialog', '', QuestionDialog, { title: _t('Terms and Conditions'), description:
    @@ -1548,8 +1563,6 @@ export default class MatrixChat extends React.PureComponent { }); cli.on("crypto.keySignatureUploadFailure", (failures, source, continuation) => { - const KeySignatureUploadFailedDialog = - sdk.getComponent('views.dialogs.KeySignatureUploadFailedDialog'); Modal.createTrackedDialog( 'Failed to upload key signatures', 'Failed to upload key signatures', @@ -1559,7 +1572,6 @@ export default class MatrixChat extends React.PureComponent { cli.on("crypto.verification.request", request => { if (request.verifier) { - const IncomingSasDialog = sdk.getComponent("views.dialogs.IncomingSasDialog"); Modal.createTrackedDialog('Incoming Verification', '', IncomingSasDialog, { verifier: request.verifier, }, null, /* priority = */ false, /* static = */ true); @@ -1977,21 +1989,18 @@ export default class MatrixChat extends React.PureComponent { let view = null; if (this.state.view === Views.LOADING) { - const Spinner = sdk.getComponent('elements.Spinner'); view = (
    ); } else if (this.state.view === Views.COMPLETE_SECURITY) { - const CompleteSecurity = sdk.getComponent('structures.auth.CompleteSecurity'); view = ( ); } else if (this.state.view === Views.E2E_SETUP) { - const E2eSetup = sdk.getComponent('structures.auth.E2eSetup'); view = ( { * we should go through and figure out what we actually need to pass down, as well * as using something like redux to avoid having a billion bits of state kicking around. */ - const LoggedInView = sdk.getComponent('structures.LoggedInView'); view = ( { ref={this.loggedInView} matrixClient={MatrixClientPeg.get()} onRoomCreated={this.onRoomCreated} - onCloseAllSettings={this.onCloseAllSettings} onRegistered={this.onRegistered} currentRoomId={this.state.currentRoomId} /> ); } else { // we think we are logged in, but are still waiting for the /sync to complete - const Spinner = sdk.getComponent('elements.Spinner'); let errorBox; if (this.state.syncError && !isStoreError) { errorBox =
    @@ -2045,10 +2051,8 @@ export default class MatrixChat extends React.PureComponent { ); } } else if (this.state.view === Views.WELCOME) { - const Welcome = sdk.getComponent('auth.Welcome'); view = ; } else if (this.state.view === Views.REGISTER && SettingsStore.getValue(UIFeature.Registration)) { - const Registration = sdk.getComponent('structures.auth.Registration'); const email = ThreepidInviteStore.instance.pickBestInvite()?.toEmail; view = ( { /> ); } else if (this.state.view === Views.FORGOT_PASSWORD && SettingsStore.getValue(UIFeature.PasswordReset)) { - const ForgotPassword = sdk.getComponent('structures.auth.ForgotPassword'); view = ( { ); } else if (this.state.view === Views.LOGIN) { const showPasswordReset = SettingsStore.getValue(UIFeature.PasswordReset); - const Login = sdk.getComponent('structures.auth.Login'); view = ( { /> ); } else if (this.state.view === Views.SOFT_LOGOUT) { - const SoftLogout = sdk.getComponent('structures.auth.SoftLogout'); view = ( { console.error(`Unknown view ${this.state.view}`); } - const ErrorBoundary = sdk.getComponent('elements.ErrorBoundary'); return {view} ; diff --git a/src/components/structures/auth/Registration.tsx b/src/components/structures/auth/Registration.tsx index 57b758091a..652d42bde5 100644 --- a/src/components/structures/auth/Registration.tsx +++ b/src/components/structures/auth/Registration.tsx @@ -24,7 +24,7 @@ import { messageForResourceLimitError } from '../../../utils/ErrorUtils'; import AutoDiscoveryUtils, { ValidatedServerConfig } from "../../../utils/AutoDiscoveryUtils"; import classNames from "classnames"; import * as Lifecycle from '../../../Lifecycle'; -import { MatrixClientPeg } from "../../../MatrixClientPeg"; +import { IMatrixClientCreds, MatrixClientPeg } from "../../../MatrixClientPeg"; import AuthPage from "../../views/auth/AuthPage"; import Login, { ISSOFlow } from "../../../Login"; import dis from "../../../dispatcher/dispatcher"; @@ -47,13 +47,7 @@ interface IProps { // - The user's password, if available and applicable (may be cached in memory // for a short time so the user is not required to re-enter their password // for operations like uploading cross-signing keys). - onLoggedIn(params: { - userId: string; - deviceId: string; - homeserverUrl: string; - identityServerUrl?: string; - accessToken: string; - }, password: string): void; + onLoggedIn(params: IMatrixClientCreds, password: string): void; makeRegistrationUrl(params: { /* eslint-disable camelcase */ client_secret: string; From 68011056cc5a80ef1c80696a93746c09e741a14b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 3 Jul 2021 12:24:07 +0200 Subject: [PATCH 149/221] Eleventh and final batch of burning MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/structures/MatrixChat.tsx | 4 ++-- src/components/structures/auth/Registration.tsx | 17 +++++++---------- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 64d32bbe62..c7a200239c 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -36,7 +36,6 @@ import dis from "../../dispatcher/dispatcher"; import Notifier from '../../Notifier'; import Modal from "../../Modal"; -import * as sdk from '../../index'; import { showRoomInviteDialog, showStartChatInviteDialog } from '../../RoomInvite'; import * as Rooms from '../../Rooms'; import linkifyMatrix from "../../linkify-matrix"; @@ -101,6 +100,7 @@ import E2eSetup from "./auth/E2eSetup"; import Registration from './auth/Registration'; import Login from "./auth/Login"; import ErrorBoundary from '../views/elements/ErrorBoundary'; +import VerificationRequestToast from '../views/toasts/VerificationRequestToast'; import PerformanceMonitor, { PerformanceEntryNames } from "../../performance"; import UIStore, { UI_EVENTS } from "../../stores/UIStore"; @@ -1581,7 +1581,7 @@ export default class MatrixChat extends React.PureComponent { title: _t("Verification requested"), icon: "verification", props: { request }, - component: sdk.getComponent("toasts.VerificationRequestToast"), + component: VerificationRequestToast, priority: 90, }); } diff --git a/src/components/structures/auth/Registration.tsx b/src/components/structures/auth/Registration.tsx index 652d42bde5..8d32981e57 100644 --- a/src/components/structures/auth/Registration.tsx +++ b/src/components/structures/auth/Registration.tsx @@ -18,7 +18,6 @@ import { createClient } from 'matrix-js-sdk/src/matrix'; import React, { ReactNode } from 'react'; import { MatrixClient } from "matrix-js-sdk/src/client"; -import * as sdk from '../../../index'; import { _t, _td } from '../../../languageHandler'; import { messageForResourceLimitError } from '../../../utils/ErrorUtils'; import AutoDiscoveryUtils, { ValidatedServerConfig } from "../../../utils/AutoDiscoveryUtils"; @@ -31,6 +30,12 @@ import dis from "../../../dispatcher/dispatcher"; import SSOButtons from "../../views/elements/SSOButtons"; import ServerPicker from '../../views/elements/ServerPicker'; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import RegistrationForm from '../../views/auth/RegistrationForm'; +import AccessibleButton from '../../views/elements/AccessibleButton'; +import AuthBody from "../../views/auth/AuthBody"; +import AuthHeader from "../../views/auth/AuthHeader"; +import InteractiveAuth from "../InteractiveAuth"; +import Spinner from "../../views/elements/Spinner"; interface IProps { serverConfig: ValidatedServerConfig; @@ -240,7 +245,7 @@ export default class Registration extends React.Component { } } - private onFormSubmit = formVals => { + private onFormSubmit = async (formVals): Promise => { this.setState({ errorText: "", busy: true, @@ -436,10 +441,6 @@ export default class Registration extends React.Component { }; private renderRegisterComponent() { - const InteractiveAuth = sdk.getComponent('structures.InteractiveAuth'); - const Spinner = sdk.getComponent('elements.Spinner'); - const RegistrationForm = sdk.getComponent('auth.RegistrationForm'); - if (this.state.matrixClient && this.state.doingUIAuth) { return { } render() { - const AuthHeader = sdk.getComponent('auth.AuthHeader'); - const AuthBody = sdk.getComponent("auth.AuthBody"); - const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); - let errorText; const err = this.state.errorText; if (err) { From dbd102541e72952f12f020267ede7fd5b92c3e8e Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Sat, 3 Jul 2021 11:37:06 +0100 Subject: [PATCH 150/221] Migrate E2eSetup to TypeScript --- .../structures/auth/{E2eSetup.js => E2eSetup.tsx} | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) rename src/components/structures/auth/{E2eSetup.js => E2eSetup.tsx} (84%) diff --git a/src/components/structures/auth/E2eSetup.js b/src/components/structures/auth/E2eSetup.tsx similarity index 84% rename from src/components/structures/auth/E2eSetup.js rename to src/components/structures/auth/E2eSetup.tsx index 9b627449bc..93cb92664f 100644 --- a/src/components/structures/auth/E2eSetup.js +++ b/src/components/structures/auth/E2eSetup.tsx @@ -15,20 +15,19 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import AuthPage from '../../views/auth/AuthPage'; import CompleteSecurityBody from '../../views/auth/CompleteSecurityBody'; import CreateCrossSigningDialog from '../../views/dialogs/security/CreateCrossSigningDialog'; import { replaceableComponent } from "../../../utils/replaceableComponent"; -@replaceableComponent("structures.auth.E2eSetup") -export default class E2eSetup extends React.Component { - static propTypes = { - onFinished: PropTypes.func.isRequired, - accountPassword: PropTypes.string, - tokenLogin: PropTypes.bool, - }; +interface IProps { + onFinished: () => void; + accountPassword?: string; + tokenLogin?: boolean; +} +@replaceableComponent("structures.auth.E2eSetup") +export default class E2eSetup extends React.Component { render() { return ( From 298a33867635086ba878a13534892da991b74ba7 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Sat, 3 Jul 2021 11:38:51 +0100 Subject: [PATCH 151/221] Migrate CompleteSecurity to TypeScript --- ...mpleteSecurity.js => CompleteSecurity.tsx} | 29 ++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) rename src/components/structures/auth/{CompleteSecurity.js => CompleteSecurity.tsx} (87%) diff --git a/src/components/structures/auth/CompleteSecurity.js b/src/components/structures/auth/CompleteSecurity.tsx similarity index 87% rename from src/components/structures/auth/CompleteSecurity.js rename to src/components/structures/auth/CompleteSecurity.tsx index d691f6034b..2f37e60450 100644 --- a/src/components/structures/auth/CompleteSecurity.js +++ b/src/components/structures/auth/CompleteSecurity.tsx @@ -15,39 +15,42 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import { _t } from '../../../languageHandler'; import * as sdk from '../../../index'; import { SetupEncryptionStore, Phase } from '../../../stores/SetupEncryptionStore'; import SetupEncryptionBody from "./SetupEncryptionBody"; import { replaceableComponent } from "../../../utils/replaceableComponent"; -@replaceableComponent("structures.auth.CompleteSecurity") -export default class CompleteSecurity extends React.Component { - static propTypes = { - onFinished: PropTypes.func.isRequired, - }; +interface IProps { + onFinished: () => void; +} - constructor() { - super(); +interface IState { + phase: Phase; +} + +@replaceableComponent("structures.auth.CompleteSecurity") +export default class CompleteSecurity extends React.Component { + constructor(props: IProps) { + super(props); const store = SetupEncryptionStore.sharedInstance(); - store.on("update", this._onStoreUpdate); + store.on("update", this.onStoreUpdate); store.start(); this.state = { phase: store.phase }; } - _onStoreUpdate = () => { + private onStoreUpdate = (): void => { const store = SetupEncryptionStore.sharedInstance(); this.setState({ phase: store.phase }); }; - componentWillUnmount() { + public componentWillUnmount(): void { const store = SetupEncryptionStore.sharedInstance(); - store.off("update", this._onStoreUpdate); + store.off("update", this.onStoreUpdate); store.stop(); } - render() { + public render() { const AuthPage = sdk.getComponent("auth.AuthPage"); const CompleteSecurityBody = sdk.getComponent("auth.CompleteSecurityBody"); const { phase } = this.state; From f4ec197513899d1117818919780b5377f21a39ce Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Sat, 3 Jul 2021 11:55:10 +0100 Subject: [PATCH 152/221] Migrate ForgotPassword to TypeScript --- .../{ForgotPassword.js => ForgotPassword.tsx} | 113 +++++++++++------- 1 file changed, 68 insertions(+), 45 deletions(-) rename src/components/structures/auth/{ForgotPassword.js => ForgotPassword.tsx} (81%) diff --git a/src/components/structures/auth/ForgotPassword.js b/src/components/structures/auth/ForgotPassword.tsx similarity index 81% rename from src/components/structures/auth/ForgotPassword.js rename to src/components/structures/auth/ForgotPassword.tsx index 9f2ac9deed..432f69fd1b 100644 --- a/src/components/structures/auth/ForgotPassword.js +++ b/src/components/structures/auth/ForgotPassword.tsx @@ -17,7 +17,6 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import { _t, _td } from '../../../languageHandler'; import * as sdk from '../../../index'; import Modal from "../../../Modal"; @@ -31,27 +30,50 @@ import PassphraseField from '../../views/auth/PassphraseField'; import { replaceableComponent } from "../../../utils/replaceableComponent"; import { PASSWORD_MIN_SCORE } from '../../views/auth/RegistrationForm'; -// Phases -// Show the forgot password inputs -const PHASE_FORGOT = 1; -// Email is in the process of being sent -const PHASE_SENDING_EMAIL = 2; -// Email has been sent -const PHASE_EMAIL_SENT = 3; -// User has clicked the link in email and completed reset -const PHASE_DONE = 4; +import { IValidationResult } from "../../views/elements/Validation"; + +enum Phase { + // Show the forgot password inputs + Forgot = 1, + // Email is in the process of being sent + SendingEmail = 2, + // Email has been sent + EmailSent = 3, + // User has clicked the link in email and completed reset + Done = 4, +} + +interface IProps { + serverConfig: ValidatedServerConfig; + onServerConfigChange: () => void; + onLoginClick?: () => void; + onComplete: () => void; +} + +interface IState { + phase: Phase; + email: string; + password: string; + password2: string; + errorText: string; + + // We perform liveliness checks later, but for now suppress the errors. + // We also track the server dead errors independently of the regular errors so + // that we can render it differently, and override any other error the user may + // be seeing. + serverIsAlive: boolean; + serverErrorIsFatal: boolean; + serverDeadError: string; + + passwordFieldValid: boolean; +} @replaceableComponent("structures.auth.ForgotPassword") -export default class ForgotPassword extends React.Component { - static propTypes = { - serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired, - onServerConfigChange: PropTypes.func.isRequired, - onLoginClick: PropTypes.func, - onComplete: PropTypes.func.isRequired, - }; +export default class ForgotPassword extends React.Component { + private reset: PasswordReset; state = { - phase: PHASE_FORGOT, + phase: Phase.Forgot, email: "", password: "", password2: "", @@ -64,30 +86,31 @@ export default class ForgotPassword extends React.Component { serverIsAlive: true, serverErrorIsFatal: false, serverDeadError: "", + passwordFieldValid: false, }; - constructor(props) { + constructor(props: IProps) { super(props); CountlyAnalytics.instance.track("onboarding_forgot_password_begin"); } - componentDidMount() { + public componentDidMount() { this.reset = null; - this._checkServerLiveliness(this.props.serverConfig); + this.checkServerLiveliness(this.props.serverConfig); } // TODO: [REACT-WARNING] Replace with appropriate lifecycle event // eslint-disable-next-line camelcase - UNSAFE_componentWillReceiveProps(newProps) { + public UNSAFE_componentWillReceiveProps(newProps: IProps): void { if (newProps.serverConfig.hsUrl === this.props.serverConfig.hsUrl && newProps.serverConfig.isUrl === this.props.serverConfig.isUrl) return; // Do a liveliness check on the new URLs - this._checkServerLiveliness(newProps.serverConfig); + this.checkServerLiveliness(newProps.serverConfig); } - async _checkServerLiveliness(serverConfig) { + private async checkServerLiveliness(serverConfig): Promise { try { await AutoDiscoveryUtils.validateServerConfigWithStaticUrls( serverConfig.hsUrl, @@ -98,28 +121,28 @@ export default class ForgotPassword extends React.Component { serverIsAlive: true, }); } catch (e) { - this.setState(AutoDiscoveryUtils.authComponentStateForError(e, "forgot_password")); + this.setState(AutoDiscoveryUtils.authComponentStateForError(e, "forgot_password") as IState); } } - submitPasswordReset(email, password) { + public submitPasswordReset(email: string, password: string): void { this.setState({ - phase: PHASE_SENDING_EMAIL, + phase: Phase.SendingEmail, }); this.reset = new PasswordReset(this.props.serverConfig.hsUrl, this.props.serverConfig.isUrl); this.reset.resetPassword(email, password).then(() => { this.setState({ - phase: PHASE_EMAIL_SENT, + phase: Phase.EmailSent, }); }, (err) => { this.showErrorDialog(_t('Failed to send email') + ": " + err.message); this.setState({ - phase: PHASE_FORGOT, + phase: Phase.Forgot, }); }); } - onVerify = async ev => { + private onVerify = async (ev: React.MouseEvent): Promise => { ev.preventDefault(); if (!this.reset) { console.error("onVerify called before submitPasswordReset!"); @@ -127,17 +150,17 @@ export default class ForgotPassword extends React.Component { } try { await this.reset.checkEmailLinkClicked(); - this.setState({ phase: PHASE_DONE }); + this.setState({ phase: Phase.Done }); } catch (err) { this.showErrorDialog(err.message); } }; - onSubmitForm = async ev => { + private onSubmitForm = async (ev: React.FormEvent): Promise => { ev.preventDefault(); // refresh the server errors, just in case the server came back online - await this._checkServerLiveliness(this.props.serverConfig); + await this.checkServerLiveliness(this.props.serverConfig); await this['password_field'].validate({ allowEmpty: false }); @@ -172,27 +195,27 @@ export default class ForgotPassword extends React.Component { } }; - onInputChanged = (stateKey, ev) => { + private onInputChanged = (stateKey: string, ev: React.FormEvent) => { this.setState({ - [stateKey]: ev.target.value, - }); + [stateKey]: ev.currentTarget.value, + } as any); }; - onLoginClick = ev => { + private onLoginClick = (ev: React.MouseEvent): void => { ev.preventDefault(); ev.stopPropagation(); this.props.onLoginClick(); }; - showErrorDialog(body, title) { + public showErrorDialog(description: string, title?: string) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createTrackedDialog('Forgot Password Error', '', ErrorDialog, { - title: title, - description: body, + title, + description, }); } - onPasswordValidate(result) { + private onPasswordValidate(result: IValidationResult) { this.setState({ passwordFieldValid: result.valid, }); @@ -316,16 +339,16 @@ export default class ForgotPassword extends React.Component { let resetPasswordJsx; switch (this.state.phase) { - case PHASE_FORGOT: + case Phase.Forgot: resetPasswordJsx = this.renderForgot(); break; - case PHASE_SENDING_EMAIL: + case Phase.SendingEmail: resetPasswordJsx = this.renderSendingEmail(); break; - case PHASE_EMAIL_SENT: + case Phase.EmailSent: resetPasswordJsx = this.renderEmailSent(); break; - case PHASE_DONE: + case Phase.Done: resetPasswordJsx = this.renderDone(); break; } From ef2848664fdf548c1eb167d3c08f54ebdd64f052 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Sat, 3 Jul 2021 12:03:00 +0100 Subject: [PATCH 153/221] Migrate InlineTermsAgreement to TypeScript --- ...sAgreement.js => InlineTermsAgreement.tsx} | 51 +++++++++++-------- 1 file changed, 30 insertions(+), 21 deletions(-) rename src/components/views/terms/{InlineTermsAgreement.js => InlineTermsAgreement.tsx} (81%) diff --git a/src/components/views/terms/InlineTermsAgreement.js b/src/components/views/terms/InlineTermsAgreement.tsx similarity index 81% rename from src/components/views/terms/InlineTermsAgreement.js rename to src/components/views/terms/InlineTermsAgreement.tsx index e3b4df4712..62594746ed 100644 --- a/src/components/views/terms/InlineTermsAgreement.js +++ b/src/components/views/terms/InlineTermsAgreement.tsx @@ -15,39 +15,48 @@ limitations under the License. */ import React from "react"; -import PropTypes from "prop-types"; import { _t, pickBestLanguage } from "../../../languageHandler"; import * as sdk from "../../.."; import { objectClone } from "../../../utils/objects"; import StyledCheckbox from "../elements/StyledCheckbox"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +interface IProps { + policiesAndServicePairs: any[]; + onFinished: (string) => void; + agreedUrls: string[], // array of URLs the user has accepted + introElement: Node, +} + +interface IState { + policies: Policy[], + busy: boolean; +} + +interface Policy { + checked: boolean; + url: string; + name: string; +} + @replaceableComponent("views.terms.InlineTermsAgreement") -export default class InlineTermsAgreement extends React.Component { - static propTypes = { - policiesAndServicePairs: PropTypes.array.isRequired, // array of service/policy pairs - agreedUrls: PropTypes.array.isRequired, // array of URLs the user has accepted - onFinished: PropTypes.func.isRequired, // takes an argument of accepted URLs - introElement: PropTypes.node, - }; - - constructor() { - super(); - +export default class InlineTermsAgreement extends React.Component { + constructor(props: IProps) { + super(props); this.state = { policies: [], busy: false, }; } - componentDidMount() { + public componentDidMount(): void { // Build all the terms the user needs to accept const policies = []; // { checked, url, name } for (const servicePolicies of this.props.policiesAndServicePairs) { const availablePolicies = Object.values(servicePolicies.policies); for (const policy of availablePolicies) { const language = pickBestLanguage(Object.keys(policy).filter(p => p !== 'version')); - const renderablePolicy = { + const renderablePolicy: Policy = { checked: false, url: policy[language].url, name: policy[language].name, @@ -59,13 +68,13 @@ export default class InlineTermsAgreement extends React.Component { this.setState({ policies }); } - _togglePolicy = (index) => { + private togglePolicy = (index: number): void => { const policies = objectClone(this.state.policies); policies[index].checked = !policies[index].checked; this.setState({ policies }); }; - _onContinue = () => { + private onContinue = (): void => { const hasUnchecked = !!this.state.policies.some(p => !p.checked); if (hasUnchecked) return; @@ -73,7 +82,7 @@ export default class InlineTermsAgreement extends React.Component { this.props.onFinished(this.state.policies.map(p => p.url)); }; - _renderCheckboxes() { + private renderCheckboxes(): React.ReactNode[] { const rendered = []; for (let i = 0; i < this.state.policies.length; i++) { const policy = this.state.policies[i]; @@ -93,7 +102,7 @@ export default class InlineTermsAgreement extends React.Component {
    {introText}
    - this._togglePolicy(i)} checked={policy.checked}> + this.togglePolicy(i)} checked={policy.checked}> {_t("Accept")}
    @@ -103,16 +112,16 @@ export default class InlineTermsAgreement extends React.Component { return rendered; } - render() { + public render(): React.ReactNode { const AccessibleButton = sdk.getComponent("views.elements.AccessibleButton"); const hasUnchecked = !!this.state.policies.some(p => !p.checked); return (
    {this.props.introElement} - {this._renderCheckboxes()} + {this.renderCheckboxes()} From ef4d58c0d9e7f81406a930e1059828eac4854e8d Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Sat, 3 Jul 2021 12:08:56 +0100 Subject: [PATCH 154/221] Migrate SetIntegrationManager to TypeScript --- ...onManager.js => SetIntegrationManager.tsx} | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) rename src/components/views/settings/{SetIntegrationManager.js => SetIntegrationManager.tsx} (89%) diff --git a/src/components/views/settings/SetIntegrationManager.js b/src/components/views/settings/SetIntegrationManager.tsx similarity index 89% rename from src/components/views/settings/SetIntegrationManager.js rename to src/components/views/settings/SetIntegrationManager.tsx index 6636c87df8..ada78e2848 100644 --- a/src/components/views/settings/SetIntegrationManager.js +++ b/src/components/views/settings/SetIntegrationManager.tsx @@ -17,15 +17,25 @@ limitations under the License. import React from 'react'; import { _t } from "../../../languageHandler"; import { IntegrationManagers } from "../../../integrations/IntegrationManagers"; +import { IntegrationManagerInstance } from "../../../integrations/IntegrationManagerInstance"; import * as sdk from '../../../index'; import SettingsStore from "../../../settings/SettingsStore"; import { SettingLevel } from "../../../settings/SettingLevel"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +interface IProps { + +} + +interface IState { + currentManager: IntegrationManagerInstance; + provisioningEnabled: boolean; +} + @replaceableComponent("views.settings.SetIntegrationManager") -export default class SetIntegrationManager extends React.Component { - constructor() { - super(); +export default class SetIntegrationManager extends React.Component { + constructor(props: IProps) { + super(props); const currentManager = IntegrationManagers.sharedInstance().getPrimaryManager(); @@ -35,7 +45,7 @@ export default class SetIntegrationManager extends React.Component { }; } - onProvisioningToggled = () => { + private onProvisioningToggled = (): void => { const current = this.state.provisioningEnabled; SettingsStore.setValue("integrationProvisioning", null, SettingLevel.ACCOUNT, !current).catch(err => { console.error("Error changing integration manager provisioning"); @@ -46,7 +56,7 @@ export default class SetIntegrationManager extends React.Component { this.setState({ provisioningEnabled: !current }); }; - render() { + public render(): React.ReactNode { const ToggleSwitch = sdk.getComponent("views.elements.ToggleSwitch"); const currentManager = this.state.currentManager; From e7743e2a57bdff3de4752f4fa0c8cc5ce94588cb Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Sat, 3 Jul 2021 12:17:38 +0100 Subject: [PATCH 155/221] Migrate RoomScrollStateStore to TypeScript --- src/@types/global.d.ts | 2 + src/components/structures/RoomView.tsx | 4 +- src/stores/RoomScrollStateStore.js | 50 ------------------------ src/stores/RoomScrollStateStore.ts | 53 ++++++++++++++++++++++++++ 4 files changed, 57 insertions(+), 52 deletions(-) delete mode 100644 src/stores/RoomScrollStateStore.js create mode 100644 src/stores/RoomScrollStateStore.ts diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index f75c17aaf4..d257ee4c5c 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -46,6 +46,7 @@ import { VoiceRecordingStore } from "../stores/VoiceRecordingStore"; import PerformanceMonitor from "../performance"; import UIStore from "../stores/UIStore"; import { SetupEncryptionStore } from "../stores/SetupEncryptionStore"; +import { RoomScrollStateStore } from "../stores/RoomScrollStateStore"; declare global { interface Window { @@ -87,6 +88,7 @@ declare global { mxPerformanceEntryNames: any; mxUIStore: UIStore; mxSetupEncryptionStore?: SetupEncryptionStore; + mxRoomScrollStateStore?: RoomScrollStateStore; } interface Document { diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 2c8fc08dac..0985719302 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -42,7 +42,7 @@ import eventSearch, { searchPagination } from '../../Searching'; import MainSplit from './MainSplit'; import RightPanel from './RightPanel'; import RoomViewStore from '../../stores/RoomViewStore'; -import RoomScrollStateStore from '../../stores/RoomScrollStateStore'; +import RoomScrollStateStore, { ScrollState } from '../../stores/RoomScrollStateStore'; import WidgetEchoStore from '../../stores/WidgetEchoStore'; import SettingsStore from "../../settings/SettingsStore"; import { Layout } from "../../settings/Layout"; @@ -1577,7 +1577,7 @@ export default class RoomView extends React.Component { // get the current scroll position of the room, so that it can be // restored when we switch back to it. // - private getScrollState() { + private getScrollState(): ScrollState { const messagePanel = this.messagePanel; if (!messagePanel) return null; diff --git a/src/stores/RoomScrollStateStore.js b/src/stores/RoomScrollStateStore.js deleted file mode 100644 index 07848283d1..0000000000 --- a/src/stores/RoomScrollStateStore.js +++ /dev/null @@ -1,50 +0,0 @@ -/* -Copyright 2017 New Vector Ltd - -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. -*/ - -/** - * Stores where the user has scrolled to in each room - */ -class RoomScrollStateStore { - constructor() { - // A map from room id to scroll state. - // - // If there is no special scroll state (ie, we are following the live - // timeline), the scroll state is null. Otherwise, it is an object with - // the following properties: - // - // focussedEvent: the ID of the 'focussed' event. Typically this is - // the last event fully visible in the viewport, though if we - // have done an explicit scroll to an explicit event, it will be - // that event. - // - // pixelOffset: the number of pixels the window is scrolled down - // from the focussedEvent. - this._scrollStateMap = {}; - } - - getScrollState(roomId) { - return this._scrollStateMap[roomId]; - } - - setScrollState(roomId, scrollState) { - this._scrollStateMap[roomId] = scrollState; - } -} - -if (global.mx_RoomScrollStateStore === undefined) { - global.mx_RoomScrollStateStore = new RoomScrollStateStore(); -} -export default global.mx_RoomScrollStateStore; diff --git a/src/stores/RoomScrollStateStore.ts b/src/stores/RoomScrollStateStore.ts new file mode 100644 index 0000000000..1d8d4eb8fd --- /dev/null +++ b/src/stores/RoomScrollStateStore.ts @@ -0,0 +1,53 @@ +/* +Copyright 2017 New Vector Ltd + +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 interface ScrollState { + focussedEvent: string; + pixelOffset: number; +} + +/** + * Stores where the user has scrolled to in each room + */ +export class RoomScrollStateStore { + // A map from room id to scroll state. + // + // If there is no special scroll state (ie, we are following the live + // timeline), the scroll state is null. Otherwise, it is an object with + // the following properties: + // + // focussedEvent: the ID of the 'focussed' event. Typically this is + // the last event fully visible in the viewport, though if we + // have done an explicit scroll to an explicit event, it will be + // that event. + // + // pixelOffset: the number of pixels the window is scrolled down + // from the focussedEvent. + private scrollStateMap = new Map(); + + public getScrollState(roomId: string): ScrollState { + return this.scrollStateMap.get(roomId); + } + + setScrollState(roomId: string, scrollState: ScrollState): void { + this.scrollStateMap.set(roomId, scrollState); + } +} + +if (window.mxRoomScrollStateStore === undefined) { + window.mxRoomScrollStateStore = new RoomScrollStateStore(); +} +export default window.mxRoomScrollStateStore; From d5cebf5e8d8f22230c109aa0809c58e44e8f337f Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Sat, 3 Jul 2021 12:22:58 +0100 Subject: [PATCH 156/221] Migrate index to TypeScript --- src/notifications/{index.js => index.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/notifications/{index.js => index.ts} (100%) diff --git a/src/notifications/index.js b/src/notifications/index.ts similarity index 100% rename from src/notifications/index.js rename to src/notifications/index.ts From 66dde68377b5d124c902da2c6da7e7b7166dadd5 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Sat, 3 Jul 2021 12:29:13 +0100 Subject: [PATCH 157/221] Migrate VectorPushRulesDefinitions to TypeScript --- ...tions.js => VectorPushRulesDefinitions.ts} | 47 ++++++++++++++----- 1 file changed, 34 insertions(+), 13 deletions(-) rename src/notifications/{VectorPushRulesDefinitions.js => VectorPushRulesDefinitions.ts} (91%) diff --git a/src/notifications/VectorPushRulesDefinitions.js b/src/notifications/VectorPushRulesDefinitions.ts similarity index 91% rename from src/notifications/VectorPushRulesDefinitions.js rename to src/notifications/VectorPushRulesDefinitions.ts index df41f23ec5..38dd88e6c6 100644 --- a/src/notifications/VectorPushRulesDefinitions.js +++ b/src/notifications/VectorPushRulesDefinitions.ts @@ -20,15 +20,25 @@ import { StandardActions } from "./StandardActions"; import { PushRuleVectorState } from "./PushRuleVectorState"; import { NotificationUtils } from "./NotificationUtils"; +interface IProps { + kind: Kind; + description: string; + vectorStateToActions: Action; +} + class VectorPushRuleDefinition { - constructor(opts) { + private kind: Kind; + private description: string; + private vectorStateToActions: Action; + + constructor(opts: IProps) { this.kind = opts.kind; this.description = opts.description; this.vectorStateToActions = opts.vectorStateToActions; } // Translate the rule actions and its enabled value into vector state - ruleToVectorState(rule) { + public ruleToVectorState(rule): VectorPushRuleDefinition { let enabled = false; if (rule) { enabled = rule.enabled; @@ -63,13 +73,24 @@ class VectorPushRuleDefinition { } } +enum Kind { + Override = "override", + Underride = "underride", +} + +interface Action { + on: StandardActions; + loud: StandardActions; + off: StandardActions; +} + /** * The descriptions of rules managed by the Vector UI. */ export const VectorPushRulesDefinitions = { // Messages containing user's display name ".m.rule.contains_display_name": new VectorPushRuleDefinition({ - kind: "override", + kind: Kind.Override, description: _td("Messages containing my display name"), // passed through _t() translation in src/components/views/settings/Notifications.js vectorStateToActions: { // The actions for each vector state, or null to disable the rule. on: StandardActions.ACTION_NOTIFY, @@ -80,7 +101,7 @@ export const VectorPushRulesDefinitions = { // Messages containing user's username (localpart/MXID) ".m.rule.contains_user_name": new VectorPushRuleDefinition({ - kind: "override", + kind: Kind.Override, description: _td("Messages containing my username"), // passed through _t() translation in src/components/views/settings/Notifications.js vectorStateToActions: { // The actions for each vector state, or null to disable the rule. on: StandardActions.ACTION_NOTIFY, @@ -91,7 +112,7 @@ export const VectorPushRulesDefinitions = { // Messages containing @room ".m.rule.roomnotif": new VectorPushRuleDefinition({ - kind: "override", + kind: Kind.Override, description: _td("Messages containing @room"), // passed through _t() translation in src/components/views/settings/Notifications.js vectorStateToActions: { // The actions for each vector state, or null to disable the rule. on: StandardActions.ACTION_NOTIFY, @@ -102,7 +123,7 @@ export const VectorPushRulesDefinitions = { // Messages just sent to the user in a 1:1 room ".m.rule.room_one_to_one": new VectorPushRuleDefinition({ - kind: "underride", + kind: Kind.Underride, description: _td("Messages in one-to-one chats"), // passed through _t() translation in src/components/views/settings/Notifications.js vectorStateToActions: { on: StandardActions.ACTION_NOTIFY, @@ -113,7 +134,7 @@ export const VectorPushRulesDefinitions = { // Encrypted messages just sent to the user in a 1:1 room ".m.rule.encrypted_room_one_to_one": new VectorPushRuleDefinition({ - kind: "underride", + kind: Kind.Underride, description: _td("Encrypted messages in one-to-one chats"), // passed through _t() translation in src/components/views/settings/Notifications.js vectorStateToActions: { on: StandardActions.ACTION_NOTIFY, @@ -126,7 +147,7 @@ export const VectorPushRulesDefinitions = { // 1:1 room messages are catched by the .m.rule.room_one_to_one rule if any defined // By opposition, all other room messages are from group chat rooms. ".m.rule.message": new VectorPushRuleDefinition({ - kind: "underride", + kind: Kind.Underride, description: _td("Messages in group chats"), // passed through _t() translation in src/components/views/settings/Notifications.js vectorStateToActions: { on: StandardActions.ACTION_NOTIFY, @@ -139,7 +160,7 @@ export const VectorPushRulesDefinitions = { // Encrypted 1:1 room messages are catched by the .m.rule.encrypted_room_one_to_one rule if any defined // By opposition, all other room messages are from group chat rooms. ".m.rule.encrypted": new VectorPushRuleDefinition({ - kind: "underride", + kind: Kind.Underride, description: _td("Encrypted messages in group chats"), // passed through _t() translation in src/components/views/settings/Notifications.js vectorStateToActions: { on: StandardActions.ACTION_NOTIFY, @@ -150,7 +171,7 @@ export const VectorPushRulesDefinitions = { // Invitation for the user ".m.rule.invite_for_me": new VectorPushRuleDefinition({ - kind: "underride", + kind: Kind.Underride, description: _td("When I'm invited to a room"), // passed through _t() translation in src/components/views/settings/Notifications.js vectorStateToActions: { on: StandardActions.ACTION_NOTIFY, @@ -161,7 +182,7 @@ export const VectorPushRulesDefinitions = { // Incoming call ".m.rule.call": new VectorPushRuleDefinition({ - kind: "underride", + kind: Kind.Underride, description: _td("Call invitation"), // passed through _t() translation in src/components/views/settings/Notifications.js vectorStateToActions: { on: StandardActions.ACTION_NOTIFY, @@ -172,7 +193,7 @@ export const VectorPushRulesDefinitions = { // Notifications from bots ".m.rule.suppress_notices": new VectorPushRuleDefinition({ - kind: "override", + kind: Kind.Override, description: _td("Messages sent by bot"), // passed through _t() translation in src/components/views/settings/Notifications.js vectorStateToActions: { // .m.rule.suppress_notices is a "negative" rule, we have to invert its enabled value for vector UI @@ -184,7 +205,7 @@ export const VectorPushRulesDefinitions = { // Room upgrades (tombstones) ".m.rule.tombstone": new VectorPushRuleDefinition({ - kind: "override", + kind: Kind.Override, description: _td("When rooms are upgraded"), // passed through _t() translation in src/components/views/settings/Notifications.js vectorStateToActions: { // The actions for each vector state, or null to disable the rule. on: StandardActions.ACTION_NOTIFY, From b18691a1cba4a251ac7f6990ad7b76b8b507318c Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Sat, 3 Jul 2021 12:31:10 +0100 Subject: [PATCH 158/221] Migrate VerificationCancelled to TypeScript --- ...cationCancelled.js => VerificationCancelled.tsx} | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) rename src/components/views/verification/{VerificationCancelled.js => VerificationCancelled.tsx} (90%) diff --git a/src/components/views/verification/VerificationCancelled.js b/src/components/views/verification/VerificationCancelled.tsx similarity index 90% rename from src/components/views/verification/VerificationCancelled.js rename to src/components/views/verification/VerificationCancelled.tsx index 0d868edaaa..aa34b22382 100644 --- a/src/components/views/verification/VerificationCancelled.js +++ b/src/components/views/verification/VerificationCancelled.tsx @@ -15,18 +15,17 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; import { replaceableComponent } from "../../../utils/replaceableComponent"; -@replaceableComponent("views.verification.VerificationCancelled") -export default class VerificationCancelled extends React.Component { - static propTypes = { - onDone: PropTypes.func.isRequired, - } +interface IProps { + onDone: () => void; +} - render() { +@replaceableComponent("views.verification.VerificationCancelled") +export default class VerificationCancelled extends React.Component { + public render(): React.ReactNode { const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); return

    {_t( From a1135783bc16c056139e910d70ed7eae4cbd8c35 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Sat, 3 Jul 2021 12:32:08 +0100 Subject: [PATCH 159/221] Migrate VerificationComplete to TypeScript --- ...ficationComplete.js => VerificationComplete.tsx} | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) rename src/components/views/verification/{VerificationComplete.js => VerificationComplete.tsx} (91%) diff --git a/src/components/views/verification/VerificationComplete.js b/src/components/views/verification/VerificationComplete.tsx similarity index 91% rename from src/components/views/verification/VerificationComplete.js rename to src/components/views/verification/VerificationComplete.tsx index c382a6dde4..7da601fc93 100644 --- a/src/components/views/verification/VerificationComplete.js +++ b/src/components/views/verification/VerificationComplete.tsx @@ -15,18 +15,17 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; import { replaceableComponent } from "../../../utils/replaceableComponent"; -@replaceableComponent("views.verification.VerificationComplete") -export default class VerificationComplete extends React.Component { - static propTypes = { - onDone: PropTypes.func.isRequired, - } +interface IProps { + onDone: () => void; +} - render() { +@replaceableComponent("views.verification.VerificationComplete") +export default class VerificationComplete extends React.Component { + public render(): React.ReactNode { const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); return

    {_t("Verified!")}

    From 103caffb5b193c45b10e111674bc7651d57b0edc Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Sat, 3 Jul 2021 12:35:14 +0100 Subject: [PATCH 160/221] Delete unused VerificationQREmojiOPtions --- .../VerificationQREmojiOptions.js | 68 ------------------- 1 file changed, 68 deletions(-) delete mode 100644 src/components/views/verification/VerificationQREmojiOptions.js diff --git a/src/components/views/verification/VerificationQREmojiOptions.js b/src/components/views/verification/VerificationQREmojiOptions.js deleted file mode 100644 index 785d383035..0000000000 --- a/src/components/views/verification/VerificationQREmojiOptions.js +++ /dev/null @@ -1,68 +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. -*/ - -import React from 'react'; -import PropTypes from 'prop-types'; -import { _t } from '../../../languageHandler'; -import AccessibleButton from "../elements/AccessibleButton"; -import { replaceableComponent } from "../../../utils/replaceableComponent"; -import VerificationQRCode from "../elements/crypto/VerificationQRCode"; -import Spinner from "../elements/Spinner"; -import { SCAN_QR_CODE_METHOD } from "matrix-js-sdk/src/crypto/verification/QRCode"; - -@replaceableComponent("views.verification.VerificationQREmojiOptions") -export default class VerificationQREmojiOptions extends React.Component { - static propTypes = { - request: PropTypes.object.isRequired, - onCancel: PropTypes.func.isRequired, - onStartEmoji: PropTypes.func.isRequired, - }; - - render() { - const { request } = this.props; - const showQR = request.otherPartySupportsMethod(SCAN_QR_CODE_METHOD); - - let qrCode; - if (showQR) { - qrCode = ; - } else { - qrCode =
    ; - } - - return ( -
    - {_t("Verify this session by completing one of the following:")} -
    -
    -

    {_t("Scan this unique code")}

    - {qrCode} -
    -
    {_t("or")}
    -
    -

    {_t("Compare unique emoji")}

    - {_t("Compare a unique set of emoji if you don't have a camera on either device")} - - {_t("Start")} - -
    -
    - - {_t("Cancel")} - -
    - ); - } -} From b5ff7eb7d2b49977a738c6c49c2b0990f9323e3e Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Sat, 3 Jul 2021 12:43:18 +0100 Subject: [PATCH 161/221] Migrate VerificationShowSas to TypeScript --- ...tionShowSas.js => VerificationShowSas.tsx} | 40 +++++++++++-------- 1 file changed, 23 insertions(+), 17 deletions(-) rename src/components/views/verification/{VerificationShowSas.js => VerificationShowSas.tsx} (91%) diff --git a/src/components/views/verification/VerificationShowSas.js b/src/components/views/verification/VerificationShowSas.tsx similarity index 91% rename from src/components/views/verification/VerificationShowSas.js rename to src/components/views/verification/VerificationShowSas.tsx index 3f154b9f01..72dd5f0669 100644 --- a/src/components/views/verification/VerificationShowSas.js +++ b/src/components/views/verification/VerificationShowSas.tsx @@ -15,7 +15,8 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; +import { SAS } from "matrix-js-sdk/src/crypto/verification/SAS"; +import { DeviceInfo } from "matrix-js-sdk/src//crypto/deviceinfo"; import { _t, _td } from '../../../languageHandler'; import { PendingActionSpinner } from "../right_panel/EncryptionInfo"; import AccessibleButton from "../elements/AccessibleButton"; @@ -23,24 +24,29 @@ import DialogButtons from "../elements/DialogButtons"; import { fixupColorFonts } from '../../../utils/FontManager'; import { replaceableComponent } from "../../../utils/replaceableComponent"; +interface IProps { + pending?: boolean; + displayName?: string // required if pending is true + device?: DeviceInfo; + onDone: () => void; + onCancel: () => void; + sas: SAS.sas; + isSelf?: boolean; + inDialog?: boolean; // whether this component is being shown in a dialog and to use DialogButtons +} + +interface IState { + pending: boolean; + cancelling?: boolean; +} + function capFirst(s) { return s.charAt(0).toUpperCase() + s.slice(1); } @replaceableComponent("views.verification.VerificationShowSas") -export default class VerificationShowSas extends React.Component { - static propTypes = { - pending: PropTypes.bool, - displayName: PropTypes.string, // required if pending is true - device: PropTypes.object, - onDone: PropTypes.func.isRequired, - onCancel: PropTypes.func.isRequired, - sas: PropTypes.object.isRequired, - isSelf: PropTypes.bool, - inDialog: PropTypes.bool, // whether this component is being shown in a dialog and to use DialogButtons - }; - - constructor(props) { +export default class VerificationShowSas extends React.Component { + constructor(props: IProps) { super(props); this.state = { @@ -48,19 +54,19 @@ export default class VerificationShowSas extends React.Component { }; } - componentWillMount() { + public componentWillMount(): void { // As this component is also used before login (during complete security), // also make sure we have a working emoji font to display the SAS emojis here. // This is also done from LoggedInView. fixupColorFonts(); } - onMatchClick = () => { + private onMatchClick = (): void => { this.setState({ pending: true }); this.props.onDone(); }; - onDontMatchClick = () => { + private onDontMatchClick = (): void => { this.setState({ cancelling: true }); this.props.onCancel(); }; From 055f3a72ad76adcca11196fceb4d7c9b97126668 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Sat, 3 Jul 2021 15:18:47 -0400 Subject: [PATCH 162/221] Fix being able to un-rotate images Since 0 is falsy, this made it impossible to return images to 0 degrees of rotation. Signed-off-by: Robin Townsend --- src/components/views/elements/ImageView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index 2628170f9c..0de34251d8 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -122,7 +122,7 @@ export default class ImageView extends React.Component { const image = this.image.current; const imageWrapper = this.imageWrapper.current; - const rotation = inputRotation || this.state.rotation; + const rotation = inputRotation ?? this.state.rotation; const imageIsNotFlipped = rotation % 180 === 0; From 8d3476776856b5d26a1d8e063ee57eabaa23bffd Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 5 Jul 2021 09:18:16 +0200 Subject: [PATCH 163/221] Fix i18n after deprecation --- src/i18n/strings/en_EN.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 618d5763fa..7b9cfe30e2 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -923,12 +923,6 @@ "You've successfully verified this user.": "You've successfully verified this user.", "Secure messages with this user are end-to-end encrypted and not able to be read by third parties.": "Secure messages with this user are end-to-end encrypted and not able to be read by third parties.", "Got It": "Got It", - "Verify this session by completing one of the following:": "Verify this session by completing one of the following:", - "Scan this unique code": "Scan this unique code", - "or": "or", - "Compare unique emoji": "Compare unique emoji", - "Compare a unique set of emoji if you don't have a camera on either device": "Compare a unique set of emoji if you don't have a camera on either device", - "Start": "Start", "Confirm the emoji below are displayed on both sessions, in the same order:": "Confirm the emoji below are displayed on both sessions, in the same order:", "Verify this user by confirming the following emoji appear on their screen.": "Verify this user by confirming the following emoji appear on their screen.", "Verify this session by confirming the following number appears on its screen.": "Verify this session by confirming the following number appears on its screen.", @@ -1828,6 +1822,12 @@ "Edit devices": "Edit devices", "Security": "Security", "The session you are trying to verify doesn't support scanning a QR code or emoji verification, which is what %(brand)s supports. Try with a different client.": "The session you are trying to verify doesn't support scanning a QR code or emoji verification, which is what %(brand)s supports. Try with a different client.", + "Scan this unique code": "Scan this unique code", + "Compare unique emoji": "Compare unique emoji", + "Compare a unique set of emoji if you don't have a camera on either device": "Compare a unique set of emoji if you don't have a camera on either device", + "Start": "Start", + "or": "or", + "Verify this session by completing one of the following:": "Verify this session by completing one of the following:", "Verify by scanning": "Verify by scanning", "Ask %(displayName)s to scan your code:": "Ask %(displayName)s to scan your code:", "If you can't scan the code above, verify by comparing unique emoji.": "If you can't scan the code above, verify by comparing unique emoji.", From 925d434d536d60e58ac5d7a11ee7beaac2582844 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 5 Jul 2021 09:19:19 +0200 Subject: [PATCH 164/221] Linting fix in TypeScript interface --- src/components/views/terms/InlineTermsAgreement.tsx | 6 +++--- src/components/views/verification/VerificationShowSas.tsx | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/views/terms/InlineTermsAgreement.tsx b/src/components/views/terms/InlineTermsAgreement.tsx index 62594746ed..306dfea6b9 100644 --- a/src/components/views/terms/InlineTermsAgreement.tsx +++ b/src/components/views/terms/InlineTermsAgreement.tsx @@ -24,12 +24,12 @@ import { replaceableComponent } from "../../../utils/replaceableComponent"; interface IProps { policiesAndServicePairs: any[]; onFinished: (string) => void; - agreedUrls: string[], // array of URLs the user has accepted - introElement: Node, + agreedUrls: string[]; // array of URLs the user has accepted + introElement: Node; } interface IState { - policies: Policy[], + policies: Policy[]; busy: boolean; } diff --git a/src/components/views/verification/VerificationShowSas.tsx b/src/components/views/verification/VerificationShowSas.tsx index 72dd5f0669..aaf0ca4848 100644 --- a/src/components/views/verification/VerificationShowSas.tsx +++ b/src/components/views/verification/VerificationShowSas.tsx @@ -26,7 +26,7 @@ import { replaceableComponent } from "../../../utils/replaceableComponent"; interface IProps { pending?: boolean; - displayName?: string // required if pending is true + displayName?: string; // required if pending is true device?: DeviceInfo; onDone: () => void; onCancel: () => void; From 9999b01c75c9bc6dd9d3a9b3c7680d6096254307 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Wed, 30 Jun 2021 08:09:55 +0100 Subject: [PATCH 165/221] Remove reminescent references to the tinter --- res/css/structures/_GroupView.scss | 2 +- res/css/views/messages/_MImageBody.scss | 2 +- src/components/structures/RoomView.tsx | 13 ------------- src/components/views/rooms/SimpleRoomHeader.js | 2 +- 4 files changed, 3 insertions(+), 16 deletions(-) diff --git a/res/css/structures/_GroupView.scss b/res/css/structures/_GroupView.scss index 2350d9f28a..60f9ebdd08 100644 --- a/res/css/structures/_GroupView.scss +++ b/res/css/structures/_GroupView.scss @@ -323,7 +323,7 @@ limitations under the License. } .mx_GroupView_featuredThing .mx_BaseAvatar { - /* To prevent misalignment with mx_TintableSvg (in addButton) */ + /* To prevent misalignment with img (in addButton) */ vertical-align: initial; } diff --git a/res/css/views/messages/_MImageBody.scss b/res/css/views/messages/_MImageBody.scss index 1c773c2f06..515d867da5 100644 --- a/res/css/views/messages/_MImageBody.scss +++ b/res/css/views/messages/_MImageBody.scss @@ -43,7 +43,7 @@ limitations under the License. top: 50%; } -// Inner img and TintableSvg should be centered around 0, 0 +// Inner img should be centered around 0, 0 .mx_MImageBody_thumbnail_spinner > * { transform: translate(-50%, -50%); } diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index aef2fbc9d4..410e1aab61 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -1059,15 +1059,6 @@ export default class RoomView extends React.Component { }); } - private updateTint() { - const room = this.state.room; - if (!room) return; - - console.log("Tinter.tint from updateTint"); - const colorScheme = SettingsStore.getValue("roomColor", room.roomId); - Tinter.tint(colorScheme.primary_color, colorScheme.secondary_color); - } - private onAccountData = (event: MatrixEvent) => { const type = event.getType(); if ((type === "org.matrix.preview_urls" || type === "im.vector.web.settings") && this.state.room) { @@ -1718,10 +1709,6 @@ export default class RoomView extends React.Component { // otherwise react calls it with null on each update. private gatherTimelinePanelRef = r => { this.messagePanel = r; - if (r) { - console.log("updateTint from RoomView.gatherTimelinePanelRef"); - this.updateTint(); - } }; private getOldRoom() { diff --git a/src/components/views/rooms/SimpleRoomHeader.js b/src/components/views/rooms/SimpleRoomHeader.js index 35e7d44469..e3d09f0678 100644 --- a/src/components/views/rooms/SimpleRoomHeader.js +++ b/src/components/views/rooms/SimpleRoomHeader.js @@ -28,7 +28,7 @@ export default class SimpleRoomHeader extends React.Component { static propTypes = { title: PropTypes.string, - // `src` to a TintableSvg. Optional. + // `src` to an image. Optional. icon: PropTypes.string, }; From 1551657d56c98b4159d56f40fd153b67062a6952 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Wed, 30 Jun 2021 09:02:00 +0100 Subject: [PATCH 166/221] Remove Tinter reference --- src/components/views/messages/MStickerBody.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/views/messages/MStickerBody.js b/src/components/views/messages/MStickerBody.js index 54eb7649b4..f995b15c7c 100644 --- a/src/components/views/messages/MStickerBody.js +++ b/src/components/views/messages/MStickerBody.js @@ -42,8 +42,7 @@ export default class MStickerBody extends MImageBody { // Placeholder to show in place of the sticker image if // img onLoad hasn't fired yet. getPlaceholder() { - const TintableSVG = sdk.getComponent('elements.TintableSvg'); - return ; + return ; } // Tooltip to show on mouse over From 2fa2877aed492f30a09b348a328b4bb9c123155e Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Mon, 5 Jul 2021 15:00:00 +0100 Subject: [PATCH 167/221] Upgrade matrix-js-sdk to 12.0.1 --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 1994de4453..9b8b0c05b9 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,7 @@ "katex": "^0.12.0", "linkifyjs": "^2.1.9", "lodash": "^4.17.20", - "matrix-js-sdk": "12.0.1-rc.1", + "matrix-js-sdk": "12.0.1", "matrix-widget-api": "^0.1.0-beta.15", "minimist": "^1.2.5", "opus-recorder": "^8.0.3", diff --git a/yarn.lock b/yarn.lock index 8bef69428a..58de824039 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5741,10 +5741,10 @@ mathml-tag-names@^2.1.3: resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz#4ddadd67308e780cf16a47685878ee27b736a0a3" integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg== -matrix-js-sdk@12.0.1-rc.1: - version "12.0.1-rc.1" - resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-12.0.1-rc.1.tgz#da8a1c38e65ccfe9cb53a6aaf681488df0667ad5" - integrity sha512-ksz/oFh4hc/+gbNQ3OIGKhuVd9Vy9lUYhoaSF2efxO4NnPTwbjLNFKzgmQ+CV5ixUgmUcn972Tv0vyNZH9DTGg== +matrix-js-sdk@12.0.1: + version "12.0.1" + resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-12.0.1.tgz#3a63881f743420a4d39474daa39bd0fb90930d43" + integrity sha512-HkOWv8QHojceo3kPbC+vAIFUjsRAig6MBvEY35UygS3g2dL0UcJ5Qx09/2wcXtu6dowlDnWsz2HHk62tS2cklA== dependencies: "@babel/runtime" "^7.12.5" another-json "^0.2.0" From 24d0906653fe497bce32b80b78026ea8c5c59e00 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Mon, 5 Jul 2021 15:06:59 +0100 Subject: [PATCH 168/221] Prepare changelog for v3.25.0 --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index efd55925a3..22b35b7c59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +Changes in [3.25.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.25.0) (2021-07-05) +===================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.25.0-rc.1...v3.25.0) + + * Remove reminescent references to the tinter + [\#6316](https://github.com/matrix-org/matrix-react-sdk/pull/6316) + * Update to released version of js-sdk + Changes in [3.25.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.25.0-rc.1) (2021-06-29) =============================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.24.0...v3.25.0-rc.1) From 8baa92f2b139b59dc1dc593e91561a7f2602394a Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Mon, 5 Jul 2021 15:07:00 +0100 Subject: [PATCH 169/221] v3.25.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9b8b0c05b9..a159e3bb16 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "3.25.0-rc.1", + "version": "3.25.0", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { From 7b4fd161e520468bb55656e5cbe5b0f21b2813a1 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Mon, 5 Jul 2021 16:27:07 +0100 Subject: [PATCH 170/221] Reset fields for development --- package.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 79fe323542..610404ba0d 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "bin": { "reskindex": "scripts/reskindex.js" }, - "main": "./lib/index.js", + "main": "./src/index.js", "matrix_src_main": "./src/index.js", "matrix_lib_main": "./lib/index.js", "matrix_lib_typings": "./lib/index.d.ts", @@ -196,6 +196,5 @@ "coverageReporters": [ "text" ] - }, - "typings": "./lib/index.d.ts" + } } From 38a061a2df6663a9eae8ec8d2734ddeb4969d503 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 6 Jul 2021 20:59:12 +0200 Subject: [PATCH 171/221] Fix ImageView context menu MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/elements/ImageView.tsx | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index 0de34251d8..90f5d18be7 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -24,7 +24,7 @@ import FocusLock from "react-focus-lock"; import MemberAvatar from "../avatars/MemberAvatar"; import { ContextMenuTooltipButton } from "../../../accessibility/context_menu/ContextMenuTooltipButton"; import MessageContextMenu from "../context_menus/MessageContextMenu"; -import { aboveLeftOf, ContextMenu } from '../../structures/ContextMenu'; +import { aboveLeftOf } from '../../structures/ContextMenu'; import MessageTimestamp from "../messages/MessageTimestamp"; import SettingsStore from "../../../settings/SettingsStore"; import { formatFullDate } from "../../../DateUtils"; @@ -304,17 +304,13 @@ export default class ImageView extends React.Component { let contextMenu = null; if (this.state.contextMenuDisplayed) { contextMenu = ( - - - + onCloseDialog={this.props.onFinished} + /> ); } From 8c1721fc351dd4d799ff074c6dd9694a1740ea9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 7 Jul 2021 08:32:06 +0200 Subject: [PATCH 172/221] Remove the old codeblock code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/rooms/_EventTile.scss | 7 ----- res/themes/dark/css/_dark.scss | 33 -------------------- res/themes/legacy-dark/css/_legacy-dark.scss | 15 ++------- 3 files changed, 2 insertions(+), 53 deletions(-) diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 27a83e58f8..e48f378d9d 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -477,8 +477,6 @@ $hover-select-border: 4px; pre, code { font-family: $monospace-font-family !important; - // deliberate constants as we're behind an invert filter - color: #333; } pre { @@ -488,11 +486,6 @@ $hover-select-border: 4px; overflow-x: overlay; overflow-y: visible; } - - code { - // deliberate constants as we're behind an invert filter - background-color: #f8f8f8; - } } .mx_EventTile_lineNumbers { diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index 81fd3c892a..2aca4c767e 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -119,8 +119,6 @@ $voipcall-plinth-color: #394049; $theme-button-bg-color: #e3e8f0; $dialpad-button-bg-color: #6F7882; -; - $roomlist-button-bg-color: rgba(141, 151, 165, 0.2); // Buttons include the filter box, explore button, and sublist buttons $roomlist-filter-active-bg-color: $bg-color; @@ -274,24 +272,7 @@ $composer-shadow-color: rgba(0, 0, 0, 0.28); } // markdown overrides: -.mx_EventTile_content .markdown-body pre:hover { - border-color: #808080 !important; // inverted due to rules below - scrollbar-color: rgba(0, 0, 0, 0.2) transparent; // copied from light theme due to inversion below - // the code above works only in Firefox, this is for other browsers - // see https://developer.mozilla.org/en-US/docs/Web/CSS/scrollbar-color - &::-webkit-scrollbar-thumb { - background-color: rgba(0, 0, 0, 0.2); // copied from light theme due to inversion below - } -} .mx_EventTile_content .markdown-body { - pre, code { - filter: invert(1); - } - - pre code { - filter: none; - } - table { tr { background-color: #000000; @@ -301,18 +282,4 @@ $composer-shadow-color: rgba(0, 0, 0, 0.28); background-color: #080808; } } - - blockquote { - color: #919191; - } -} - -// diff highlight colors -// intentionally swapped to avoid inversion -.hljs-addition { - background: #fdd; -} - -.hljs-deletion { - background: #dfd; } diff --git a/res/themes/legacy-dark/css/_legacy-dark.scss b/res/themes/legacy-dark/css/_legacy-dark.scss index df01efbe1e..fa56d24c81 100644 --- a/res/themes/legacy-dark/css/_legacy-dark.scss +++ b/res/themes/legacy-dark/css/_legacy-dark.scss @@ -118,7 +118,7 @@ $voipcall-plinth-color: #394049; $theme-button-bg-color: #e3e8f0; $dialpad-button-bg-color: #6F7882; -; + $roomlist-button-bg-color: #1A1D23; // Buttons include the filter box, explore button, and sublist buttons $roomlist-filter-active-bg-color: $roomlist-button-bg-color; @@ -249,7 +249,7 @@ $composer-shadow-color: tranparent; @define-mixin mx_DialogButton_secondary { // flip colours for the secondary ones font-weight: 600; - border: 1px solid $accent-color ! important; + border: 1px solid $accent-color !important; color: $accent-color; background-color: $button-secondary-bg-color; } @@ -267,18 +267,7 @@ $composer-shadow-color: tranparent; } // markdown overrides: -.mx_EventTile_content .markdown-body pre:hover { - border-color: #808080 !important; // inverted due to rules below -} .mx_EventTile_content .markdown-body { - pre, code { - filter: invert(1); - } - - pre code { - filter: none; - } - table { tr { background-color: #000000; From 9d12439ee71a14df0ce73602dfad7ce145d6f91a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 7 Jul 2021 08:42:16 +0200 Subject: [PATCH 173/221] Give codeblocks a background color MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/rooms/_EventTile.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index e48f378d9d..55f73c0315 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -477,6 +477,7 @@ $hover-select-border: 4px; pre, code { font-family: $monospace-font-family !important; + background-color: $header-panel-bg-color; } pre { From f8307a92d88b84d986e8ef488538cf91f8c4fb48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 7 Jul 2021 09:22:12 +0200 Subject: [PATCH 174/221] Use atom-one-dark for dark theme MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/themes/dark/css/_dark.scss | 5 +++++ res/themes/dark/css/dark.scss | 1 + res/themes/legacy-dark/css/_legacy-dark.scss | 11 +++-------- res/themes/legacy-dark/css/legacy-dark.scss | 1 + 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index 2aca4c767e..f5b23d2c3d 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -283,3 +283,8 @@ $composer-shadow-color: rgba(0, 0, 0, 0.28); } } } + +// highlight.js overrides +.hljs-tag { + color: inherit; +} diff --git a/res/themes/dark/css/dark.scss b/res/themes/dark/css/dark.scss index f9695018e4..600cfd528a 100644 --- a/res/themes/dark/css/dark.scss +++ b/res/themes/dark/css/dark.scss @@ -9,3 +9,4 @@ @import "_dark.scss"; @import "../../light/css/_mods.scss"; @import "../../../../res/css/_components.scss"; +@import url("highlight.js/styles/atom-one-dark.css"); diff --git a/res/themes/legacy-dark/css/_legacy-dark.scss b/res/themes/legacy-dark/css/_legacy-dark.scss index fa56d24c81..ec4c3094de 100644 --- a/res/themes/legacy-dark/css/_legacy-dark.scss +++ b/res/themes/legacy-dark/css/_legacy-dark.scss @@ -279,12 +279,7 @@ $composer-shadow-color: tranparent; } } -// diff highlight colors -// intentionally swapped to avoid inversion -.hljs-addition { - background: #fdd; -} - -.hljs-deletion { - background: #dfd; +// highlight.js overrides: +.hljs-tag { + color: inherit; } diff --git a/res/themes/legacy-dark/css/legacy-dark.scss b/res/themes/legacy-dark/css/legacy-dark.scss index 2a4d432d26..840794f7c0 100644 --- a/res/themes/legacy-dark/css/legacy-dark.scss +++ b/res/themes/legacy-dark/css/legacy-dark.scss @@ -4,3 +4,4 @@ @import "../../legacy-light/css/_legacy-light.scss"; @import "_legacy-dark.scss"; @import "../../../../res/css/_components.scss"; +@import url("highlight.js/styles/atom-one-dark.css"); From 2cd2015e3673cf21a18ac88aba6eb0659a64dbec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 7 Jul 2021 10:24:26 +0200 Subject: [PATCH 175/221] Use atom-one-light for light theme MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/themes/legacy-light/css/legacy-light.scss | 1 + res/themes/light/css/light.scss | 1 + 2 files changed, 2 insertions(+) diff --git a/res/themes/legacy-light/css/legacy-light.scss b/res/themes/legacy-light/css/legacy-light.scss index e39a1765f3..347d240fc6 100644 --- a/res/themes/legacy-light/css/legacy-light.scss +++ b/res/themes/legacy-light/css/legacy-light.scss @@ -3,3 +3,4 @@ @import "_fonts.scss"; @import "_legacy-light.scss"; @import "../../../../res/css/_components.scss"; +@import url("highlight.js/styles/atom-one-light.css"); diff --git a/res/themes/light/css/light.scss b/res/themes/light/css/light.scss index f31ce5c139..4e912bc756 100644 --- a/res/themes/light/css/light.scss +++ b/res/themes/light/css/light.scss @@ -4,3 +4,4 @@ @import "_light.scss"; @import "_mods.scss"; @import "../../../../res/css/_components.scss"; +@import url("highlight.js/styles/atom-one-light.css"); From b5bad0cc237a6cf22ffe53cbd0d66eaf3919f38b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 7 Jul 2021 10:28:29 +0200 Subject: [PATCH 176/221] Add comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/themes/dark/css/_dark.scss | 2 +- res/themes/legacy-dark/css/_legacy-dark.scss | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index f5b23d2c3d..57cbc7efa9 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -286,5 +286,5 @@ $composer-shadow-color: rgba(0, 0, 0, 0.28); // highlight.js overrides .hljs-tag { - color: inherit; + color: inherit; // Without this they'd be weirdly blue which doesn't match the theme } diff --git a/res/themes/legacy-dark/css/_legacy-dark.scss b/res/themes/legacy-dark/css/_legacy-dark.scss index ec4c3094de..555ef4f66c 100644 --- a/res/themes/legacy-dark/css/_legacy-dark.scss +++ b/res/themes/legacy-dark/css/_legacy-dark.scss @@ -281,5 +281,5 @@ $composer-shadow-color: tranparent; // highlight.js overrides: .hljs-tag { - color: inherit; + color: inherit; // Without this they'd be weirdly blue which doesn't match the theme } From 984dc40f9cb10bf360acbc200f33b485eb59dc52 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 7 Jul 2021 10:54:21 +0100 Subject: [PATCH 177/221] Clarify the keys we use when submitting rageshakes 'pk' is a bit opaque in a context with both private keys and public keys --- src/rageshake/submit-rageshake.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/rageshake/submit-rageshake.ts b/src/rageshake/submit-rageshake.ts index 5e0b2c3c4d..b629ddafd8 100644 --- a/src/rageshake/submit-rageshake.ts +++ b/src/rageshake/submit-rageshake.ts @@ -93,15 +93,15 @@ async function collectBugReport(opts: IOpts = {}, gzipLogs = true) { body.append("cross_signing_supported_by_hs", String(await client.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing"))); body.append("cross_signing_key", crossSigning.getId()); - body.append("cross_signing_pk_in_secret_storage", + body.append("cross_signing_privkey_in_secret_storage", String(!!(await crossSigning.isStoredInSecretStorage(secretStorage)))); const pkCache = client.getCrossSigningCacheCallbacks(); - body.append("cross_signing_master_pk_cached", + body.append("cross_signing_master_privkey_cached", String(!!(pkCache && await pkCache.getCrossSigningKeyCache("master")))); - body.append("cross_signing_self_signing_pk_cached", + body.append("cross_signing_self_signing_privkey_cached", String(!!(pkCache && await pkCache.getCrossSigningKeyCache("self_signing")))); - body.append("cross_signing_user_signing_pk_cached", + body.append("cross_signing_user_signing_privkey_cached", String(!!(pkCache && await pkCache.getCrossSigningKeyCache("user_signing")))); body.append("secret_storage_ready", String(await client.isSecretStorageReady())); From 06337e72fa875657640d19dedd26c555b742d5b5 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Wed, 7 Jul 2021 12:10:35 +0200 Subject: [PATCH 178/221] Update lockfile with correct dependencies --- yarn.lock | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/yarn.lock b/yarn.lock index c8c3315855..ea4adfb09f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1677,6 +1677,11 @@ "@types/scheduler" "*" csstype "^3.0.2" +"@types/retry@^0.12.0": + version "0.12.0" + resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d" + integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA== + "@types/sanitize-html@^2.3.1": version "2.3.1" resolved "https://registry.yarnpkg.com/@types/sanitize-html/-/sanitize-html-2.3.1.tgz#094d696b83b7394b016e96342bbffa6a028795ce" @@ -5445,10 +5450,10 @@ mathml-tag-names@^2.1.3: resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz#4ddadd67308e780cf16a47685878ee27b736a0a3" integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg== -matrix-js-sdk@12.0.0: - version "12.0.0" - resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-12.0.0.tgz#8ee7cc37661476341d0c792a1a12bc78b19f9fdd" - integrity sha512-DHeq87Sx9Dv37FYyvZkmA1VYsQUNaVgc3QzMUkFwoHt1T4EZzgyYpdsp3uYruJzUW0ACvVJcwFdrU4e1VS97dQ== +matrix-js-sdk@12.0.1: + version "12.0.1" + resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-12.0.1.tgz#3a63881f743420a4d39474daa39bd0fb90930d43" + integrity sha512-HkOWv8QHojceo3kPbC+vAIFUjsRAig6MBvEY35UygS3g2dL0UcJ5Qx09/2wcXtu6dowlDnWsz2HHk62tS2cklA== dependencies: "@babel/runtime" "^7.12.5" another-json "^0.2.0" @@ -5456,6 +5461,7 @@ matrix-js-sdk@12.0.0: bs58 "^4.0.1" content-type "^1.0.4" loglevel "^1.7.1" + p-retry "^4.5.0" qs "^6.9.6" request "^2.88.2" unhomoglyph "^1.0.6" @@ -6007,6 +6013,14 @@ p-locate@^4.1.0: dependencies: p-limit "^2.2.0" +p-retry@^4.5.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-4.6.0.tgz#9de15ae696278cffe86fce2d8f73b7f894f8bc9e" + integrity sha512-SAHbQEwg3X5DRNaLmWjT+DlGc93ba5i+aP3QLfVNDncQEQO4xjbYW4N/lcVTSuP0aJietGfx2t94dJLzfBMpXw== + dependencies: + "@types/retry" "^0.12.0" + retry "^0.13.1" + p-try@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" @@ -6816,6 +6830,11 @@ ret@~0.1.10: resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== +retry@^0.13.1: + version "0.13.1" + resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658" + integrity sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg== + reusify@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" From 1220ad37ce102bff3db97498b8c7327266ba6d5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 7 Jul 2021 11:54:53 +0200 Subject: [PATCH 179/221] Remove the usage of symbol MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/structures/RoomDirectory.tsx | 2 +- src/components/views/directory/NetworkDropdown.tsx | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/structures/RoomDirectory.tsx b/src/components/structures/RoomDirectory.tsx index 3acd9f1a2e..fe07920d95 100644 --- a/src/components/structures/RoomDirectory.tsx +++ b/src/components/structures/RoomDirectory.tsx @@ -61,7 +61,7 @@ interface IState { loading: boolean; protocolsLoading: boolean; error?: string; - instanceId: string | symbol; + instanceId: string; roomServer: string; filterString: string; selectedCommunityId?: string; diff --git a/src/components/views/directory/NetworkDropdown.tsx b/src/components/views/directory/NetworkDropdown.tsx index c57aa7bccc..155349e39d 100644 --- a/src/components/views/directory/NetworkDropdown.tsx +++ b/src/components/views/directory/NetworkDropdown.tsx @@ -41,7 +41,8 @@ import QuestionDialog from "../dialogs/QuestionDialog"; import UIStore from "../../../stores/UIStore"; import { compare } from "../../../utils/strings"; -export const ALL_ROOMS = Symbol("ALL_ROOMS"); +// XXX: We would ideally use a symbol here but we can't since we save this value to localStorage +export const ALL_ROOMS = "ALL_ROOMS"; const SETTING_NAME = "room_directory_servers"; @@ -94,8 +95,7 @@ export interface IInstance { fields: object; network_id: string; // XXX: this is undocumented but we rely on it. - // we inject a fake entry with a symbolic instance_id. - instance_id: string | symbol; + instance_id: string; } export interface IProtocol { @@ -112,8 +112,8 @@ export type Protocols = Record; interface IProps { protocols: Protocols; selectedServerName: string; - selectedInstanceId: string | symbol; - onOptionChange(server: string, instanceId?: string | symbol): void; + selectedInstanceId: string; + onOptionChange(server: string, instanceId?: string): void; } // This dropdown sources homeservers from three places: @@ -171,7 +171,7 @@ const NetworkDropdown = ({ onOptionChange, protocols = {}, selectedServerName, s const protocolsList = server === hsName ? Object.values(protocols) : []; if (protocolsList.length > 0) { - // add a fake protocol with the ALL_ROOMS symbol + // add a fake protocol with the ALL_ROOMS protocolsList.push({ instances: [{ fields: [], From b94dc2d0e5b3eea9d62077f99c206b8c42e06707 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 7 Jul 2021 12:01:08 +0200 Subject: [PATCH 180/221] Remember the last used server for room directory searches MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/structures/RoomDirectory.tsx | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/components/structures/RoomDirectory.tsx b/src/components/structures/RoomDirectory.tsx index fe07920d95..c0fdcebc75 100644 --- a/src/components/structures/RoomDirectory.tsx +++ b/src/components/structures/RoomDirectory.tsx @@ -48,6 +48,9 @@ import { ActionPayload } from "../../dispatcher/payloads"; const MAX_NAME_LENGTH = 80; const MAX_TOPIC_LENGTH = 800; +const LAST_SERVER_KEY = "mx_last_room_directory_server"; +const LAST_INSTANCE_KEY = "mx_last_room_directory_instance"; + function track(action: string) { Analytics.trackEvent('RoomDirectory', action); } @@ -150,8 +153,8 @@ export default class RoomDirectory extends React.Component { publicRooms: [], loading: true, error: null, - instanceId: undefined, - roomServer: MatrixClientPeg.getHomeserverName(), + instanceId: localStorage.getItem(LAST_INSTANCE_KEY), + roomServer: localStorage.getItem(LAST_SERVER_KEY) || MatrixClientPeg.getHomeserverName(), filterString: this.props.initialText || "", selectedCommunityId, communityName: null, @@ -342,7 +345,7 @@ export default class RoomDirectory extends React.Component { } }; - private onOptionChange = (server: string, instanceId?: string | symbol) => { + private onOptionChange = (server: string, instanceId?: string) => { // clear next batch so we don't try to load more rooms this.nextBatch = null; this.setState({ @@ -360,6 +363,14 @@ export default class RoomDirectory extends React.Component { // find the five gitter ones, at which point we do not want // to render all those rooms when switching back to 'all networks'. // Easiest to just blow away the state & re-fetch. + + // We have to be careful here so that we don't set instanceId = "undefined" + localStorage.setItem(LAST_SERVER_KEY, server); + if (instanceId) { + localStorage.setItem(LAST_INSTANCE_KEY, instanceId); + } else { + localStorage.removeItem(LAST_INSTANCE_KEY); + } }; private onFillRequest = (backwards: boolean) => { From 255ab49ccbbc65f7235460c83c9bebe59669d45f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 7 Jul 2021 13:48:48 +0200 Subject: [PATCH 181/221] Handle edge cases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/structures/RoomDirectory.tsx | 28 ++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/src/components/structures/RoomDirectory.tsx b/src/components/structures/RoomDirectory.tsx index c0fdcebc75..3892c9da5c 100644 --- a/src/components/structures/RoomDirectory.tsx +++ b/src/components/structures/RoomDirectory.tsx @@ -119,7 +119,29 @@ export default class RoomDirectory extends React.Component { } else if (!selectedCommunityId) { MatrixClientPeg.get().getThirdpartyProtocols().then((response) => { this.protocols = response; - this.setState({ protocolsLoading: false }); + const myHomeserver = MatrixClientPeg.getHomeserverName(); + const lsRoomServer = localStorage.getItem(LAST_SERVER_KEY); + const lsInstanceId = localStorage.getItem(LAST_INSTANCE_KEY); + const configSevers = SdkConfig.get().roomDirectory?.servers || []; + const roomServer = configSevers.includes(lsRoomServer) + ? lsRoomServer + : myHomeserver; + const instanceIds = []; + if (roomServer === myHomeserver) { + Object.values(this.protocols).forEach((protocol) => { + protocol.instances.forEach((instance) => instanceIds.push(instance.instance_id)); + }); + } + const instanceId = (instanceIds.includes(lsInstanceId) || lsInstanceId === ALL_ROOMS) + ? lsInstanceId + : null; + + this.setState({ + protocolsLoading: false, + instanceId: instanceId, + roomServer: roomServer, + }); + this.refreshRoomList(); }, (err) => { console.warn(`error loading third party protocols: ${err}`); this.setState({ protocolsLoading: false }); @@ -153,8 +175,8 @@ export default class RoomDirectory extends React.Component { publicRooms: [], loading: true, error: null, - instanceId: localStorage.getItem(LAST_INSTANCE_KEY), - roomServer: localStorage.getItem(LAST_SERVER_KEY) || MatrixClientPeg.getHomeserverName(), + instanceId: null, + roomServer: null, filterString: this.props.initialText || "", selectedCommunityId, communityName: null, From 5b5ef5e04c5f7ccfd77ba4c717c2a5f0a6f560cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 7 Jul 2021 13:59:02 +0200 Subject: [PATCH 182/221] Handle servers from settings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/structures/RoomDirectory.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/structures/RoomDirectory.tsx b/src/components/structures/RoomDirectory.tsx index 3892c9da5c..4a0e7615c4 100644 --- a/src/components/structures/RoomDirectory.tsx +++ b/src/components/structures/RoomDirectory.tsx @@ -123,7 +123,8 @@ export default class RoomDirectory extends React.Component { const lsRoomServer = localStorage.getItem(LAST_SERVER_KEY); const lsInstanceId = localStorage.getItem(LAST_INSTANCE_KEY); const configSevers = SdkConfig.get().roomDirectory?.servers || []; - const roomServer = configSevers.includes(lsRoomServer) + const settingsServers = SettingsStore.getValue("room_directory_servers") || []; + const roomServer = [...configSevers, ...settingsServers].includes(lsRoomServer) ? lsRoomServer : myHomeserver; const instanceIds = []; From 30fa5419dbab646ca4885e0ad465c0a75a73f022 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 7 Jul 2021 14:30:08 +0100 Subject: [PATCH 183/221] Revert "ignore hash/fragment when de-duplicating links for url previews" --- src/components/views/messages/TextualBody.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/components/views/messages/TextualBody.tsx b/src/components/views/messages/TextualBody.tsx index 6ba018c512..c1df39cf27 100644 --- a/src/components/views/messages/TextualBody.tsx +++ b/src/components/views/messages/TextualBody.tsx @@ -294,15 +294,15 @@ export default class TextualBody extends React.Component { // pass only the first child which is the event tile otherwise this recurses on edited events let links = this.findLinks([this.contentRef.current]); if (links.length) { - // de-duplicate the links after stripping hashes as they don't affect the preview - // using a set here maintains the order - links = Array.from(new Set(links.map(link => { - const url = new URL(link); - url.hash = ""; - return url.toString(); - }))); + // de-dup the links (but preserve ordering) + const seen = new Set(); + links = links.filter((link) => { + if (seen.has(link)) return false; + seen.add(link); + return true; + }); - this.setState({ links }); + this.setState({ links: links }); // lazy-load the hidden state of the preview widget from localstorage if (window.localStorage) { From 5fb7dbee3e83ee1e3b7b2d09df8424c6fa45cf14 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Wed, 7 Jul 2021 16:11:47 +0200 Subject: [PATCH 184/221] Do not generate a lockfile when running in CI --- scripts/ci/install-deps.sh | 4 ++-- scripts/ci/layered.sh | 6 +++--- test/end-to-end-tests/element/install.sh | 2 +- test/end-to-end-tests/install.sh | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/scripts/ci/install-deps.sh b/scripts/ci/install-deps.sh index bbda74ef9d..fcbf6b1198 100755 --- a/scripts/ci/install-deps.sh +++ b/scripts/ci/install-deps.sh @@ -6,8 +6,8 @@ scripts/fetchdep.sh matrix-org matrix-js-sdk pushd matrix-js-sdk yarn link -yarn install $@ +yarn install --pure-lockfile $@ popd yarn link matrix-js-sdk -yarn install $@ +yarn install --pure-lockfile $@ diff --git a/scripts/ci/layered.sh b/scripts/ci/layered.sh index 039f90c7df..2e163456fe 100755 --- a/scripts/ci/layered.sh +++ b/scripts/ci/layered.sh @@ -13,13 +13,13 @@ scripts/fetchdep.sh matrix-org matrix-js-sdk pushd matrix-js-sdk yarn link -yarn install +yarn install --pure-lockfile popd # Now set up the react-sdk yarn link matrix-js-sdk yarn link -yarn install +yarn install --pure-lockfile yarn reskindex # Finally, set up element-web @@ -27,6 +27,6 @@ scripts/fetchdep.sh vector-im element-web pushd element-web yarn link matrix-js-sdk yarn link matrix-react-sdk -yarn install +yarn install --pure-lockfile yarn build:res popd diff --git a/test/end-to-end-tests/element/install.sh b/test/end-to-end-tests/element/install.sh index e38f795df1..e1df709c68 100755 --- a/test/end-to-end-tests/element/install.sh +++ b/test/end-to-end-tests/element/install.sh @@ -12,5 +12,5 @@ unzip -q element.zip rm element.zip mv element-web-${ELEMENT_BRANCH} element-web cd element-web -yarn install +yarn install --pure-lockfile yarn run build diff --git a/test/end-to-end-tests/install.sh b/test/end-to-end-tests/install.sh index bbe7a24c9b..b2254a4ea7 100755 --- a/test/end-to-end-tests/install.sh +++ b/test/end-to-end-tests/install.sh @@ -4,4 +4,4 @@ set -e ./synapse/install.sh # local testing doesn't need a Element fetched from master, # so not installing that by default -yarn install +yarn install --pure-lockfile From 14303a4ca8da6b93451d4895e292c44ac4c97cc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 7 Jul 2021 17:20:31 +0200 Subject: [PATCH 185/221] Delint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/structures/auth/ForgotPassword.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/auth/ForgotPassword.tsx b/src/components/structures/auth/ForgotPassword.tsx index 432f69fd1b..6382e143f9 100644 --- a/src/components/structures/auth/ForgotPassword.tsx +++ b/src/components/structures/auth/ForgotPassword.tsx @@ -45,7 +45,7 @@ enum Phase { interface IProps { serverConfig: ValidatedServerConfig; - onServerConfigChange: () => void; + onServerConfigChange: (serverConfig: ValidatedServerConfig) => void; onLoginClick?: () => void; onComplete: () => void; } From b9a539eaa280318abcde6573010d4e0ab3d2f2dd Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 7 Jul 2021 18:04:30 +0100 Subject: [PATCH 186/221] Improve URL Previews only show 2 by default with expand/collapse mechanism show all hashes again, but dedup requests clean up hide mechanism, instead of one `x` per preview have one per group --- res/css/_components.scss | 1 + res/css/views/rooms/_LinkPreviewGroup.scss | 38 ++++++++++ res/css/views/rooms/_LinkPreviewWidget.scss | 16 ---- src/components/views/messages/TextualBody.tsx | 29 +++---- .../views/rooms/LinkPreviewGroup.tsx | 76 +++++++++++++++++++ ...PreviewWidget.js => LinkPreviewWidget.tsx} | 61 +++++++-------- src/i18n/strings/en_EN.json | 2 + 7 files changed, 155 insertions(+), 68 deletions(-) create mode 100644 res/css/views/rooms/_LinkPreviewGroup.scss create mode 100644 src/components/views/rooms/LinkPreviewGroup.tsx rename src/components/views/rooms/{LinkPreviewWidget.js => LinkPreviewWidget.tsx} (73%) diff --git a/res/css/_components.scss b/res/css/_components.scss index 389be11c60..7360c61c25 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -201,6 +201,7 @@ @import "./views/rooms/_GroupLayout.scss"; @import "./views/rooms/_IRCLayout.scss"; @import "./views/rooms/_JumpToBottomButton.scss"; +@import "./views/rooms/_LinkPreviewGroup.scss"; @import "./views/rooms/_LinkPreviewWidget.scss"; @import "./views/rooms/_MemberInfo.scss"; @import "./views/rooms/_MemberList.scss"; diff --git a/res/css/views/rooms/_LinkPreviewGroup.scss b/res/css/views/rooms/_LinkPreviewGroup.scss new file mode 100644 index 0000000000..ed341904fd --- /dev/null +++ b/res/css/views/rooms/_LinkPreviewGroup.scss @@ -0,0 +1,38 @@ +/* +Copyright 2021 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. +*/ + +.mx_LinkPreviewGroup { + .mx_LinkPreviewGroup_hide { + cursor: pointer; + width: 18px; + height: 18px; + + img { + flex: 0 0 40px; + visibility: hidden; + } + } + + &:hover .mx_LinkPreviewGroup_hide img, + .mx_LinkPreviewGroup_hide.focus-visible:focus img { + visibility: visible; + } + + > .mx_AccessibleButton { + color: $accent-color; + text-align: center; + } +} diff --git a/res/css/views/rooms/_LinkPreviewWidget.scss b/res/css/views/rooms/_LinkPreviewWidget.scss index 022cf3ed28..d547b749f8 100644 --- a/res/css/views/rooms/_LinkPreviewWidget.scss +++ b/res/css/views/rooms/_LinkPreviewWidget.scss @@ -51,22 +51,6 @@ limitations under the License. word-wrap: break-word; } -.mx_LinkPreviewWidget_cancel { - cursor: pointer; - width: 18px; - height: 18px; - - img { - flex: 0 0 40px; - visibility: hidden; - } -} - -.mx_LinkPreviewWidget:hover .mx_LinkPreviewWidget_cancel img, -.mx_LinkPreviewWidget_cancel.focus-visible:focus img { - visibility: visible; -} - .mx_MatrixChat_useCompactLayout { .mx_LinkPreviewWidget { margin-top: 6px; diff --git a/src/components/views/messages/TextualBody.tsx b/src/components/views/messages/TextualBody.tsx index c1df39cf27..9c2786c642 100644 --- a/src/components/views/messages/TextualBody.tsx +++ b/src/components/views/messages/TextualBody.tsx @@ -45,7 +45,7 @@ import Spoiler from "../elements/Spoiler"; import QuestionDialog from "../dialogs/QuestionDialog"; import MessageEditHistoryDialog from "../dialogs/MessageEditHistoryDialog"; import EditMessageComposer from '../rooms/EditMessageComposer'; -import LinkPreviewWidget from '../rooms/LinkPreviewWidget'; +import LinkPreviewGroup from '../rooms/LinkPreviewGroup'; interface IProps { /* the MatrixEvent to show */ @@ -294,15 +294,9 @@ export default class TextualBody extends React.Component { // pass only the first child which is the event tile otherwise this recurses on edited events let links = this.findLinks([this.contentRef.current]); if (links.length) { - // de-dup the links (but preserve ordering) - const seen = new Set(); - links = links.filter((link) => { - if (seen.has(link)) return false; - seen.add(link); - return true; - }); - - this.setState({ links: links }); + // de-duplicate the links using a set here maintains the order + links = Array.from(new Set(links)); + this.setState({ links }); // lazy-load the hidden state of the preview widget from localstorage if (window.localStorage) { @@ -530,15 +524,12 @@ export default class TextualBody extends React.Component { let widgets; if (this.state.links.length && !this.state.widgetHidden && this.props.showUrlPreview) { - widgets = this.state.links.map((link)=>{ - return ; - }); + widgets = ; } switch (content.msgtype) { diff --git a/src/components/views/rooms/LinkPreviewGroup.tsx b/src/components/views/rooms/LinkPreviewGroup.tsx new file mode 100644 index 0000000000..ff6fd4afd2 --- /dev/null +++ b/src/components/views/rooms/LinkPreviewGroup.tsx @@ -0,0 +1,76 @@ +/* +Copyright 2021 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. +*/ + +import React, { useEffect } from "react"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; + +import { useStateToggle } from "../../../hooks/useStateToggle"; +import LinkPreviewWidget from "./LinkPreviewWidget"; +import AccessibleButton from "../elements/AccessibleButton"; +import { _t } from "../../../languageHandler"; + +const INITIAL_NUM_PREVIEWS = 2; + +interface IProps { + links: string[]; // the URLs to be previewed + mxEvent: MatrixEvent; // the Event associated with the preview + onCancelClick?(): void; // called when the preview's cancel ('hide') button is clicked + onHeightChanged?(): void; // called when the preview's contents has loaded +} + +const LinkPreviewGroup: React.FC = ({ links, mxEvent, onCancelClick, onHeightChanged }) => { + const [expanded, toggleExpanded] = useStateToggle(); + useEffect(() => { + onHeightChanged(); + }, [onHeightChanged, expanded]); + + const shownLinks = expanded ? links : links.slice(0, INITIAL_NUM_PREVIEWS); + + let toggleButton; + if (links.length > INITIAL_NUM_PREVIEWS) { + toggleButton = + { expanded + ? _t("Collapse") + : _t("Show %(count)s other previews", { count: links.length - shownLinks.length }) } + ; + } + + return
    + { shownLinks.map((link, i) => ( + + { i === 0 ? ( + + + + ): undefined } + + )) } + { toggleButton } +
    ; +}; + +export default LinkPreviewGroup; diff --git a/src/components/views/rooms/LinkPreviewWidget.js b/src/components/views/rooms/LinkPreviewWidget.tsx similarity index 73% rename from src/components/views/rooms/LinkPreviewWidget.js rename to src/components/views/rooms/LinkPreviewWidget.tsx index 360ca41d55..db13021b32 100644 --- a/src/components/views/rooms/LinkPreviewWidget.js +++ b/src/components/views/rooms/LinkPreviewWidget.tsx @@ -1,6 +1,5 @@ /* -Copyright 2016 OpenMarket Ltd -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2016 - 2021 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. @@ -16,26 +15,33 @@ limitations under the License. */ import React, { createRef } from 'react'; -import PropTypes from 'prop-types'; import { AllHtmlEntities } from 'html-entities'; +import { MatrixEvent } from 'matrix-js-sdk/src/models/event'; +import { IPreviewUrlResponse } from 'matrix-js-sdk/src/client'; + import { linkifyElement } from '../../../HtmlUtils'; import SettingsStore from "../../../settings/SettingsStore"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; -import * as sdk from "../../../index"; import Modal from "../../../Modal"; import * as ImageUtils from "../../../ImageUtils"; -import { _t } from "../../../languageHandler"; import { replaceableComponent } from "../../../utils/replaceableComponent"; import { mediaFromMxc } from "../../../customisations/Media"; +import ImageView from '../elements/ImageView'; + +interface IProps { + link: string; // the URL being previewed + mxEvent: MatrixEvent; // the Event associated with the preview + onHeightChanged(): void; // called when the preview's contents has loaded +} + +interface IState { + preview?: IPreviewUrlResponse; +} @replaceableComponent("views.rooms.LinkPreviewWidget") -export default class LinkPreviewWidget extends React.Component { - static propTypes = { - link: PropTypes.string.isRequired, // the URL being previewed - mxEvent: PropTypes.object.isRequired, // the Event associated with the preview - onCancelClick: PropTypes.func, // called when the preview's cancel ('hide') button is clicked - onHeightChanged: PropTypes.func, // called when the preview's contents has loaded - }; +export default class LinkPreviewWidget extends React.Component { + private unmounted = false; + private readonly description = createRef(); constructor(props) { super(props); @@ -44,31 +50,25 @@ export default class LinkPreviewWidget extends React.Component { preview: null, }; - this.unmounted = false; - MatrixClientPeg.get().getUrlPreview(this.props.link, this.props.mxEvent.getTs()).then((res)=>{ + MatrixClientPeg.get().getUrlPreview(this.props.link, this.props.mxEvent.getTs()).then((preview) => { if (this.unmounted) { return; } - this.setState( - { preview: res }, - this.props.onHeightChanged, - ); - }, (error)=>{ + this.setState({ preview }, this.props.onHeightChanged); + }, (error) => { console.error("Failed to get URL preview: " + error); }); - - this._description = createRef(); } componentDidMount() { - if (this._description.current) { - linkifyElement(this._description.current); + if (this.description.current) { + linkifyElement(this.description.current); } } componentDidUpdate() { - if (this._description.current) { - linkifyElement(this._description.current); + if (this.description.current) { + linkifyElement(this.description.current); } } @@ -76,11 +76,10 @@ export default class LinkPreviewWidget extends React.Component { this.unmounted = true; } - onImageClick = ev => { + private onImageClick = ev => { const p = this.state.preview; if (ev.button != 0 || ev.metaKey) return; ev.preventDefault(); - const ImageView = sdk.getComponent("elements.ImageView"); let src = p["og:image"]; if (src && src.startsWith("mxc://")) { @@ -136,21 +135,17 @@ export default class LinkPreviewWidget extends React.Component { // opaque string. This does not allow any HTML to be injected into the DOM. const description = AllHtmlEntities.decode(p["og:description"] || ""); - const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); return (
    { img }
    { p["og:site_name"] ? (" - " + p["og:site_name"]) : null }
    -
    +
    { description }
    - - - + { this.props.children }
    ); } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index bbf6954435..7d4252545b 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1508,6 +1508,8 @@ "Your message was sent": "Your message was sent", "Failed to send": "Failed to send", "Scroll to most recent messages": "Scroll to most recent messages", + "Show %(count)s other previews|other": "Show %(count)s other previews", + "Show %(count)s other previews|one": "Show %(count)s other preview", "Close preview": "Close preview", "and %(count)s others...|other": "and %(count)s others...", "and %(count)s others...|one": "and one other...", From 4a6af5a4d715d31bb4e7c4f6407ca42730722048 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 7 Jul 2021 18:12:31 +0100 Subject: [PATCH 187/221] fix test missing required prop --- test/components/views/messages/TextualBody-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/components/views/messages/TextualBody-test.js b/test/components/views/messages/TextualBody-test.js index c9418fc557..c6a3f3c779 100644 --- a/test/components/views/messages/TextualBody-test.js +++ b/test/components/views/messages/TextualBody-test.js @@ -302,7 +302,7 @@ describe("", () => { event: true, }); - const wrapper = mount(); + const wrapper = mount( {}} />); expect(wrapper.text()).toBe(ev.getContent().body); let widgets = wrapper.find("LinkPreviewWidget"); From 882e66e215f8308bafef30ed5c804faf73cd25ce Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 7 Jul 2021 18:17:33 +0100 Subject: [PATCH 188/221] remove stale comment, we include vias now --- src/components/views/context_menus/MessageContextMenu.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/views/context_menus/MessageContextMenu.js b/src/components/views/context_menus/MessageContextMenu.js index a2086451cd..3e01954a21 100644 --- a/src/components/views/context_menus/MessageContextMenu.js +++ b/src/components/views/context_menus/MessageContextMenu.js @@ -327,7 +327,6 @@ export default class MessageContextMenu extends React.Component { if (this.props.permalinkCreator) { permalink = this.props.permalinkCreator.forEvent(this.props.mxEvent.getId()); } - // XXX: if we use room ID, we should also include a server where the event can be found (other than in the domain of the event ID) const permalinkButton = ( Date: Wed, 7 Jul 2021 19:43:12 +0100 Subject: [PATCH 189/221] Lower number of blurhash components and apply image ratio to it --- src/ContentMessages.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/ContentMessages.tsx b/src/ContentMessages.tsx index 66ca8a559f..670c175a48 100644 --- a/src/ContentMessages.tsx +++ b/src/ContentMessages.tsx @@ -49,8 +49,6 @@ const MAX_HEIGHT = 600; const PHYS_HIDPI = [0x00, 0x00, 0x16, 0x25, 0x00, 0x00, 0x16, 0x25, 0x01]; export const BLURHASH_FIELD = "xyz.amorgan.blurhash"; // MSC2448 -const BLURHASH_X_COMPONENTS = 6; -const BLURHASH_Y_COMPONENTS = 6; export class UploadCanceledError extends Error {} @@ -137,8 +135,9 @@ function createThumbnail( imageData.data, imageData.width, imageData.height, - BLURHASH_X_COMPONENTS, - BLURHASH_Y_COMPONENTS, + // use 4 components on the longer dimension, if square then both + imageData.width >= imageData.height ? 4 : 3, + imageData.height >= imageData.width ? 4 : 3, ); canvas.toBlob(function(thumbnail) { resolve({ From 4ce3723e84db6a9b5771f9adf73fca36abc2620d Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 7 Jul 2021 20:00:31 +0100 Subject: [PATCH 190/221] Switch to blurhash-react instead of homegrown component this has the advantage of resolution scaling logic to do more gpu accelerated scaling --- package.json | 1 + .../views/elements/BlurhashPlaceholder.tsx | 56 ------------------- src/components/views/messages/MImageBody.js | 4 +- yarn.lock | 5 ++ 4 files changed, 8 insertions(+), 58 deletions(-) delete mode 100644 src/components/views/elements/BlurhashPlaceholder.tsx diff --git a/package.json b/package.json index 610404ba0d..54425a1f74 100644 --- a/package.json +++ b/package.json @@ -92,6 +92,7 @@ "re-resizable": "^6.9.0", "react": "^17.0.2", "react-beautiful-dnd": "^13.1.0", + "react-blurhash": "^0.1.3", "react-dom": "^17.0.2", "react-focus-lock": "^2.5.0", "react-transition-group": "^4.4.1", diff --git a/src/components/views/elements/BlurhashPlaceholder.tsx b/src/components/views/elements/BlurhashPlaceholder.tsx deleted file mode 100644 index 0e59253fe8..0000000000 --- a/src/components/views/elements/BlurhashPlaceholder.tsx +++ /dev/null @@ -1,56 +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. - */ - -import React from 'react'; -import { decode } from "blurhash"; - -interface IProps { - blurhash: string; - width: number; - height: number; -} - -export default class BlurhashPlaceholder extends React.PureComponent { - private canvas: React.RefObject = React.createRef(); - - public componentDidMount() { - this.draw(); - } - - public componentDidUpdate() { - this.draw(); - } - - private draw() { - if (!this.canvas.current) return; - - try { - const { width, height } = this.props; - - const pixels = decode(this.props.blurhash, Math.ceil(width), Math.ceil(height)); - const ctx = this.canvas.current.getContext("2d"); - const imgData = ctx.createImageData(width, height); - imgData.data.set(pixels); - ctx.putImageData(imgData, 0, 0); - } catch (e) { - console.error("Error rendering blurhash: ", e); - } - } - - public render() { - return ; - } -} diff --git a/src/components/views/messages/MImageBody.js b/src/components/views/messages/MImageBody.js index 5566f5aec0..6da4aa1494 100644 --- a/src/components/views/messages/MImageBody.js +++ b/src/components/views/messages/MImageBody.js @@ -18,6 +18,7 @@ limitations under the License. import React, { createRef } from 'react'; import PropTypes from 'prop-types'; +import { Blurhash } from "react-blurhash"; import MFileBody from './MFileBody'; import Modal from '../../../Modal'; @@ -29,7 +30,6 @@ import MatrixClientContext from "../../../contexts/MatrixClientContext"; import InlineSpinner from '../elements/InlineSpinner'; import { replaceableComponent } from "../../../utils/replaceableComponent"; import { mediaFromContent } from "../../../customisations/Media"; -import BlurhashPlaceholder from "../elements/BlurhashPlaceholder"; import { BLURHASH_FIELD } from "../../../ContentMessages"; @replaceableComponent("views.messages.MImageBody") @@ -436,7 +436,7 @@ export default class MImageBody extends React.Component { // Overidden by MStickerBody getPlaceholder(width, height) { const blurhash = this.props.mxEvent.getContent().info[BLURHASH_FIELD]; - if (blurhash) return ; + if (blurhash) return ; return
    ; diff --git a/yarn.lock b/yarn.lock index ea4adfb09f..90f415673d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6454,6 +6454,11 @@ react-beautiful-dnd@^13.1.0: redux "^4.0.4" use-memo-one "^1.1.1" +react-blurhash@^0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/react-blurhash/-/react-blurhash-0.1.3.tgz#735f28f8f07fb358d7efe7e7e6dc65a7272bf89e" + integrity sha512-Q9lqbXg92NU6/2DoIl/cBM8YWL+Z4X66OiG4aT9ozOgjBwx104LHFCH5stf6aF+s0Q9Wf310Ul+dG+VXJltmPg== + react-clientside-effect@^1.2.2: version "1.2.3" resolved "https://registry.yarnpkg.com/react-clientside-effect/-/react-clientside-effect-1.2.3.tgz#95c95f520addfb71743608b990bfe01eb002012b" From d8bf618d73983515bc08f9038975b267c3cd00e9 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 7 Jul 2021 20:08:52 +0100 Subject: [PATCH 191/221] unrelated: move @types/commonmark to devDeps --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 54425a1f74..bb92ad11d8 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,6 @@ }, "dependencies": { "@babel/runtime": "^7.12.5", - "@types/commonmark": "^0.27.4", "await-lock": "^2.1.0", "blurhash": "^1.1.3", "browser-encrypt-attachment": "^0.3.0", @@ -125,6 +124,7 @@ "@peculiar/webcrypto": "^1.1.4", "@sinonjs/fake-timers": "^7.0.2", "@types/classnames": "^2.2.11", + "@types/commonmark": "^0.27.4", "@types/counterpart": "^0.18.1", "@types/diff-match-patch": "^1.0.32", "@types/flux": "^3.1.9", From 086a8cbbb39bdd6e68a597a6bab5ef05031a6047 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 7 Jul 2021 20:19:52 +0100 Subject: [PATCH 192/221] Update import location for types Apparently all the types themselves are fine, even though some of the function signatures have been updated to return Promises... we were alreaady await-ing on them. --- src/SecurityManager.ts | 3 ++- src/components/structures/auth/SetupEncryptionBody.tsx | 2 +- .../views/dialogs/security/AccessSecretStorageDialog.tsx | 2 +- src/customisations/Security.ts | 2 +- src/stores/SetupEncryptionStore.ts | 2 +- 5 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/SecurityManager.ts b/src/SecurityManager.ts index 214047c4fa..854457db50 100644 --- a/src/SecurityManager.ts +++ b/src/SecurityManager.ts @@ -14,7 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { ICryptoCallbacks, ISecretStorageKeyInfo } from 'matrix-js-sdk/src/matrix'; +import { ICryptoCallbacks } from 'matrix-js-sdk/src/matrix'; +import { ISecretStorageKeyInfo } from 'matrix-js-sdk/src/crypto/api'; import { MatrixClient } from 'matrix-js-sdk/src/client'; import Modal from './Modal'; import * as sdk from './index'; diff --git a/src/components/structures/auth/SetupEncryptionBody.tsx b/src/components/structures/auth/SetupEncryptionBody.tsx index 13790c2e47..c7ce74077b 100644 --- a/src/components/structures/auth/SetupEncryptionBody.tsx +++ b/src/components/structures/auth/SetupEncryptionBody.tsx @@ -21,7 +21,7 @@ import Modal from '../../../Modal'; import VerificationRequestDialog from '../../views/dialogs/VerificationRequestDialog'; import { SetupEncryptionStore, Phase } from '../../../stores/SetupEncryptionStore'; import { replaceableComponent } from "../../../utils/replaceableComponent"; -import { ISecretStorageKeyInfo } from 'matrix-js-sdk'; +import { ISecretStorageKeyInfo } from 'matrix-js-sdk/src/crypto/api'; import EncryptionPanel from "../../views/right_panel/EncryptionPanel"; import AccessibleButton from '../../views/elements/AccessibleButton'; import Spinner from '../../views/elements/Spinner'; diff --git a/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx b/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx index d614cc0956..90c640977c 100644 --- a/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx +++ b/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx @@ -17,7 +17,7 @@ limitations under the License. import { debounce } from "lodash"; import classNames from 'classnames'; import React, { ChangeEvent, FormEvent } from 'react'; -import { ISecretStorageKeyInfo } from "matrix-js-sdk/src"; +import { ISecretStorageKeyInfo } from "matrix-js-sdk/src/crypto/api"; import * as sdk from '../../../../index'; import { MatrixClientPeg } from '../../../../MatrixClientPeg'; diff --git a/src/customisations/Security.ts b/src/customisations/Security.ts index c2262e5f71..9083274aa5 100644 --- a/src/customisations/Security.ts +++ b/src/customisations/Security.ts @@ -16,7 +16,7 @@ limitations under the License. import { IMatrixClientCreds } from "../MatrixClientPeg"; import { Kind as SetupEncryptionKind } from "../toasts/SetupEncryptionToast"; -import { ISecretStorageKeyInfo } from 'matrix-js-sdk/src/matrix'; +import { ISecretStorageKeyInfo } from 'matrix-js-sdk/src/crypto/api'; /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ function examineLoginResponse( diff --git a/src/stores/SetupEncryptionStore.ts b/src/stores/SetupEncryptionStore.ts index e969c64853..7197374502 100644 --- a/src/stores/SetupEncryptionStore.ts +++ b/src/stores/SetupEncryptionStore.ts @@ -17,7 +17,7 @@ limitations under the License. import EventEmitter from 'events'; import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; import { IKeyBackupInfo } from "matrix-js-sdk/src/crypto/keybackup"; -import { ISecretStorageKeyInfo } from "matrix-js-sdk/src/matrix"; +import { ISecretStorageKeyInfo } from "matrix-js-sdk/src/crypto/api"; import { PHASE_DONE as VERIF_PHASE_DONE } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; import { MatrixClientPeg } from '../MatrixClientPeg'; From 64f32a9fc154cb274b2e8bb75ea64f77f87f7491 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 8 Jul 2021 13:12:57 +0200 Subject: [PATCH 193/221] Focus composer after reacting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/emojipicker/ReactionPicker.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/views/emojipicker/ReactionPicker.tsx b/src/components/views/emojipicker/ReactionPicker.tsx index d8f8b7f2ff..d5a2277ab2 100644 --- a/src/components/views/emojipicker/ReactionPicker.tsx +++ b/src/components/views/emojipicker/ReactionPicker.tsx @@ -22,6 +22,7 @@ import EmojiPicker from "./EmojiPicker"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import dis from "../../../dispatcher/dispatcher"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { Action } from '../../../dispatcher/actions'; interface IProps { mxEvent: MatrixEvent; @@ -93,6 +94,7 @@ class ReactionPicker extends React.Component { this.props.mxEvent.getRoomId(), myReactions[reaction], ); + dis.dispatch({ action: Action.FocusComposer }); // Tell the emoji picker not to bump this in the more frequently used list. return false; } else { @@ -104,6 +106,7 @@ class ReactionPicker extends React.Component { }, }); dis.dispatch({ action: "message_sent" }); + dis.dispatch({ action: Action.FocusComposer }); return true; } }; From 1cdae54880acd906581c8fd7864654528e35f4b0 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 8 Jul 2021 13:16:58 +0100 Subject: [PATCH 194/221] Fix text wrapping for Link Preview Widget --- res/css/views/rooms/_LinkPreviewWidget.scss | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/res/css/views/rooms/_LinkPreviewWidget.scss b/res/css/views/rooms/_LinkPreviewWidget.scss index d547b749f8..54b785a565 100644 --- a/res/css/views/rooms/_LinkPreviewWidget.scss +++ b/res/css/views/rooms/_LinkPreviewWidget.scss @@ -33,12 +33,17 @@ limitations under the License. .mx_LinkPreviewWidget_caption { margin-left: 15px; flex: 1 1 auto; + overflow-x: hidden; // cause it to wrap rather than clip } .mx_LinkPreviewWidget_title { display: inline; font-weight: bold; white-space: normal; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; } .mx_LinkPreviewWidget_siteName { @@ -49,6 +54,9 @@ limitations under the License. margin-top: 8px; white-space: normal; word-wrap: break-word; + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; } .mx_MatrixChat_useCompactLayout { From fc05395dbacdaa087d252e9c51e9efa9397a4060 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 8 Jul 2021 13:24:16 +0100 Subject: [PATCH 195/221] Don't close settings dialog when opening spaces feedback prompt --- src/components/views/spaces/SpaceSettingsGeneralTab.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/spaces/SpaceSettingsGeneralTab.tsx b/src/components/views/spaces/SpaceSettingsGeneralTab.tsx index 3afdc629e4..9f4e0ecea7 100644 --- a/src/components/views/spaces/SpaceSettingsGeneralTab.tsx +++ b/src/components/views/spaces/SpaceSettingsGeneralTab.tsx @@ -96,7 +96,7 @@ const SpaceSettingsGeneralTab = ({ matrixClient: cli, space, onFinished }: IProp { error &&
    { error }
    } - onFinished(false)} /> +
    Date: Thu, 8 Jul 2021 13:24:56 +0100 Subject: [PATCH 196/221] Fix Space Create menu not disabling the alias field when busy --- src/components/views/elements/RoomAliasField.tsx | 2 ++ src/components/views/spaces/SpaceCreateMenu.tsx | 1 + 2 files changed, 3 insertions(+) diff --git a/src/components/views/elements/RoomAliasField.tsx b/src/components/views/elements/RoomAliasField.tsx index d9e081341b..62de4dd2bb 100644 --- a/src/components/views/elements/RoomAliasField.tsx +++ b/src/components/views/elements/RoomAliasField.tsx @@ -27,6 +27,7 @@ interface IProps { value: string; label?: string; placeholder?: string; + disabled?: boolean; onChange?(value: string): void; } @@ -68,6 +69,7 @@ export default class RoomAliasField extends React.PureComponent onChange={this.onChange} value={this.props.value.substring(1, this.props.value.length - this.props.domain.length - 1)} maxLength={maxlength} + disabled={this.props.disabled} /> ); } diff --git a/src/components/views/spaces/SpaceCreateMenu.tsx b/src/components/views/spaces/SpaceCreateMenu.tsx index 4bb61d7ccb..5f16684fb8 100644 --- a/src/components/views/spaces/SpaceCreateMenu.tsx +++ b/src/components/views/spaces/SpaceCreateMenu.tsx @@ -220,6 +220,7 @@ const SpaceCreateMenu = ({ onFinished }) => { value={alias} placeholder={name ? nameToAlias(name, domain) : _t("e.g. my-space")} label={_t("Address")} + disabled={busy} /> : null } From 1c2bdf3cf433f5e8f2e602105807ffbfec6cea7f Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 8 Jul 2021 13:35:26 +0100 Subject: [PATCH 197/221] delint --- res/css/views/rooms/_LinkPreviewWidget.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/res/css/views/rooms/_LinkPreviewWidget.scss b/res/css/views/rooms/_LinkPreviewWidget.scss index 54b785a565..e1628e19a6 100644 --- a/res/css/views/rooms/_LinkPreviewWidget.scss +++ b/res/css/views/rooms/_LinkPreviewWidget.scss @@ -37,7 +37,6 @@ limitations under the License. } .mx_LinkPreviewWidget_title { - display: inline; font-weight: bold; white-space: normal; display: -webkit-box; From 8e7e4c9e8dcc3f62f5a1def5b64a439e76f4f483 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 8 Jul 2021 14:47:36 +0100 Subject: [PATCH 198/221] Convert MessageContextMenu to Typescript --- ...eContextMenu.js => MessageContextMenu.tsx} | 175 ++++++++++-------- .../views/elements/AccessibleButton.tsx | 6 +- 2 files changed, 96 insertions(+), 85 deletions(-) rename src/components/views/context_menus/{MessageContextMenu.js => MessageContextMenu.tsx} (73%) diff --git a/src/components/views/context_menus/MessageContextMenu.js b/src/components/views/context_menus/MessageContextMenu.tsx similarity index 73% rename from src/components/views/context_menus/MessageContextMenu.js rename to src/components/views/context_menus/MessageContextMenu.tsx index a2086451cd..7619b116d6 100644 --- a/src/components/views/context_menus/MessageContextMenu.js +++ b/src/components/views/context_menus/MessageContextMenu.tsx @@ -1,6 +1,6 @@ /* Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> -Copyright 2015, 2016, 2018, 2019, 2021 The Matrix.org Foundation C.I.C. +Copyright 2015 - 2021 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. @@ -16,12 +16,11 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; -import { EventStatus } from 'matrix-js-sdk/src/models/event'; +import { EventStatus, MatrixEvent } from 'matrix-js-sdk/src/models/event'; +import { EventType, RelationType } from "matrix-js-sdk/src/@types/event"; import { MatrixClientPeg } from '../../../MatrixClientPeg'; import dis from '../../../dispatcher/dispatcher'; -import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; import Modal from '../../../Modal'; import Resend from '../../../Resend'; @@ -29,53 +28,65 @@ import SettingsStore from '../../../settings/SettingsStore'; import { isUrlPermitted } from '../../../HtmlUtils'; import { isContentActionable } from '../../../utils/EventUtils'; import IconizedContextMenu, { IconizedContextMenuOption, IconizedContextMenuOptionList } from './IconizedContextMenu'; -import { EventType } from "matrix-js-sdk/src/@types/event"; import { replaceableComponent } from "../../../utils/replaceableComponent"; import { ReadPinsEventId } from "../right_panel/PinnedMessagesCard"; import ForwardDialog from "../dialogs/ForwardDialog"; import { Action } from "../../../dispatcher/actions"; +import ReportEventDialog from '../dialogs/ReportEventDialog'; +import ViewSource from '../../structures/ViewSource'; +import ConfirmRedactDialog from '../dialogs/ConfirmRedactDialog'; +import ErrorDialog from '../dialogs/ErrorDialog'; +import ShareDialog from '../dialogs/ShareDialog'; +import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks"; -export function canCancel(eventStatus) { +export function canCancel(eventStatus: EventStatus): boolean { return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT; } +interface IEventTileOps { + isWidgetHidden(): boolean; + unhideWidget(): void; +} + +interface IProps { + /* the MatrixEvent associated with the context menu */ + mxEvent: MatrixEvent; + /* an optional EventTileOps implementation that can be used to unhide preview widgets */ + eventTileOps?: IEventTileOps; + permalinkCreator?: RoomPermalinkCreator; + /* an optional function to be called when the user clicks collapse thread, if not provided hide button */ + collapseReplyThread?(): void; + /* callback called when the menu is dismissed */ + onFinished(): void; + /* if the menu is inside a dialog, we sometimes need to close that dialog after click (forwarding) */ + onCloseDialog?(): void; +} + +interface IState { + canRedact: boolean; + canPin: boolean; +} + @replaceableComponent("views.context_menus.MessageContextMenu") -export default class MessageContextMenu extends React.Component { - static propTypes = { - /* the MatrixEvent associated with the context menu */ - mxEvent: PropTypes.object.isRequired, - - /* an optional EventTileOps implementation that can be used to unhide preview widgets */ - eventTileOps: PropTypes.object, - - /* an optional function to be called when the user clicks collapse thread, if not provided hide button */ - collapseReplyThread: PropTypes.func, - - /* callback called when the menu is dismissed */ - onFinished: PropTypes.func, - - /* if the menu is inside a dialog, we sometimes need to close that dialog after click (forwarding) */ - onCloseDialog: PropTypes.func, - }; - +export default class MessageContextMenu extends React.Component { state = { canRedact: false, canPin: false, }; componentDidMount() { - MatrixClientPeg.get().on('RoomMember.powerLevel', this._checkPermissions); - this._checkPermissions(); + MatrixClientPeg.get().on('RoomMember.powerLevel', this.checkPermissions); + this.checkPermissions(); } componentWillUnmount() { const cli = MatrixClientPeg.get(); if (cli) { - cli.removeListener('RoomMember.powerLevel', this._checkPermissions); + cli.removeListener('RoomMember.powerLevel', this.checkPermissions); } } - _checkPermissions = () => { + private checkPermissions = (): void => { const cli = MatrixClientPeg.get(); const room = cli.getRoom(this.props.mxEvent.getRoomId()); @@ -93,7 +104,7 @@ export default class MessageContextMenu extends React.Component { this.setState({ canRedact, canPin }); }; - _isPinned() { + private isPinned(): boolean { const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId()); const pinnedEvent = room.currentState.getStateEvents(EventType.RoomPinnedEvents, ''); if (!pinnedEvent) return false; @@ -101,38 +112,35 @@ export default class MessageContextMenu extends React.Component { return content.pinned && Array.isArray(content.pinned) && content.pinned.includes(this.props.mxEvent.getId()); } - onResendReactionsClick = () => { - for (const reaction of this._getUnsentReactions()) { + private onResendReactionsClick = (): void => { + for (const reaction of this.getUnsentReactions()) { Resend.resend(reaction); } this.closeMenu(); }; - onReportEventClick = () => { - const ReportEventDialog = sdk.getComponent("dialogs.ReportEventDialog"); + private onReportEventClick = (): void => { Modal.createTrackedDialog('Report Event', '', ReportEventDialog, { mxEvent: this.props.mxEvent, }, 'mx_Dialog_reportEvent'); this.closeMenu(); }; - onViewSourceClick = () => { - const ViewSource = sdk.getComponent('structures.ViewSource'); + private onViewSourceClick = (): void => { Modal.createTrackedDialog('View Event Source', '', ViewSource, { mxEvent: this.props.mxEvent, }, 'mx_Dialog_viewsource'); this.closeMenu(); }; - onRedactClick = () => { - const ConfirmRedactDialog = sdk.getComponent("dialogs.ConfirmRedactDialog"); + private onRedactClick = (): void => { Modal.createTrackedDialog('Confirm Redact Dialog', '', ConfirmRedactDialog, { - onFinished: async (proceed, reason) => { + onFinished: async (proceed: boolean, reason?: string) => { if (!proceed) return; const cli = MatrixClientPeg.get(); try { - if (this.props.onCloseDialog) this.props.onCloseDialog(); + this.props.onCloseDialog?.(); await cli.redactEvent( this.props.mxEvent.getRoomId(), this.props.mxEvent.getId(), @@ -145,7 +153,6 @@ export default class MessageContextMenu extends React.Component { // (e.g. no errcode or statusCode) as in that case the redactions end up in the // detached queue and we show the room status bar to allow retry if (typeof code !== "undefined") { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); // display error message stating you couldn't delete this. Modal.createTrackedDialog('You cannot delete this message', '', ErrorDialog, { title: _t('Error'), @@ -158,7 +165,7 @@ export default class MessageContextMenu extends React.Component { this.closeMenu(); }; - onForwardClick = () => { + private onForwardClick = (): void => { Modal.createTrackedDialog('Forward Message', '', ForwardDialog, { matrixClient: MatrixClientPeg.get(), event: this.props.mxEvent, @@ -167,7 +174,7 @@ export default class MessageContextMenu extends React.Component { this.closeMenu(); }; - onPinClick = () => { + private onPinClick = (): void => { const cli = MatrixClientPeg.get(); const room = cli.getRoom(this.props.mxEvent.getRoomId()); const eventId = this.props.mxEvent.getId(); @@ -188,18 +195,16 @@ export default class MessageContextMenu extends React.Component { this.closeMenu(); }; - closeMenu = () => { - if (this.props.onFinished) this.props.onFinished(); + private closeMenu = (): void => { + this.props.onFinished(); }; - onUnhidePreviewClick = () => { - if (this.props.eventTileOps) { - this.props.eventTileOps.unhideWidget(); - } + private onUnhidePreviewClick = (): void => { + this.props.eventTileOps?.unhideWidget(); this.closeMenu(); }; - onQuoteClick = () => { + private onQuoteClick = (): void => { dis.dispatch({ action: Action.ComposerInsert, event: this.props.mxEvent, @@ -207,9 +212,8 @@ export default class MessageContextMenu extends React.Component { this.closeMenu(); }; - onPermalinkClick = (e) => { + private onPermalinkClick = (e: React.MouseEvent): void => { e.preventDefault(); - const ShareDialog = sdk.getComponent("dialogs.ShareDialog"); Modal.createTrackedDialog('share room message dialog', '', ShareDialog, { target: this.props.mxEvent, permalinkCreator: this.props.permalinkCreator, @@ -217,30 +221,27 @@ export default class MessageContextMenu extends React.Component { this.closeMenu(); }; - onCollapseReplyThreadClick = () => { + private onCollapseReplyThreadClick = (): void => { this.props.collapseReplyThread(); this.closeMenu(); }; - _getReactions(filter) { + private getReactions(filter: (e: MatrixEvent) => boolean): MatrixEvent[] { const cli = MatrixClientPeg.get(); const room = cli.getRoom(this.props.mxEvent.getRoomId()); const eventId = this.props.mxEvent.getId(); return room.getPendingEvents().filter(e => { const relation = e.getRelation(); - return relation && - relation.rel_type === "m.annotation" && - relation.event_id === eventId && - filter(e); + return relation?.rel_type === RelationType.Annotation && relation.event_id === eventId && filter(e); }); } - _getPendingReactions() { - return this._getReactions(e => canCancel(e.status)); + private getPendingReactions(): MatrixEvent[] { + return this.getReactions(e => canCancel(e.status)); } - _getUnsentReactions() { - return this._getReactions(e => e.status === EventStatus.NOT_SENT); + private getUnsentReactions(): MatrixEvent[] { + return this.getReactions(e => e.status === EventStatus.NOT_SENT); } render() { @@ -248,16 +249,17 @@ export default class MessageContextMenu extends React.Component { const me = cli.getUserId(); const mxEvent = this.props.mxEvent; const eventStatus = mxEvent.status; - const unsentReactionsCount = this._getUnsentReactions().length; - let resendReactionsButton; - let redactButton; - let forwardButton; - let pinButton; - let unhidePreviewButton; - let externalURLButton; - let quoteButton; - let collapseReplyThread; - let redactItemList; + const unsentReactionsCount = this.getUnsentReactions().length; + + let resendReactionsButton: JSX.Element; + let redactButton: JSX.Element; + let forwardButton: JSX.Element; + let pinButton: JSX.Element; + let unhidePreviewButton: JSX.Element; + let externalURLButton: JSX.Element; + let quoteButton: JSX.Element; + let collapseReplyThread: JSX.Element; + let redactItemList: JSX.Element; // status is SENT before remote-echo, null after const isSent = !eventStatus || eventStatus === EventStatus.SENT; @@ -296,7 +298,7 @@ export default class MessageContextMenu extends React.Component { pinButton = ( ); @@ -327,16 +329,20 @@ export default class MessageContextMenu extends React.Component { if (this.props.permalinkCreator) { permalink = this.props.permalinkCreator.forEvent(this.props.mxEvent.getId()); } - // XXX: if we use room ID, we should also include a server where the event can be found (other than in the domain of the event ID) const permalinkButton = ( ); @@ -351,8 +357,8 @@ export default class MessageContextMenu extends React.Component { } // Bridges can provide a 'external_url' to link back to the source. - if (typeof (mxEvent.event.content.external_url) === "string" && - isUrlPermitted(mxEvent.event.content.external_url) + if (typeof (mxEvent.getContent().external_url) === "string" && + isUrlPermitted(mxEvent.getContent().external_url) ) { externalURLButton = ( ); } @@ -377,7 +388,7 @@ export default class MessageContextMenu extends React.Component { ); } - let reportEventButton; + let reportEventButton: JSX.Element; if (mxEvent.getSender() !== me) { reportEventButton = ( | React.KeyboardEvent { inputRef?: React.Ref; - element?: string; + element?: keyof ReactHTML; // The kind of button, similar to how Bootstrap works. // See available classes for AccessibleButton for options. kind?: string; @@ -122,7 +122,7 @@ export default function AccessibleButton({ } AccessibleButton.defaultProps = { - element: 'div', + element: 'div' as keyof ReactHTML, role: 'button', tabIndex: 0, }; From f8907a6ea4fc8a4fb5ed9b6a6e5ac076a28eb3cc Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 8 Jul 2021 14:48:06 +0100 Subject: [PATCH 199/221] Fix bug which prevented more than one event getting pinned --- src/components/views/context_menus/MessageContextMenu.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/context_menus/MessageContextMenu.tsx b/src/components/views/context_menus/MessageContextMenu.tsx index 7619b116d6..999e98f4ad 100644 --- a/src/components/views/context_menus/MessageContextMenu.tsx +++ b/src/components/views/context_menus/MessageContextMenu.tsx @@ -179,7 +179,7 @@ export default class MessageContextMenu extends React.Component const room = cli.getRoom(this.props.mxEvent.getRoomId()); const eventId = this.props.mxEvent.getId(); - const pinnedIds = room?.currentState?.getStateEvents(EventType.RoomPinnedEvents, "")?.pinned || []; + const pinnedIds = room?.currentState?.getStateEvents(EventType.RoomPinnedEvents, "")?.getContent().pinned || []; if (pinnedIds.includes(eventId)) { pinnedIds.splice(pinnedIds.indexOf(eventId), 1); } else { From 7734f8aeefcbbc2d9186f6efa88a537d6040c253 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 8 Jul 2021 17:33:49 +0200 Subject: [PATCH 200/221] Handle focusing multiple composers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/structures/RoomView.tsx | 16 ++++++++++++++++ .../views/emojipicker/ReactionPicker.tsx | 4 ++-- .../views/rooms/EditMessageComposer.tsx | 2 ++ src/dispatcher/actions.ts | 14 +++++++++++++- 4 files changed, 33 insertions(+), 3 deletions(-) diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 0e77c301fd..0b345de49f 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -832,6 +832,22 @@ export default class RoomView extends React.Component { break; } + case Action.FocusAComposer: { + // re-dispatch to the correct composer + if (this.state.editState) { + dis.dispatch({ + ...payload, + action: Action.FocusEditMessageComposer, + }); + } else { + dis.dispatch({ + ...payload, + action: Action.FocusComposer, + }); + } + break; + } + case "scroll_to_bottom": this.messagePanel?.jumpToLiveTimeline(); break; diff --git a/src/components/views/emojipicker/ReactionPicker.tsx b/src/components/views/emojipicker/ReactionPicker.tsx index d5a2277ab2..e129b45c9a 100644 --- a/src/components/views/emojipicker/ReactionPicker.tsx +++ b/src/components/views/emojipicker/ReactionPicker.tsx @@ -94,7 +94,7 @@ class ReactionPicker extends React.Component { this.props.mxEvent.getRoomId(), myReactions[reaction], ); - dis.dispatch({ action: Action.FocusComposer }); + dis.dispatch({ action: Action.FocusAComposer }); // Tell the emoji picker not to bump this in the more frequently used list. return false; } else { @@ -106,7 +106,7 @@ class ReactionPicker extends React.Component { }, }); dis.dispatch({ action: "message_sent" }); - dis.dispatch({ action: Action.FocusComposer }); + dis.dispatch({ action: Action.FocusAComposer }); return true; } }; diff --git a/src/components/views/rooms/EditMessageComposer.tsx b/src/components/views/rooms/EditMessageComposer.tsx index fea6499dd8..3bfa121799 100644 --- a/src/components/views/rooms/EditMessageComposer.tsx +++ b/src/components/views/rooms/EditMessageComposer.tsx @@ -452,6 +452,8 @@ export default class EditMessageComposer extends React.Component } else if (payload.text) { this.editorRef.current?.insertPlaintext(payload.text); } + } else if (payload.action === Action.FocusEditMessageComposer && this.editorRef.current) { + this.editorRef.current.focus(); } }; diff --git a/src/dispatcher/actions.ts b/src/dispatcher/actions.ts index 6438ecc0f2..d376560ace 100644 --- a/src/dispatcher/actions.ts +++ b/src/dispatcher/actions.ts @@ -56,10 +56,22 @@ export enum Action { CheckUpdates = "check_updates", /** - * Focuses the user's cursor to the composer. No additional payload information required. + * Focuses the user's cursor to the send message composer. No additional payload information required. */ FocusComposer = "focus_composer", + /** + * Focuses the user's cursor to the edit message composer. No additional payload information required. + */ + FocusEditMessageComposer = "focus_edit_message_composer", + + /** + * Focuses the user's cursor to the edit message composer or send message + * composer based on the current edit state. No additional payload + * information required. + */ + FocusAComposer = "focus_a_composer", + /** * Opens the user menu (previously known as the top left menu). No additional payload information required. */ From 68d194444a907b5dafcc88f7271a48cdb9310485 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 8 Jul 2021 17:36:31 +0200 Subject: [PATCH 201/221] FocusComposer -> FocusSendMessageComposer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/ContentMessages.tsx | 2 +- src/components/structures/LoggedInView.tsx | 4 ++-- src/components/structures/MatrixChat.tsx | 4 ++-- src/components/structures/RoomSearch.tsx | 2 +- src/components/structures/RoomStatusBar.js | 4 ++-- src/components/structures/RoomView.tsx | 6 +++--- src/components/views/elements/ReplyThread.js | 2 +- src/components/views/rooms/EditMessageComposer.tsx | 6 +++--- src/components/views/rooms/SendMessageComposer.tsx | 2 +- src/dispatcher/actions.ts | 2 +- 10 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/ContentMessages.tsx b/src/ContentMessages.tsx index 670c175a48..0ab193081b 100644 --- a/src/ContentMessages.tsx +++ b/src/ContentMessages.tsx @@ -569,7 +569,7 @@ export default class ContentMessages { dis.dispatch({ action: Action.UploadStarted, upload }); // Focus the composer view - dis.fire(Action.FocusComposer); + dis.fire(Action.FocusSendMessageComposer); function onProgress(ev) { upload.total = ev.total; diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx index 5a26967cb0..89fa8db376 100644 --- a/src/components/structures/LoggedInView.tsx +++ b/src/components/structures/LoggedInView.tsx @@ -398,7 +398,7 @@ class LoggedInView extends React.Component { // refocusing during a paste event will make the // paste end up in the newly focused element, // so dispatch synchronously before paste happens - dis.fire(Action.FocusComposer, true); + dis.fire(Action.FocusSendMessageComposer, true); } }; @@ -552,7 +552,7 @@ class LoggedInView extends React.Component { if (!isClickShortcut && ev.key !== Key.TAB && !canElementReceiveInput(ev.target)) { // synchronous dispatch so we focus before key generates input - dis.fire(Action.FocusComposer, true); + dis.fire(Action.FocusSendMessageComposer, true); ev.stopPropagation(); // we should *not* preventDefault() here as // that would prevent typing in the now-focussed composer diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index c7a200239c..d692b0fa7f 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -443,7 +443,7 @@ export default class MatrixChat extends React.PureComponent { CountlyAnalytics.instance.trackPageChange(durationMs); } if (this.focusComposer) { - dis.fire(Action.FocusComposer); + dis.fire(Action.FocusSendMessageComposer); this.focusComposer = false; } } @@ -1427,7 +1427,7 @@ export default class MatrixChat extends React.PureComponent { showNotificationsToast(false); } - dis.fire(Action.FocusComposer); + dis.fire(Action.FocusSendMessageComposer); this.setState({ ready: true, }); diff --git a/src/components/structures/RoomSearch.tsx b/src/components/structures/RoomSearch.tsx index 9cdd1efe7e..e8080b4f7b 100644 --- a/src/components/structures/RoomSearch.tsx +++ b/src/components/structures/RoomSearch.tsx @@ -131,7 +131,7 @@ export default class RoomSearch extends React.PureComponent { switch (action) { case RoomListAction.ClearSearch: this.clearInput(); - defaultDispatcher.fire(Action.FocusComposer); + defaultDispatcher.fire(Action.FocusSendMessageComposer); break; case RoomListAction.NextRoom: case RoomListAction.PrevRoom: diff --git a/src/components/structures/RoomStatusBar.js b/src/components/structures/RoomStatusBar.js index f6e42a4f9c..80ea26c3f2 100644 --- a/src/components/structures/RoomStatusBar.js +++ b/src/components/structures/RoomStatusBar.js @@ -118,12 +118,12 @@ export default class RoomStatusBar extends React.PureComponent { this.setState({ isResending: false }); }); this.setState({ isResending: true }); - dis.fire(Action.FocusComposer); + dis.fire(Action.FocusSendMessageComposer); }; _onCancelAllClick = () => { Resend.cancelUnsentEvents(this.props.room); - dis.fire(Action.FocusComposer); + dis.fire(Action.FocusSendMessageComposer); }; _onRoomLocalEchoUpdated = (event, room, oldEventId, oldStatus) => { diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 0b345de49f..0f8d7189b7 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -842,7 +842,7 @@ export default class RoomView extends React.Component { } else { dis.dispatch({ ...payload, - action: Action.FocusComposer, + action: Action.FocusSendMessageComposer, }); } break; @@ -1262,7 +1262,7 @@ export default class RoomView extends React.Component { ContentMessages.sharedInstance().sendContentListToRoom( ev.dataTransfer.files, this.state.room.roomId, this.context, ); - dis.fire(Action.FocusComposer); + dis.fire(Action.FocusSendMessageComposer); this.setState({ draggingFile: false, @@ -1564,7 +1564,7 @@ export default class RoomView extends React.Component { } else { // Otherwise we have to jump manually this.messagePanel.jumpToLiveTimeline(); - dis.fire(Action.FocusComposer); + dis.fire(Action.FocusSendMessageComposer); } }; diff --git a/src/components/views/elements/ReplyThread.js b/src/components/views/elements/ReplyThread.js index aea447c9b1..2047de6c58 100644 --- a/src/components/views/elements/ReplyThread.js +++ b/src/components/views/elements/ReplyThread.js @@ -334,7 +334,7 @@ export default class ReplyThread extends React.Component { events, }); - dis.fire(Action.FocusComposer); + dis.fire(Action.FocusSendMessageComposer); } render() { diff --git a/src/components/views/rooms/EditMessageComposer.tsx b/src/components/views/rooms/EditMessageComposer.tsx index 3bfa121799..e4b13e2155 100644 --- a/src/components/views/rooms/EditMessageComposer.tsx +++ b/src/components/views/rooms/EditMessageComposer.tsx @@ -181,7 +181,7 @@ export default class EditMessageComposer extends React.Component } else { this.clearStoredEditorState(); dis.dispatch({ action: 'edit_event', event: null }); - dis.fire(Action.FocusComposer); + dis.fire(Action.FocusSendMessageComposer); } event.preventDefault(); break; @@ -200,7 +200,7 @@ export default class EditMessageComposer extends React.Component private cancelEdit = (): void => { this.clearStoredEditorState(); dis.dispatch({ action: "edit_event", event: null }); - dis.fire(Action.FocusComposer); + dis.fire(Action.FocusSendMessageComposer); }; private get shouldSaveStoredEditorState(): boolean { @@ -375,7 +375,7 @@ export default class EditMessageComposer extends React.Component // close the event editing and focus composer dis.dispatch({ action: "edit_event", event: null }); - dis.fire(Action.FocusComposer); + dis.fire(Action.FocusSendMessageComposer); }; private cancelPreviousPendingEdit(): void { diff --git a/src/components/views/rooms/SendMessageComposer.tsx b/src/components/views/rooms/SendMessageComposer.tsx index 2c45c1bbf8..0639c20fef 100644 --- a/src/components/views/rooms/SendMessageComposer.tsx +++ b/src/components/views/rooms/SendMessageComposer.tsx @@ -497,7 +497,7 @@ export default class SendMessageComposer extends React.Component { switch (payload.action) { case 'reply_to_event': - case Action.FocusComposer: + case Action.FocusSendMessageComposer: this.editorRef.current?.focus(); break; case "send_composer_insert": diff --git a/src/dispatcher/actions.ts b/src/dispatcher/actions.ts index d376560ace..26011063b7 100644 --- a/src/dispatcher/actions.ts +++ b/src/dispatcher/actions.ts @@ -58,7 +58,7 @@ export enum Action { /** * Focuses the user's cursor to the send message composer. No additional payload information required. */ - FocusComposer = "focus_composer", + FocusSendMessageComposer = "focus_composer", /** * Focuses the user's cursor to the edit message composer. No additional payload information required. From 6401577fe49ea2e81a26299cc96dc56a0af7a72f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 8 Jul 2021 17:37:39 +0200 Subject: [PATCH 202/221] focus_composer -> focus_send_message_composer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/dispatcher/actions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dispatcher/actions.ts b/src/dispatcher/actions.ts index 26011063b7..a4bfa171cd 100644 --- a/src/dispatcher/actions.ts +++ b/src/dispatcher/actions.ts @@ -58,7 +58,7 @@ export enum Action { /** * Focuses the user's cursor to the send message composer. No additional payload information required. */ - FocusSendMessageComposer = "focus_composer", + FocusSendMessageComposer = "focus_send_message_composer", /** * Focuses the user's cursor to the edit message composer. No additional payload information required. From 27db0da43738dfed89028044f0032d895c7dc28d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 8 Jul 2021 17:40:41 +0200 Subject: [PATCH 203/221] Simpler code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/structures/RoomView.tsx | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 0f8d7189b7..3ba14b32fb 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -834,17 +834,7 @@ export default class RoomView extends React.Component { case Action.FocusAComposer: { // re-dispatch to the correct composer - if (this.state.editState) { - dis.dispatch({ - ...payload, - action: Action.FocusEditMessageComposer, - }); - } else { - dis.dispatch({ - ...payload, - action: Action.FocusSendMessageComposer, - }); - } + dis.fire(this.state.editState ? Action.FocusEditMessageComposer : Action.FocusSendMessageComposer); break; } From 7a1381135aab49837215ead81f963682f5fdf61e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 8 Jul 2021 18:31:47 +0200 Subject: [PATCH 204/221] Simplifie some code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/structures/RoomView.tsx | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 3ba14b32fb..8e0b8a5f4a 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -818,17 +818,10 @@ export default class RoomView extends React.Component { case Action.ComposerInsert: { // re-dispatch to the correct composer - if (this.state.editState) { - dis.dispatch({ - ...payload, - action: "edit_composer_insert", - }); - } else { - dis.dispatch({ - ...payload, - action: "send_composer_insert", - }); - } + dis.dispatch({ + ...payload, + action: this.state.editState ? "edit_composer_insert" : "send_composer_insert", + }); break; } From 197b6aeefef7b3532f496372f34ce5024011b5b4 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 8 Jul 2021 21:16:19 +0100 Subject: [PATCH 205/221] Make DeviceListener also update on megolm key in SSSS The device listener checks for a megolm key stored in SSSS but didn't update when one was added, so the encryption upgrade toast would not disappear after the key was fixed by https://github.com/matrix-org/matrix-js-sdk/pull/1776 --- src/DeviceListener.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/DeviceListener.ts b/src/DeviceListener.ts index d70585e5ec..d033063677 100644 --- a/src/DeviceListener.ts +++ b/src/DeviceListener.ts @@ -160,7 +160,8 @@ export default class DeviceListener { // which result in account data changes affecting checks below. if ( ev.getType().startsWith('m.secret_storage.') || - ev.getType().startsWith('m.cross_signing.') + ev.getType().startsWith('m.cross_signing.') || + ev.getType() === 'm.megolm_backup.v1' ) { this._recheck(); } From 88a5969f2d0a790e52ce8be839889b055db4f509 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 9 Jul 2021 10:32:43 +0200 Subject: [PATCH 206/221] Remove a word Co-authored-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/directory/NetworkDropdown.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/directory/NetworkDropdown.tsx b/src/components/views/directory/NetworkDropdown.tsx index 155349e39d..0492168f36 100644 --- a/src/components/views/directory/NetworkDropdown.tsx +++ b/src/components/views/directory/NetworkDropdown.tsx @@ -171,7 +171,7 @@ const NetworkDropdown = ({ onOptionChange, protocols = {}, selectedServerName, s const protocolsList = server === hsName ? Object.values(protocols) : []; if (protocolsList.length > 0) { - // add a fake protocol with the ALL_ROOMS + // add a fake protocol with ALL_ROOMS protocolsList.push({ instances: [{ fields: [], From 84b00b5c388709aab1f7c0e3b45291fd7e0ec5a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 9 Jul 2021 10:39:02 +0200 Subject: [PATCH 207/221] Make the code more readable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/structures/RoomDirectory.tsx | 31 +++++++++++---------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/src/components/structures/RoomDirectory.tsx b/src/components/structures/RoomDirectory.tsx index 4a0e7615c4..4d335bf63c 100644 --- a/src/components/structures/RoomDirectory.tsx +++ b/src/components/structures/RoomDirectory.tsx @@ -122,25 +122,26 @@ export default class RoomDirectory extends React.Component { const myHomeserver = MatrixClientPeg.getHomeserverName(); const lsRoomServer = localStorage.getItem(LAST_SERVER_KEY); const lsInstanceId = localStorage.getItem(LAST_INSTANCE_KEY); - const configSevers = SdkConfig.get().roomDirectory?.servers || []; - const settingsServers = SettingsStore.getValue("room_directory_servers") || []; - const roomServer = [...configSevers, ...settingsServers].includes(lsRoomServer) - ? lsRoomServer - : myHomeserver; - const instanceIds = []; - if (roomServer === myHomeserver) { - Object.values(this.protocols).forEach((protocol) => { - protocol.instances.forEach((instance) => instanceIds.push(instance.instance_id)); - }); + const roomServer = ( + SdkConfig.get().roomDirectory?.servers?.includes(lsRoomServer) || + SettingsStore.getValue("room_directory_servers")?.includes(lsRoomServer) + ) ? lsRoomServer : myHomeserver; + + let instanceId: string = null; + if ( + (lsInstanceId === ALL_ROOMS) || + ( + roomServer === myHomeserver && + Object.values(this.protocols).some(p => p.instances.some(i => i.instance_id === lsInstanceId)) + ) + ) { + instanceId = lsInstanceId; } - const instanceId = (instanceIds.includes(lsInstanceId) || lsInstanceId === ALL_ROOMS) - ? lsInstanceId - : null; this.setState({ protocolsLoading: false, - instanceId: instanceId, - roomServer: roomServer, + instanceId, + roomServer, }); this.refreshRoomList(); }, (err) => { From 6dcf860181dc04bb93417a7d3687c505a1d0bedb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 9 Jul 2021 10:44:20 +0200 Subject: [PATCH 208/221] Refresh the room list only if validation failed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/structures/RoomDirectory.tsx | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/components/structures/RoomDirectory.tsx b/src/components/structures/RoomDirectory.tsx index 4d335bf63c..b36531236a 100644 --- a/src/components/structures/RoomDirectory.tsx +++ b/src/components/structures/RoomDirectory.tsx @@ -138,12 +138,17 @@ export default class RoomDirectory extends React.Component { instanceId = lsInstanceId; } - this.setState({ - protocolsLoading: false, - instanceId, - roomServer, - }); - this.refreshRoomList(); + // Refresh the room list only if validation failed and we had to change these + if (this.state.instanceId !== instanceId || this.state.roomServer !== roomServer) { + this.setState({ + protocolsLoading: false, + instanceId, + roomServer, + }); + this.refreshRoomList(); + return; + } + this.setState({ protocolsLoading: false }); }, (err) => { console.warn(`error loading third party protocols: ${err}`); this.setState({ protocolsLoading: false }); @@ -177,8 +182,8 @@ export default class RoomDirectory extends React.Component { publicRooms: [], loading: true, error: null, - instanceId: null, - roomServer: null, + instanceId: localStorage.getItem(LAST_INSTANCE_KEY), + roomServer: localStorage.getItem(LAST_SERVER_KEY), filterString: this.props.initialText || "", selectedCommunityId, communityName: null, From 046b3f325c79eca455c0780331e8b44a90d3bc28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 9 Jul 2021 12:31:44 +0200 Subject: [PATCH 209/221] Iterate PR MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/structures/RoomDirectory.tsx | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/components/structures/RoomDirectory.tsx b/src/components/structures/RoomDirectory.tsx index b36531236a..bd25a764a0 100644 --- a/src/components/structures/RoomDirectory.tsx +++ b/src/components/structures/RoomDirectory.tsx @@ -122,19 +122,20 @@ export default class RoomDirectory extends React.Component { const myHomeserver = MatrixClientPeg.getHomeserverName(); const lsRoomServer = localStorage.getItem(LAST_SERVER_KEY); const lsInstanceId = localStorage.getItem(LAST_INSTANCE_KEY); - const roomServer = ( + + let roomServer = myHomeserver; + if ( SdkConfig.get().roomDirectory?.servers?.includes(lsRoomServer) || SettingsStore.getValue("room_directory_servers")?.includes(lsRoomServer) - ) ? lsRoomServer : myHomeserver; + ) { + roomServer = lsRoomServer; + } let instanceId: string = null; - if ( - (lsInstanceId === ALL_ROOMS) || - ( - roomServer === myHomeserver && - Object.values(this.protocols).some(p => p.instances.some(i => i.instance_id === lsInstanceId)) - ) - ) { + if (roomServer === myHomeserver && ( + lsInstanceId === ALL_ROOMS || + Object.values(this.protocols).some(p => p.instances.some(i => i.instance_id === lsInstanceId)) + )) { instanceId = lsInstanceId; } From 318a68e761623f7e75f9df2f246591c5e4c3f5a6 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 9 Jul 2021 11:49:05 +0100 Subject: [PATCH 210/221] Update Modernizr and stop it from polluting classes on the html tag --- src/@types/global.d.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index d257ee4c5c..759cc306f5 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -15,7 +15,7 @@ limitations under the License. */ import "matrix-js-sdk/src/@types/global"; // load matrix-js-sdk's type extensions first -import * as ModernizrStatic from "modernizr"; +import "@types/modernizr"; import ContentMessages from "../ContentMessages"; import { IMatrixClientPeg } from "../MatrixClientPeg"; @@ -50,7 +50,6 @@ import { RoomScrollStateStore } from "../stores/RoomScrollStateStore"; declare global { interface Window { - Modernizr: ModernizrStatic; matrixChat: ReturnType; mxMatrixClientPeg: IMatrixClientPeg; Olm: { From 866c1b76bd842e31ec1f040bd88ad5c8629a1772 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 9 Jul 2021 12:57:42 +0200 Subject: [PATCH 211/221] Basic TS conversion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- ...ettingsTab.js => VoiceUserSettingsTab.tsx} | 55 +++++++++++-------- 1 file changed, 31 insertions(+), 24 deletions(-) rename src/components/views/settings/tabs/user/{VoiceUserSettingsTab.js => VoiceUserSettingsTab.tsx} (84%) diff --git a/src/components/views/settings/tabs/user/VoiceUserSettingsTab.js b/src/components/views/settings/tabs/user/VoiceUserSettingsTab.tsx similarity index 84% rename from src/components/views/settings/tabs/user/VoiceUserSettingsTab.js rename to src/components/views/settings/tabs/user/VoiceUserSettingsTab.tsx index fe6261cb21..bce047665d 100644 --- a/src/components/views/settings/tabs/user/VoiceUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/VoiceUserSettingsTab.tsx @@ -18,7 +18,7 @@ limitations under the License. import React from 'react'; import { _t } from "../../../../../languageHandler"; import SdkConfig from "../../../../../SdkConfig"; -import MediaDeviceHandler from "../../../../../MediaDeviceHandler"; +import MediaDeviceHandler, { IMediaDevices } from "../../../../../MediaDeviceHandler"; import Field from "../../../elements/Field"; import AccessibleButton from "../../../elements/AccessibleButton"; import { MatrixClientPeg } from "../../../../../MatrixClientPeg"; @@ -27,13 +27,20 @@ import Modal from "../../../../../Modal"; import { SettingLevel } from "../../../../../settings/SettingLevel"; import { replaceableComponent } from "../../../../../utils/replaceableComponent"; +interface IState { + mediaDevices: IMediaDevices; + activeAudioOutput: string; + activeAudioInput: string; + activeVideoInput: string; +} + @replaceableComponent("views.settings.tabs.user.VoiceUserSettingsTab") -export default class VoiceUserSettingsTab extends React.Component { +export default class VoiceUserSettingsTab extends React.Component<{}, IState> { constructor() { - super(); + super({}); this.state = { - mediaDevices: false, + mediaDevices: null, activeAudioOutput: null, activeAudioInput: null, activeVideoInput: null, @@ -43,11 +50,11 @@ export default class VoiceUserSettingsTab extends React.Component { async componentDidMount() { const canSeeDeviceLabels = await MediaDeviceHandler.hasAnyLabeledDevices(); if (canSeeDeviceLabels) { - this._refreshMediaDevices(); + this.refreshMediaDevices(); } } - _refreshMediaDevices = async (stream) => { + private refreshMediaDevices = async (stream?: MediaStream) => { this.setState({ mediaDevices: await MediaDeviceHandler.getDevices(), activeAudioOutput: MediaDeviceHandler.getAudioOutput(), @@ -62,7 +69,7 @@ export default class VoiceUserSettingsTab extends React.Component { } }; - _requestMediaPermissions = async () => { + private requestMediaPermissions = async () => { let constraints; let stream; let error; @@ -95,40 +102,40 @@ export default class VoiceUserSettingsTab extends React.Component { ), }); } else { - this._refreshMediaDevices(stream); + this.refreshMediaDevices(stream); } }; - _setAudioOutput = (e) => { + private setAudioOutput = (e) => { MediaDeviceHandler.instance.setAudioOutput(e.target.value); this.setState({ activeAudioOutput: e.target.value, }); }; - _setAudioInput = (e) => { + private setAudioInput = (e) => { MediaDeviceHandler.instance.setAudioInput(e.target.value); this.setState({ activeAudioInput: e.target.value, }); }; - _setVideoInput = (e) => { + private setVideoInput = (e) => { MediaDeviceHandler.instance.setVideoInput(e.target.value); this.setState({ activeVideoInput: e.target.value, }); }; - _changeWebRtcMethod = (p2p) => { + private changeWebRtcMethod = (p2p) => { MatrixClientPeg.get().setForceTURN(!p2p); }; - _changeFallbackICEServerAllowed = (allow) => { + private changeFallbackICEServerAllowed = (allow) => { MatrixClientPeg.get().setFallbackICEServerAllowed(allow); }; - _renderDeviceOptions(devices, category) { + private renderDeviceOptions(devices, category) { return devices.map((d) => { return (); }); @@ -141,11 +148,11 @@ export default class VoiceUserSettingsTab extends React.Component { let speakerDropdown = null; let microphoneDropdown = null; let webcamDropdown = null; - if (this.state.mediaDevices === false) { + if (!this.state.mediaDevices) { requestButton = (

    {_t("Missing media permissions, click the button below to request.")}

    - + {_t("Request media permissions")}
    @@ -177,8 +184,8 @@ export default class VoiceUserSettingsTab extends React.Component { speakerDropdown = ( - {this._renderDeviceOptions(audioOutputs, 'audioOutput')} + onChange={this.setAudioOutput}> + {this.renderDeviceOptions(audioOutputs, 'audioOutput')} ); } @@ -189,8 +196,8 @@ export default class VoiceUserSettingsTab extends React.Component { microphoneDropdown = ( - {this._renderDeviceOptions(audioInputs, 'audioInput')} + onChange={this.setAudioInput}> + {this.renderDeviceOptions(audioInputs, 'audioInput')} ); } @@ -201,8 +208,8 @@ export default class VoiceUserSettingsTab extends React.Component { webcamDropdown = ( - {this._renderDeviceOptions(videoInputs, 'videoInput')} + onChange={this.setVideoInput}> + {this.renderDeviceOptions(videoInputs, 'videoInput')} ); } @@ -220,12 +227,12 @@ export default class VoiceUserSettingsTab extends React.Component {
    From dadfba90753607508872619679025ee63a76b53f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 9 Jul 2021 13:08:39 +0200 Subject: [PATCH 212/221] Add MediaDeviceKindEnum MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/MediaDeviceHandler.ts | 31 +++++++++---------- .../views/rooms/VoiceRecordComposerTile.tsx | 4 +-- .../tabs/user/VoiceUserSettingsTab.tsx | 8 ++--- 3 files changed, 20 insertions(+), 23 deletions(-) diff --git a/src/MediaDeviceHandler.ts b/src/MediaDeviceHandler.ts index 49ef123def..bc0291a623 100644 --- a/src/MediaDeviceHandler.ts +++ b/src/MediaDeviceHandler.ts @@ -20,12 +20,15 @@ import { SettingLevel } from "./settings/SettingLevel"; import { setMatrixCallAudioInput, setMatrixCallVideoInput } from "matrix-js-sdk/src/matrix"; import EventEmitter from 'events'; -interface IMediaDevices { - audioOutput: Array; - audioInput: Array; - videoInput: Array; +// XXX: MediaDeviceKind is a union type, so we make our own enum +export enum MediaDeviceKindEnum { + AudioOutput = "audiooutput", + AudioInput = "audioinput", + VideoInput = "videoinput", } +export type IMediaDevices = Record>; + export enum MediaDeviceHandlerEvent { AudioOutputChanged = "audio_output_changed", } @@ -51,20 +54,14 @@ export default class MediaDeviceHandler extends EventEmitter { try { const devices = await navigator.mediaDevices.enumerateDevices(); + const output = { + [MediaDeviceKindEnum.AudioOutput]: [], + [MediaDeviceKindEnum.AudioInput]: [], + [MediaDeviceKindEnum.VideoInput]: [], + }; - const audioOutput = []; - const audioInput = []; - const videoInput = []; - - devices.forEach((device) => { - switch (device.kind) { - case 'audiooutput': audioOutput.push(device); break; - case 'audioinput': audioInput.push(device); break; - case 'videoinput': videoInput.push(device); break; - } - }); - - return { audioOutput, audioInput, videoInput }; + devices.forEach((device) => output[device.kind].push(device)); + return output; } catch (error) { console.warn('Unable to refresh WebRTC Devices: ', error); } diff --git a/src/components/views/rooms/VoiceRecordComposerTile.tsx b/src/components/views/rooms/VoiceRecordComposerTile.tsx index f08c8fe6df..5d984eacfa 100644 --- a/src/components/views/rooms/VoiceRecordComposerTile.tsx +++ b/src/components/views/rooms/VoiceRecordComposerTile.tsx @@ -33,7 +33,7 @@ import RecordingPlayback from "../audio_messages/RecordingPlayback"; import { MsgType } from "matrix-js-sdk/src/@types/event"; import Modal from "../../../Modal"; import ErrorDialog from "../dialogs/ErrorDialog"; -import MediaDeviceHandler from "../../../MediaDeviceHandler"; +import MediaDeviceHandler, { MediaDeviceKindEnum } from "../../../MediaDeviceHandler"; interface IProps { room: Room; @@ -135,7 +135,7 @@ export default class VoiceRecordComposerTile extends React.PureComponent diff --git a/src/components/views/settings/tabs/user/VoiceUserSettingsTab.tsx b/src/components/views/settings/tabs/user/VoiceUserSettingsTab.tsx index bce047665d..f5adc05d6b 100644 --- a/src/components/views/settings/tabs/user/VoiceUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/VoiceUserSettingsTab.tsx @@ -18,7 +18,7 @@ limitations under the License. import React from 'react'; import { _t } from "../../../../../languageHandler"; import SdkConfig from "../../../../../SdkConfig"; -import MediaDeviceHandler, { IMediaDevices } from "../../../../../MediaDeviceHandler"; +import MediaDeviceHandler, { IMediaDevices, MediaDeviceKindEnum } from "../../../../../MediaDeviceHandler"; import Field from "../../../elements/Field"; import AccessibleButton from "../../../elements/AccessibleButton"; import { MatrixClientPeg } from "../../../../../MatrixClientPeg"; @@ -178,7 +178,7 @@ export default class VoiceUserSettingsTab extends React.Component<{}, IState> { } }; - const audioOutputs = this.state.mediaDevices.audioOutput.slice(0); + const audioOutputs = this.state.mediaDevices[MediaDeviceKindEnum.AudioOutput].slice(0); if (audioOutputs.length > 0) { const defaultDevice = getDefaultDevice(audioOutputs); speakerDropdown = ( @@ -190,7 +190,7 @@ export default class VoiceUserSettingsTab extends React.Component<{}, IState> { ); } - const audioInputs = this.state.mediaDevices.audioInput.slice(0); + const audioInputs = this.state.mediaDevices[MediaDeviceKindEnum.AudioInput].slice(0); if (audioInputs.length > 0) { const defaultDevice = getDefaultDevice(audioInputs); microphoneDropdown = ( @@ -202,7 +202,7 @@ export default class VoiceUserSettingsTab extends React.Component<{}, IState> { ); } - const videoInputs = this.state.mediaDevices.videoInput.slice(0); + const videoInputs = this.state.mediaDevices[MediaDeviceKindEnum.VideoInput].slice(0); if (videoInputs.length > 0) { const defaultDevice = getDefaultDevice(videoInputs); webcamDropdown = ( From cd95be147c87afd5136db324313052c1efe7ed15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 9 Jul 2021 13:45:39 +0200 Subject: [PATCH 213/221] Finish TS conversion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- .../tabs/user/VoiceUserSettingsTab.tsx | 57 +++++++++---------- 1 file changed, 26 insertions(+), 31 deletions(-) diff --git a/src/components/views/settings/tabs/user/VoiceUserSettingsTab.tsx b/src/components/views/settings/tabs/user/VoiceUserSettingsTab.tsx index f5adc05d6b..02ab96667c 100644 --- a/src/components/views/settings/tabs/user/VoiceUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/VoiceUserSettingsTab.tsx @@ -22,16 +22,14 @@ import MediaDeviceHandler, { IMediaDevices, MediaDeviceKindEnum } from "../../.. import Field from "../../../elements/Field"; import AccessibleButton from "../../../elements/AccessibleButton"; import { MatrixClientPeg } from "../../../../../MatrixClientPeg"; -import * as sdk from "../../../../../index"; import Modal from "../../../../../Modal"; import { SettingLevel } from "../../../../../settings/SettingLevel"; import { replaceableComponent } from "../../../../../utils/replaceableComponent"; +import SettingsFlag from '../../../elements/SettingsFlag'; +import ErrorDialog from '../../../dialogs/ErrorDialog'; -interface IState { +interface IState extends Record { mediaDevices: IMediaDevices; - activeAudioOutput: string; - activeAudioInput: string; - activeVideoInput: string; } @replaceableComponent("views.settings.tabs.user.VoiceUserSettingsTab") @@ -41,9 +39,9 @@ export default class VoiceUserSettingsTab extends React.Component<{}, IState> { this.state = { mediaDevices: null, - activeAudioOutput: null, - activeAudioInput: null, - activeVideoInput: null, + [MediaDeviceKindEnum.AudioOutput]: null, + [MediaDeviceKindEnum.AudioInput]: null, + [MediaDeviceKindEnum.VideoInput]: null, }; } @@ -54,12 +52,12 @@ export default class VoiceUserSettingsTab extends React.Component<{}, IState> { } } - private refreshMediaDevices = async (stream?: MediaStream) => { + private refreshMediaDevices = async (stream?: MediaStream): Promise => { this.setState({ mediaDevices: await MediaDeviceHandler.getDevices(), - activeAudioOutput: MediaDeviceHandler.getAudioOutput(), - activeAudioInput: MediaDeviceHandler.getAudioInput(), - activeVideoInput: MediaDeviceHandler.getVideoInput(), + [MediaDeviceKindEnum.AudioOutput]: MediaDeviceHandler.getAudioOutput(), + [MediaDeviceKindEnum.AudioInput]: MediaDeviceHandler.getAudioInput(), + [MediaDeviceKindEnum.VideoInput]: MediaDeviceHandler.getVideoInput(), }); if (stream) { // kill stream (after we've enumerated the devices, otherwise we'd get empty labels again) @@ -69,7 +67,7 @@ export default class VoiceUserSettingsTab extends React.Component<{}, IState> { } }; - private requestMediaPermissions = async () => { + private requestMediaPermissions = async (): Promise => { let constraints; let stream; let error; @@ -93,7 +91,6 @@ export default class VoiceUserSettingsTab extends React.Component<{}, IState> { if (error) { console.log("Failed to list userMedia devices", error); const brand = SdkConfig.get().brand; - const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog'); Modal.createTrackedDialog('No media permissions', '', ErrorDialog, { title: _t('No media permissions'), description: _t( @@ -106,44 +103,42 @@ export default class VoiceUserSettingsTab extends React.Component<{}, IState> { } }; - private setAudioOutput = (e) => { + private setAudioOutput = (e): void => { MediaDeviceHandler.instance.setAudioOutput(e.target.value); this.setState({ - activeAudioOutput: e.target.value, + [MediaDeviceKindEnum.AudioOutput]: e.target.value, }); }; - private setAudioInput = (e) => { + private setAudioInput = (e): void => { MediaDeviceHandler.instance.setAudioInput(e.target.value); this.setState({ - activeAudioInput: e.target.value, + [MediaDeviceKindEnum.AudioInput]: e.target.value, }); }; - private setVideoInput = (e) => { + private setVideoInput = (e): void => { MediaDeviceHandler.instance.setVideoInput(e.target.value); this.setState({ - activeVideoInput: e.target.value, + [MediaDeviceKindEnum.VideoInput]: e.target.value, }); }; - private changeWebRtcMethod = (p2p) => { + private changeWebRtcMethod = (p2p: boolean): void => { MatrixClientPeg.get().setForceTURN(!p2p); }; - private changeFallbackICEServerAllowed = (allow) => { + private changeFallbackICEServerAllowed = (allow: boolean): void => { MatrixClientPeg.get().setFallbackICEServerAllowed(allow); }; - private renderDeviceOptions(devices, category) { + private renderDeviceOptions(devices: Array, category: MediaDeviceKindEnum): Array { return devices.map((d) => { return (); }); } render() { - const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag"); - let requestButton = null; let speakerDropdown = null; let microphoneDropdown = null; @@ -183,9 +178,9 @@ export default class VoiceUserSettingsTab extends React.Component<{}, IState> { const defaultDevice = getDefaultDevice(audioOutputs); speakerDropdown = ( - {this.renderDeviceOptions(audioOutputs, 'audioOutput')} + {this.renderDeviceOptions(audioOutputs, MediaDeviceKindEnum.AudioOutput)} ); } @@ -195,9 +190,9 @@ export default class VoiceUserSettingsTab extends React.Component<{}, IState> { const defaultDevice = getDefaultDevice(audioInputs); microphoneDropdown = ( - {this.renderDeviceOptions(audioInputs, 'audioInput')} + {this.renderDeviceOptions(audioInputs, MediaDeviceKindEnum.AudioInput)} ); } @@ -207,9 +202,9 @@ export default class VoiceUserSettingsTab extends React.Component<{}, IState> { const defaultDevice = getDefaultDevice(videoInputs); webcamDropdown = ( - {this.renderDeviceOptions(videoInputs, 'videoInput')} + {this.renderDeviceOptions(videoInputs, MediaDeviceKindEnum.VideoInput)} ); } From 1b209a9cb32a2505696cc1fed782f71740736769 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 9 Jul 2021 14:13:31 +0200 Subject: [PATCH 214/221] Add setDevice() method MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/MediaDeviceHandler.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/MediaDeviceHandler.ts b/src/MediaDeviceHandler.ts index bc0291a623..073f24523d 100644 --- a/src/MediaDeviceHandler.ts +++ b/src/MediaDeviceHandler.ts @@ -103,6 +103,14 @@ export default class MediaDeviceHandler extends EventEmitter { setMatrixCallVideoInput(deviceId); } + public setDevice(deviceId: string, kind: MediaDeviceKindEnum): void { + switch (kind) { + case MediaDeviceKindEnum.AudioOutput: this.setAudioOutput(deviceId); break; + case MediaDeviceKindEnum.AudioInput: this.setAudioInput(deviceId); break; + case MediaDeviceKindEnum.VideoInput: this.setVideoInput(deviceId); break; + } + } + public static getAudioOutput(): string { return SettingsStore.getValueAt(SettingLevel.DEVICE, "webrtc_audiooutput"); } From dbc37675a02bd23dd5e0e1f7d2b7bd72e2dce425 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 9 Jul 2021 14:15:36 +0200 Subject: [PATCH 215/221] Simplifie the code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- .../tabs/user/VoiceUserSettingsTab.tsx | 133 +++++++----------- 1 file changed, 53 insertions(+), 80 deletions(-) diff --git a/src/components/views/settings/tabs/user/VoiceUserSettingsTab.tsx b/src/components/views/settings/tabs/user/VoiceUserSettingsTab.tsx index 02ab96667c..3fd4455271 100644 --- a/src/components/views/settings/tabs/user/VoiceUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/VoiceUserSettingsTab.tsx @@ -28,14 +28,29 @@ import { replaceableComponent } from "../../../../../utils/replaceableComponent" import SettingsFlag from '../../../elements/SettingsFlag'; import ErrorDialog from '../../../dialogs/ErrorDialog'; +const getDefaultDevice = (devices: Array>) => { + // Note we're looking for a device with deviceId 'default' but adding a device + // with deviceId == the empty string: this is because Chrome gives us a device + // with deviceId 'default', so we're looking for this, not the one we are adding. + if (!devices.some((i) => i.deviceId === 'default')) { + devices.unshift({ + deviceId: '', + label: _t('Default Device'), + }); + return ''; + } else { + return 'default'; + } +}; + interface IState extends Record { mediaDevices: IMediaDevices; } @replaceableComponent("views.settings.tabs.user.VoiceUserSettingsTab") export default class VoiceUserSettingsTab extends React.Component<{}, IState> { - constructor() { - super({}); + constructor(props: {}) { + super(props); this.state = { mediaDevices: null, @@ -103,25 +118,9 @@ export default class VoiceUserSettingsTab extends React.Component<{}, IState> { } }; - private setAudioOutput = (e): void => { - MediaDeviceHandler.instance.setAudioOutput(e.target.value); - this.setState({ - [MediaDeviceKindEnum.AudioOutput]: e.target.value, - }); - }; - - private setAudioInput = (e): void => { - MediaDeviceHandler.instance.setAudioInput(e.target.value); - this.setState({ - [MediaDeviceKindEnum.AudioInput]: e.target.value, - }); - }; - - private setVideoInput = (e): void => { - MediaDeviceHandler.instance.setVideoInput(e.target.value); - this.setState({ - [MediaDeviceKindEnum.VideoInput]: e.target.value, - }); + private setDevice = (deviceId: string, kind: MediaDeviceKindEnum): void => { + MediaDeviceHandler.instance.setDevice(deviceId, kind); + this.setState({ [kind]: deviceId }); }; private changeWebRtcMethod = (p2p: boolean): void => { @@ -138,6 +137,23 @@ export default class VoiceUserSettingsTab extends React.Component<{}, IState> { }); } + private renderDropdown(kind: MediaDeviceKindEnum, label: string): JSX.Element { + const devices = this.state.mediaDevices[kind].slice(0); + if (devices.length === 0) return null; + + const defaultDevice = getDefaultDevice(devices); + return ( + this.setDevice(e.target.value, kind)} + > + { this.renderDeviceOptions(devices, kind) } + + ); + } + render() { let requestButton = null; let speakerDropdown = null; @@ -153,71 +169,28 @@ export default class VoiceUserSettingsTab extends React.Component<{}, IState> {
    ); } else if (this.state.mediaDevices) { - speakerDropdown =

    { _t('No Audio Outputs detected') }

    ; - microphoneDropdown =

    { _t('No Microphones detected') }

    ; - webcamDropdown =

    { _t('No Webcams detected') }

    ; - - const defaultOption = { - deviceId: '', - label: _t('Default Device'), - }; - const getDefaultDevice = (devices) => { - // Note we're looking for a device with deviceId 'default' but adding a device - // with deviceId == the empty string: this is because Chrome gives us a device - // with deviceId 'default', so we're looking for this, not the one we are adding. - if (!devices.some((i) => i.deviceId === 'default')) { - devices.unshift(defaultOption); - return ''; - } else { - return 'default'; - } - }; - - const audioOutputs = this.state.mediaDevices[MediaDeviceKindEnum.AudioOutput].slice(0); - if (audioOutputs.length > 0) { - const defaultDevice = getDefaultDevice(audioOutputs); - speakerDropdown = ( - - {this.renderDeviceOptions(audioOutputs, MediaDeviceKindEnum.AudioOutput)} - - ); - } - - const audioInputs = this.state.mediaDevices[MediaDeviceKindEnum.AudioInput].slice(0); - if (audioInputs.length > 0) { - const defaultDevice = getDefaultDevice(audioInputs); - microphoneDropdown = ( - - {this.renderDeviceOptions(audioInputs, MediaDeviceKindEnum.AudioInput)} - - ); - } - - const videoInputs = this.state.mediaDevices[MediaDeviceKindEnum.VideoInput].slice(0); - if (videoInputs.length > 0) { - const defaultDevice = getDefaultDevice(videoInputs); - webcamDropdown = ( - - {this.renderDeviceOptions(videoInputs, MediaDeviceKindEnum.VideoInput)} - - ); - } + speakerDropdown = ( + this.renderDropdown(MediaDeviceKindEnum.AudioOutput, _t("Audio Output")) || +

    { _t('No Audio Outputs detected') }

    + ); + microphoneDropdown = ( + this.renderDropdown(MediaDeviceKindEnum.AudioInput, _t("Microphone")) || +

    { _t('No Microphones detected') }

    + ); + webcamDropdown = ( + this.renderDropdown(MediaDeviceKindEnum.VideoInput, _t("Camera")) || +

    { _t('No Webcams detected') }

    + ); } return (
    {_t("Voice & Video")}
    - {requestButton} - {speakerDropdown} - {microphoneDropdown} - {webcamDropdown} + { requestButton } + { speakerDropdown } + { microphoneDropdown } + { webcamDropdown } Date: Fri, 9 Jul 2021 14:19:27 +0200 Subject: [PATCH 216/221] Fix styling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- .../views/settings/tabs/user/VoiceUserSettingsTab.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/components/views/settings/tabs/user/VoiceUserSettingsTab.tsx b/src/components/views/settings/tabs/user/VoiceUserSettingsTab.tsx index 3fd4455271..86c32cc6cd 100644 --- a/src/components/views/settings/tabs/user/VoiceUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/VoiceUserSettingsTab.tsx @@ -33,10 +33,7 @@ const getDefaultDevice = (devices: Array>) => { // with deviceId == the empty string: this is because Chrome gives us a device // with deviceId 'default', so we're looking for this, not the one we are adding. if (!devices.some((i) => i.deviceId === 'default')) { - devices.unshift({ - deviceId: '', - label: _t('Default Device'), - }); + devices.unshift({ deviceId: '', label: _t('Default Device') }); return ''; } else { return 'default'; From cdeb0be84778ff790873f619a748274c8745bdb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 9 Jul 2021 14:22:41 +0200 Subject: [PATCH 217/221] i18n MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/i18n/strings/en_EN.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 7d4252545b..7795bb2610 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1364,17 +1364,17 @@ "Where you’re logged in": "Where you’re logged in", "Manage the names of and sign out of your sessions below or verify them in your User Profile.": "Manage the names of and sign out of your sessions below or verify them in your User Profile.", "A session's public name is visible to people you communicate with": "A session's public name is visible to people you communicate with", + "Default Device": "Default Device", "No media permissions": "No media permissions", "You may need to manually permit %(brand)s to access your microphone/webcam": "You may need to manually permit %(brand)s to access your microphone/webcam", "Missing media permissions, click the button below to request.": "Missing media permissions, click the button below to request.", "Request media permissions": "Request media permissions", - "No Audio Outputs detected": "No Audio Outputs detected", - "No Microphones detected": "No Microphones detected", - "No Webcams detected": "No Webcams detected", - "Default Device": "Default Device", "Audio Output": "Audio Output", + "No Audio Outputs detected": "No Audio Outputs detected", "Microphone": "Microphone", + "No Microphones detected": "No Microphones detected", "Camera": "Camera", + "No Webcams detected": "No Webcams detected", "Voice & Video": "Voice & Video", "This room is not accessible by remote Matrix servers": "This room is not accessible by remote Matrix servers", "Warning: Upgrading a room will not automatically migrate room members to the new version of the room. We'll post a link to the new room in the old version of the room - room members will have to click this link to join the new room.": "Warning: Upgrading a room will not automatically migrate room members to the new version of the room. We'll post a link to the new room in the old version of the room - room members will have to click this link to join the new room.", From ba3d7f9beeb1e0cdaf5a070a6a8b4d10c0641333 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 9 Jul 2021 15:50:52 +0200 Subject: [PATCH 218/221] Use marked execution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/voip/CallPreview.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/components/views/voip/CallPreview.tsx b/src/components/views/voip/CallPreview.tsx index 1257d6760a..ddcb9057ec 100644 --- a/src/components/views/voip/CallPreview.tsx +++ b/src/components/views/voip/CallPreview.tsx @@ -29,6 +29,7 @@ import { MatrixClientPeg } from '../../../MatrixClientPeg'; import { replaceableComponent } from "../../../utils/replaceableComponent"; import UIStore from '../../../stores/UIStore'; import { lerp } from '../../../utils/AnimationUtils'; +import { MarkedExecution } from '../../../utils/MarkedExecution'; const PIP_VIEW_WIDTH = 336; const PIP_VIEW_HEIGHT = 232; @@ -116,6 +117,10 @@ export default class CallPreview extends React.Component { private desiredTranslationX = UIStore.instance.windowWidth - PADDING.right - PIP_VIEW_WIDTH; private desiredTranslationY = UIStore.instance.windowHeight - PADDING.bottom - PIP_VIEW_WIDTH; private moving = false; + private scheduledUpdate = new MarkedExecution( + () => this.animationCallback(), + () => requestAnimationFrame(() => this.scheduledUpdate.trigger()), + ); constructor(props: IProps) { super(props); @@ -175,7 +180,7 @@ export default class CallPreview extends React.Component { translationX: lerp(this.state.translationX, this.desiredTranslationX, amt), translationY: lerp(this.state.translationY, this.desiredTranslationY, amt), }); - requestAnimationFrame(this.animationCallback); + this.scheduledUpdate.mark(); }; private setTranslation(inTranslationX: number, inTranslationY: number) { @@ -232,7 +237,7 @@ export default class CallPreview extends React.Component { // We start animating here because we want the PiP to move when we're // resizing the window - requestAnimationFrame(this.animationCallback); + this.scheduledUpdate.mark(); }; private onRoomViewStoreUpdate = (payload) => { @@ -290,7 +295,7 @@ export default class CallPreview extends React.Component { this.moving = true; this.initX = event.pageX - this.desiredTranslationX; this.initY = event.pageY - this.desiredTranslationY; - requestAnimationFrame(this.animationCallback); + this.scheduledUpdate.mark(); }; private onMoving = (event: React.MouseEvent | MouseEvent) => { From 8072007782f420529f827d8672a9c114d449f46a Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 9 Jul 2021 14:50:55 +0100 Subject: [PATCH 219/221] Fix small visual regression with the site name on url previews --- res/css/views/rooms/_LinkPreviewWidget.scss | 6 +++--- src/components/views/rooms/LinkPreviewWidget.tsx | 8 ++++++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/res/css/views/rooms/_LinkPreviewWidget.scss b/res/css/views/rooms/_LinkPreviewWidget.scss index e1628e19a6..0832337ecd 100644 --- a/res/css/views/rooms/_LinkPreviewWidget.scss +++ b/res/css/views/rooms/_LinkPreviewWidget.scss @@ -43,10 +43,10 @@ limitations under the License. -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; -} -.mx_LinkPreviewWidget_siteName { - display: inline; + .mx_LinkPreviewWidget_siteName { + font-weight: normal; + } } .mx_LinkPreviewWidget_description { diff --git a/src/components/views/rooms/LinkPreviewWidget.tsx b/src/components/views/rooms/LinkPreviewWidget.tsx index db13021b32..7e6dd86d19 100644 --- a/src/components/views/rooms/LinkPreviewWidget.tsx +++ b/src/components/views/rooms/LinkPreviewWidget.tsx @@ -139,8 +139,12 @@ export default class LinkPreviewWidget extends React.Component {
    { img }
    - -
    { p["og:site_name"] ? (" - " + p["og:site_name"]) : null }
    +
    + { p["og:title"] } + { p["og:site_name"] && + { (" - " + p["og:site_name"]) } + } +
    { description }
    From d9b8f0d540d1506b1fc885b6e0b87adb8374cf8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 9 Jul 2021 15:58:35 +0200 Subject: [PATCH 220/221] Add docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/utils/AnimationUtils.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/utils/AnimationUtils.ts b/src/utils/AnimationUtils.ts index 9654bdeb11..0ade08df84 100644 --- a/src/utils/AnimationUtils.ts +++ b/src/utils/AnimationUtils.ts @@ -14,6 +14,16 @@ See the License for the specific language governing permissions and limitations under the License. */ +/** + * This method linearly interpolates between two points (start, end). This is + * most commonly used to find a point some fraction of the way along a line + * between two endpoints (e.g. to move an object gradually between those + * points). + * @param {number} start the starting point + * @param {number} end the ending point + * @param {number} amt the interpolant + * @returns + */ export function lerp(start: number, end: number, amt: number) { return (1 - amt) * start + amt * end; } From a90b8f32f19eab28b9c0f7baa9b96712e77eeb91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 9 Jul 2021 16:45:04 +0200 Subject: [PATCH 221/221] Add some tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/utils/AnimationUtils.ts | 3 +++ test/utils/AnimationUtils-test.ts | 35 +++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 test/utils/AnimationUtils-test.ts diff --git a/src/utils/AnimationUtils.ts b/src/utils/AnimationUtils.ts index 0ade08df84..61df52826d 100644 --- a/src/utils/AnimationUtils.ts +++ b/src/utils/AnimationUtils.ts @@ -14,6 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { clamp } from "lodash"; + /** * This method linearly interpolates between two points (start, end). This is * most commonly used to find a point some fraction of the way along a line @@ -25,5 +27,6 @@ limitations under the License. * @returns */ export function lerp(start: number, end: number, amt: number) { + amt = clamp(amt, 0, 1); return (1 - amt) * start + amt * end; } diff --git a/test/utils/AnimationUtils-test.ts b/test/utils/AnimationUtils-test.ts new file mode 100644 index 0000000000..b6d75a706f --- /dev/null +++ b/test/utils/AnimationUtils-test.ts @@ -0,0 +1,35 @@ +/* +Copyright 2021 Šimon Brandner + +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. +*/ + +import { lerp } from "../../src/utils/AnimationUtils"; + +describe("lerp", () => { + it("correctly interpolates", () => { + expect(lerp(0, 100, 0.5)).toBe(50); + expect(lerp(50, 100, 0.5)).toBe(75); + expect(lerp(0, 1, 0.1)).toBe(0.1); + }); + + it("clamps the interpolant", () => { + expect(lerp(0, 100, 50)).toBe(100); + expect(lerp(0, 100, -50)).toBe(0); + }); + + it("handles negative numbers", () => { + expect(lerp(-100, 0, 0.5)).toBe(-50); + expect(lerp(100, -100, 0.5)).toBe(0); + }); +});