From 53db386731422c88d4e24e299e9e56a150dad3cf Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 10 Aug 2020 22:06:30 -0600 Subject: [PATCH 001/999] 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 6f464feded9c3ea9541bd2be9a5a704bc70ba2b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 23 Feb 2021 18:12:46 +0100 Subject: [PATCH 002/999] Quit sticker picker on m.sticker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/elements/AppTile.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index 213351889f..9a345a7bf6 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -226,6 +226,7 @@ export default class AppTile extends React.Component { case 'm.sticker': if (this._sgWidget.widgetApi.hasCapability(MatrixCapabilities.StickerSending)) { dis.dispatch({action: 'post_sticker_message', data: payload.data}); + dis.dispatch({action: 'stickerpicker_close'}); } else { console.warn('Ignoring sticker message. Invalid capability'); } 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 003/999] 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 004/999] 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 3df9557df2f886db13185872641b2f763baf41aa Mon Sep 17 00:00:00 2001 From: Ayush PS Date: Wed, 24 Mar 2021 14:00:09 +0530 Subject: [PATCH 005/999] Dial Pad UI fix --- res/css/views/voip/_DialPad.scss | 1 + res/css/views/voip/_DialPadContextMenu.scss | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/res/css/views/voip/_DialPad.scss b/res/css/views/voip/_DialPad.scss index 0c7bff0ce8..fd7c5f56f6 100644 --- a/res/css/views/voip/_DialPad.scss +++ b/res/css/views/voip/_DialPad.scss @@ -30,6 +30,7 @@ limitations under the License. text-align: center; vertical-align: middle; line-height: 40px; + color: #15191e; } .mx_DialPad_deleteButton, .mx_DialPad_dialButton { diff --git a/res/css/views/voip/_DialPadContextMenu.scss b/res/css/views/voip/_DialPadContextMenu.scss index 520f51cf93..c400060b6c 100644 --- a/res/css/views/voip/_DialPadContextMenu.scss +++ b/res/css/views/voip/_DialPadContextMenu.scss @@ -27,9 +27,11 @@ limitations under the License. } .mx_DialPadContextMenu_dialled { - height: 1em; + height: 1.5em; font-size: 18px; font-weight: 600; + max-width: 150px; + overflow: auto; } .mx_DialPadContextMenu_dialPad { From 1488457c3322b6d5dd6ef7882d5b14d9bf49e20f Mon Sep 17 00:00:00 2001 From: Ayush PS Date: Thu, 25 Mar 2021 01:31:45 +0530 Subject: [PATCH 006/999] Added the class -button-bg-color for all themes --- res/css/views/voip/_DialPad.scss | 3 +-- res/themes/dark/css/_dark.scss | 2 ++ res/themes/legacy-dark/css/_legacy-dark.scss | 1 + res/themes/legacy-light/css/_legacy-light.scss | 2 ++ res/themes/light/css/_light.scss | 2 ++ 5 files changed, 8 insertions(+), 2 deletions(-) diff --git a/res/css/views/voip/_DialPad.scss b/res/css/views/voip/_DialPad.scss index fd7c5f56f6..483b131bfe 100644 --- a/res/css/views/voip/_DialPad.scss +++ b/res/css/views/voip/_DialPad.scss @@ -23,14 +23,13 @@ limitations under the License. .mx_DialPad_button { width: 40px; height: 40px; - background-color: $theme-button-bg-color; + background-color: $dialpad-button-bg-color; border-radius: 40px; font-size: 18px; font-weight: 600; text-align: center; vertical-align: middle; line-height: 40px; - color: #15191e; } .mx_DialPad_deleteButton, .mx_DialPad_dialButton { diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index 7a751ad9c1..42d592c1e1 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -114,6 +114,8 @@ $voipcall-plinth-color: #21262c; // ******************** $theme-button-bg-color: #e3e8f0; +$dialpad-button-bg-color: #545454; + $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; diff --git a/res/themes/legacy-dark/css/_legacy-dark.scss b/res/themes/legacy-dark/css/_legacy-dark.scss index 764b8f302a..ae98141d06 100644 --- a/res/themes/legacy-dark/css/_legacy-dark.scss +++ b/res/themes/legacy-dark/css/_legacy-dark.scss @@ -111,6 +111,7 @@ $voipcall-plinth-color: #f2f5f8; // ******************** $theme-button-bg-color: #e3e8f0; +$dialpad-button-bg-color: #545454; $roomlist-button-bg-color: #1A1D23; // Buttons include the filter box, explore button, and sublist buttons $roomlist-filter-active-bg-color: $roomlist-button-bg-color; diff --git a/res/themes/legacy-light/css/_legacy-light.scss b/res/themes/legacy-light/css/_legacy-light.scss index 9ad154dd93..4313e3c0b6 100644 --- a/res/themes/legacy-light/css/_legacy-light.scss +++ b/res/themes/legacy-light/css/_legacy-light.scss @@ -178,6 +178,8 @@ $voipcall-plinth-color: #f2f5f8; // ******************** $theme-button-bg-color: #e3e8f0; +$dialpad-button-bg-color: #e3e8f0; + $roomlist-button-bg-color: #fff; // Buttons include the filter box, explore button, and sublist buttons $roomlist-filter-active-bg-color: $roomlist-button-bg-color; diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index 25fbd0201b..81330d07c9 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -169,6 +169,8 @@ $voipcall-plinth-color: #f2f5f8; // ******************** $theme-button-bg-color: #e3e8f0; +$dialpad-button-bg-color: #e3e8f0; + $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: #ffffff; From a308a5418323f9529986791a59e27e37ce2eebae Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Wed, 31 Mar 2021 12:28:24 +0100 Subject: [PATCH 007/999] Clicking jump to bottom resets room hash --- res/css/views/rooms/_JumpToBottomButton.scss | 1 + src/components/structures/RoomView.tsx | 1 + src/components/views/rooms/JumpToBottomButton.js | 2 ++ 3 files changed, 4 insertions(+) diff --git a/res/css/views/rooms/_JumpToBottomButton.scss b/res/css/views/rooms/_JumpToBottomButton.scss index 6cb3b6bce9..a8dc2ce11c 100644 --- a/res/css/views/rooms/_JumpToBottomButton.scss +++ b/res/css/views/rooms/_JumpToBottomButton.scss @@ -52,6 +52,7 @@ limitations under the License. .mx_JumpToBottomButton_scrollDown { position: relative; + display: block; height: 38px; border-radius: 19px; box-sizing: border-box; diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index a180afba29..e08461b511 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -2037,6 +2037,7 @@ export default class RoomView extends React.Component { highlight={this.state.room.getUnreadNotificationCount('highlight') > 0} numUnreadMessages={this.state.numUnreadMessages} onScrollToBottomClick={this.jumpToLiveTimeline} + roomId={this.state.roomId} />); } diff --git a/src/components/views/rooms/JumpToBottomButton.js b/src/components/views/rooms/JumpToBottomButton.js index b6cefc1231..2c62877dc3 100644 --- a/src/components/views/rooms/JumpToBottomButton.js +++ b/src/components/views/rooms/JumpToBottomButton.js @@ -29,6 +29,8 @@ export default (props) => { } return (
From c5eb17eabd2fbb9245cd401397d6b385c58d5128 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Tue, 6 Apr 2021 17:26:32 +0100 Subject: [PATCH 008/999] reset highlighted event on room timeline scroll --- src/components/structures/MessagePanel.js | 4 ++++ src/components/structures/RoomView.tsx | 12 ++++++++++++ src/components/structures/ScrollPanel.js | 13 +++++++++++++ src/components/structures/TimelinePanel.js | 8 +++++++- 4 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 41a3015721..371ee5dce7 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -120,6 +120,9 @@ export default class MessagePanel extends React.Component { // callback which is called when the panel is scrolled. onScroll: PropTypes.func, + // callback which is called when the user interacts with the room timeline + onUserScroll: PropTypes.func, + // callback which is called when more content is needed. onFillRequest: PropTypes.func, @@ -869,6 +872,7 @@ export default class MessagePanel extends React.Component { ref={this._scrollPanel} className={className} onScroll={this.props.onScroll} + onUserScroll={this.props.onUserScroll} onResize={this.onResize} onFillRequest={this.props.onFillRequest} onUnfillRequest={this.props.onUnfillRequest} diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index e08461b511..8d815c79a1 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -637,6 +637,17 @@ export default class RoomView extends React.Component { SettingsStore.unwatchSetting(this.layoutWatcherRef); } + private onUserScroll = () => { + if (this.state.initialEventId && this.state.isInitialEventHighlighted) { + dis.dispatch({ + action: 'view_room', + room_id: this.state.room.roomId, + event_id: this.state.initialEventId, + highlighted: false, + }); + } + } + private onLayoutChange = () => { this.setState({ layout: SettingsStore.getValue("layout"), @@ -2011,6 +2022,7 @@ export default class RoomView extends React.Component { eventId={this.state.initialEventId} eventPixelOffset={this.state.initialEventPixelOffset} onScroll={this.onMessageListScroll} + onUserScroll={this.onUserScroll} onReadMarkerUpdated={this.updateTopUnreadMessagesBar} showUrlPreview = {this.state.showUrlPreview} className={messagePanelClassNames} diff --git a/src/components/structures/ScrollPanel.js b/src/components/structures/ScrollPanel.js index 3a9b2b8a77..5cb9437b81 100644 --- a/src/components/structures/ScrollPanel.js +++ b/src/components/structures/ScrollPanel.js @@ -133,6 +133,10 @@ export default class ScrollPanel extends React.Component { */ onScroll: PropTypes.func, + /* onUserScroll: callback which is called when the user interacts with the room timeline + */ + onUserScroll: PropTypes.func, + /* className: classnames to add to the top-level div */ className: PropTypes.string, @@ -535,31 +539,39 @@ export default class ScrollPanel extends React.Component { * @param {object} ev the keyboard event */ handleScrollKey = ev => { + let isScrolling = false; switch (ev.key) { case Key.PAGE_UP: if (!ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) { + isScrolling = true; this.scrollRelative(-1); } break; case Key.PAGE_DOWN: if (!ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) { + isScrolling = true; this.scrollRelative(1); } break; case Key.HOME: if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) { + isScrolling = true; this.scrollToTop(); } break; case Key.END: if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) { + isScrolling = true; this.scrollToBottom(); } break; } + if (isScrolling && this.props.onUserScroll) { + this.props.onUserScroll(ev); + } }; /* Scroll the panel to bring the DOM node with the scroll token @@ -896,6 +908,7 @@ export default class ScrollPanel extends React.Component { // list-style-type: none; is no longer a list return ( { this.props.fixedChildren }
diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 12f5d6e890..b1d1e16719 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -92,6 +92,9 @@ class TimelinePanel extends React.Component { // callback which is called when the panel is scrolled. onScroll: PropTypes.func, + // callback which is called when the user interacts with the room timeline + onUserScroll: PropTypes.func, + // callback which is called when the read-up-to mark is updated. onReadMarkerUpdated: PropTypes.func, @@ -255,7 +258,9 @@ class TimelinePanel extends React.Component { console.warn("Replacing timelineSet on a TimelinePanel - confusion may ensue"); } - if (newProps.eventId != this.props.eventId) { + const differentEventId = newProps.eventId != this.props.eventId; + const differentHighlightedEventId = newProps.highlightedEventId != this.props.highlightedEventId; + if (differentEventId || differentHighlightedEventId) { console.log("TimelinePanel switching to eventId " + newProps.eventId + " (was " + this.props.eventId + ")"); return this._initTimeline(newProps); @@ -1438,6 +1443,7 @@ class TimelinePanel extends React.Component { ourUserId={MatrixClientPeg.get().credentials.userId} stickyBottom={stickyBottom} onScroll={this.onMessageListScroll} + onUserScroll={this.props.onUserScroll} onFillRequest={this.onMessageListFillRequest} onUnfillRequest={this.onMessageListUnfillRequest} isTwelveHour={this.state.isTwelveHour} From ef1da6acddf04530100c467274d0fb1f4dae7907 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Fri, 9 Apr 2021 09:02:47 +0100 Subject: [PATCH 009/999] remove wrongly committed orig file --- src/components/structures/ScrollPanel.js.orig | 938 ------------------ 1 file changed, 938 deletions(-) delete mode 100644 src/components/structures/ScrollPanel.js.orig diff --git a/src/components/structures/ScrollPanel.js.orig b/src/components/structures/ScrollPanel.js.orig deleted file mode 100644 index 5909632aa6..0000000000 --- a/src/components/structures/ScrollPanel.js.orig +++ /dev/null @@ -1,938 +0,0 @@ -/* -Copyright 2015, 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, {createRef} from "react"; -import PropTypes from 'prop-types'; -import Timer from '../../utils/Timer'; -import AutoHideScrollbar from "./AutoHideScrollbar"; -import {replaceableComponent} from "../../utils/replaceableComponent"; -import {getKeyBindingsManager, RoomAction} from "../../KeyBindingsManager"; - -const DEBUG_SCROLL = false; - -// The amount of extra scroll distance to allow prior to unfilling. -// See _getExcessHeight. -const UNPAGINATION_PADDING = 6000; -// The number of milliseconds to debounce calls to onUnfillRequest, to prevent -// many scroll events causing many unfilling requests. -const UNFILL_REQUEST_DEBOUNCE_MS = 200; -// _updateHeight makes the height a ceiled multiple of this so we -// don't have to update the height too often. It also allows the user -// to scroll past the pagination spinner a bit so they don't feel blocked so -// much while the content loads. -const PAGE_SIZE = 400; - -let debuglog; -if (DEBUG_SCROLL) { - // using bind means that we get to keep useful line numbers in the console - debuglog = console.log.bind(console, "ScrollPanel debuglog:"); -} else { - debuglog = function() {}; -} - -/* This component implements an intelligent scrolling list. - * - * It wraps a list of
  • children; when items are added to the start or end - * of the list, the scroll position is updated so that the user still sees the - * same position in the list. - * - * It also provides a hook which allows parents to provide more list elements - * when we get close to the start or end of the list. - * - * Each child element should have a 'data-scroll-tokens'. This string of - * comma-separated tokens may contain a single token or many, where many indicates - * that the element contains elements that have scroll tokens themselves. The first - * token in 'data-scroll-tokens' is used to serialise the scroll state, and returned - * as the 'trackedScrollToken' attribute by getScrollState(). - * - * IMPORTANT: INDIVIDUAL TOKENS WITHIN 'data-scroll-tokens' MUST NOT CONTAIN COMMAS. - * - * Some notes about the implementation: - * - * The saved 'scrollState' can exist in one of two states: - * - * - stuckAtBottom: (the default, and restored by resetScrollState): the - * viewport is scrolled down as far as it can be. When the children are - * updated, the scroll position will be updated to ensure it is still at - * the bottom. - * - * - fixed, in which the viewport is conceptually tied at a specific scroll - * offset. We don't save the absolute scroll offset, because that would be - * affected by window width, zoom level, amount of scrollback, etc. Instead - * we save an identifier for the last fully-visible message, and the number - * of pixels the window was scrolled below it - which is hopefully near - * enough. - * - * The 'stickyBottom' property controls the behaviour when we reach the bottom - * of the window (either through a user-initiated scroll, or by calling - * scrollToBottom). If stickyBottom is enabled, the scrollState will enter - * 'stuckAtBottom' state - ensuring that new additions cause the window to - * scroll down further. If stickyBottom is disabled, we just save the scroll - * offset as normal. - */ - -@replaceableComponent("structures.ScrollPanel") -export default class ScrollPanel extends React.Component { - static propTypes = { - /* stickyBottom: if set to true, then once the user hits the bottom of - * the list, any new children added to the list will cause the list to - * scroll down to show the new element, rather than preserving the - * existing view. - */ - stickyBottom: PropTypes.bool, - - /* startAtBottom: if set to true, the view is assumed to start - * scrolled to the bottom. - * XXX: It's likely this is unnecessary and can be derived from - * stickyBottom, but I'm adding an extra parameter to ensure - * behaviour stays the same for other uses of ScrollPanel. - * If so, let's remove this parameter down the line. - */ - startAtBottom: PropTypes.bool, - - /* onFillRequest(backwards): a callback which is called on scroll when - * the user nears the start (backwards = true) or end (backwards = - * false) of the list. - * - * This should return a promise; no more calls will be made until the - * promise completes. - * - * The promise should resolve to true if there is more data to be - * retrieved in this direction (in which case onFillRequest may be - * called again immediately), or false if there is no more data in this - * directon (at this time) - which will stop the pagination cycle until - * the user scrolls again. - */ - onFillRequest: PropTypes.func, - - /* onUnfillRequest(backwards): a callback which is called on scroll when - * there are children elements that are far out of view and could be removed - * without causing pagination to occur. - * - * This function should accept a boolean, which is true to indicate the back/top - * of the panel and false otherwise, and a scroll token, which refers to the - * first element to remove if removing from the front/bottom, and last element - * to remove if removing from the back/top. - */ - onUnfillRequest: PropTypes.func, - - /* onScroll: a callback which is called whenever any scroll happens. - */ - onScroll: PropTypes.func, - - /* onUserScroll: callback which is called when the user interacts with the room timeline - */ - onUserScroll: PropTypes.func, - - /* className: classnames to add to the top-level div - */ - className: PropTypes.string, - - /* style: styles to add to the top-level div - */ - style: PropTypes.object, - - /* resizeNotifier: ResizeNotifier to know when middle column has changed size - */ - resizeNotifier: PropTypes.object, - - /* fixedChildren: allows for children to be passed which are rendered outside - * of the wrapper - */ - fixedChildren: PropTypes.node, - }; - - static defaultProps = { - stickyBottom: true, - startAtBottom: true, - onFillRequest: function(backwards) { return Promise.resolve(false); }, - onUnfillRequest: function(backwards, scrollToken) {}, - onScroll: function() {}, - }; - - constructor(props) { - super(props); - - this._pendingFillRequests = {b: null, f: null}; - - if (this.props.resizeNotifier) { - this.props.resizeNotifier.on("middlePanelResizedNoisy", this.onResize); - } - - this.resetScrollState(); - - this._itemlist = createRef(); - } - - componentDidMount() { - this.checkScroll(); - } - - componentDidUpdate() { - // after adding event tiles, we may need to tweak the scroll (either to - // keep at the bottom of the timeline, or to maintain the view after - // adding events to the top). - // - // This will also re-check the fill state, in case the paginate was inadequate - this.checkScroll(); - this.updatePreventShrinking(); - } - - componentWillUnmount() { - // set a boolean to say we've been unmounted, which any pending - // promises can use to throw away their results. - // - // (We could use isMounted(), but facebook have deprecated that.) - this.unmounted = true; - - if (this.props.resizeNotifier) { - this.props.resizeNotifier.removeListener("middlePanelResizedNoisy", this.onResize); - } - } - - onScroll = ev => { - // skip scroll events caused by resizing - if (this.props.resizeNotifier && this.props.resizeNotifier.isResizing) return; - debuglog("onScroll", this._getScrollNode().scrollTop); - this._scrollTimeout.restart(); - this._saveScrollState(); - this.updatePreventShrinking(); - this.props.onScroll(ev); - this.checkFillState(); - }; - - onResize = () => { - debuglog("onResize"); - this.checkScroll(); - // update preventShrinkingState if present - if (this.preventShrinkingState) { - this.preventShrinking(); - } - }; - - // after an update to the contents of the panel, check that the scroll is - // where it ought to be, and set off pagination requests if necessary. - checkScroll = () => { - if (this.unmounted) { - return; - } - this._restoreSavedScrollState(); - this.checkFillState(); - }; - - // return true if the content is fully scrolled down right now; else false. - // - // note that this is independent of the 'stuckAtBottom' state - it is simply - // about whether the content is scrolled down right now, irrespective of - // whether it will stay that way when the children update. - isAtBottom = () => { - const sn = this._getScrollNode(); - // fractional values (both too big and too small) - // for scrollTop happen on certain browsers/platforms - // when scrolled all the way down. E.g. Chrome 72 on debian. - // so check difference <= 1; - return Math.abs(sn.scrollHeight - (sn.scrollTop + sn.clientHeight)) <= 1; - }; - - // returns the vertical height in the given direction that can be removed from - // the content box (which has a height of scrollHeight, see checkFillState) without - // pagination occuring. - // - // padding* = UNPAGINATION_PADDING - // - // ### Region determined as excess. - // - // .---------. - - - // |#########| | | - // |#########| - | scrollTop | - // | | | padding* | | - // | | | | | - // .-+---------+-. - - | | - // : | | : | | | - // : | | : | clientHeight | | - // : | | : | | | - // .-+---------+-. - - | - // | | | | | | - // | | | | | clientHeight | scrollHeight - // | | | | | | - // `-+---------+-' - | - // : | | : | | - // : | | : | clientHeight | - // : | | : | | - // `-+---------+-' - - | - // | | | padding* | - // | | | | - // |#########| - | - // |#########| | - // `---------' - - _getExcessHeight(backwards) { - const sn = this._getScrollNode(); - const contentHeight = this._getMessagesHeight(); - const listHeight = this._getListHeight(); - const clippedHeight = contentHeight - listHeight; - const unclippedScrollTop = sn.scrollTop + clippedHeight; - - if (backwards) { - return unclippedScrollTop - sn.clientHeight - UNPAGINATION_PADDING; - } else { - return contentHeight - (unclippedScrollTop + 2*sn.clientHeight) - UNPAGINATION_PADDING; - } - } - - // check the scroll state and send out backfill requests if necessary. - checkFillState = async (depth=0) => { - if (this.unmounted) { - return; - } - - const isFirstCall = depth === 0; - const sn = this._getScrollNode(); - - // if there is less than a screenful of messages above or below the - // viewport, try to get some more messages. - // - // scrollTop is the number of pixels between the top of the content and - // the top of the viewport. - // - // scrollHeight is the total height of the content. - // - // clientHeight is the height of the viewport (excluding borders, - // margins, and scrollbars). - // - // - // .---------. - - - // | | | scrollTop | - // .-+---------+-. - - | - // | | | | | | - // | | | | | clientHeight | scrollHeight - // | | | | | | - // `-+---------+-' - | - // | | | - // | | | - // `---------' - - // - - // as filling is async and recursive, - // don't allow more than 1 chain of calls concurrently - // do make a note when a new request comes in while already running one, - // so we can trigger a new chain of calls once done. - if (isFirstCall) { - if (this._isFilling) { - debuglog("_isFilling: not entering while request is ongoing, marking for a subsequent request"); - this._fillRequestWhileRunning = true; - return; - } - debuglog("_isFilling: setting"); - this._isFilling = true; - } - - const itemlist = this._itemlist.current; - const firstTile = itemlist && itemlist.firstElementChild; - const contentTop = firstTile && firstTile.offsetTop; - const fillPromises = []; - - // if scrollTop gets to 1 screen from the top of the first tile, - // try backward filling - if (!firstTile || (sn.scrollTop - contentTop) < sn.clientHeight) { - // need to back-fill - fillPromises.push(this._maybeFill(depth, true)); - } - // if scrollTop gets to 2 screens from the end (so 1 screen below viewport), - // try forward filling - if ((sn.scrollHeight - sn.scrollTop) < sn.clientHeight * 2) { - // need to forward-fill - fillPromises.push(this._maybeFill(depth, false)); - } - - if (fillPromises.length) { - try { - await Promise.all(fillPromises); - } catch (err) { - console.error(err); - } - } - if (isFirstCall) { - debuglog("_isFilling: clearing"); - this._isFilling = false; - } - - if (this._fillRequestWhileRunning) { - this._fillRequestWhileRunning = false; - this.checkFillState(); - } - }; - - // check if unfilling is possible and send an unfill request if necessary - _checkUnfillState(backwards) { - let excessHeight = this._getExcessHeight(backwards); - if (excessHeight <= 0) { - return; - } - - const origExcessHeight = excessHeight; - - const tiles = this._itemlist.current.children; - - // The scroll token of the first/last tile to be unpaginated - let markerScrollToken = null; - - // Subtract heights of tiles to simulate the tiles being unpaginated until the - // excess height is less than the height of the next tile to subtract. This - // prevents excessHeight becoming negative, which could lead to future - // pagination. - // - // If backwards is true, we unpaginate (remove) tiles from the back (top). - let tile; - for (let i = 0; i < tiles.length; i++) { - tile = tiles[backwards ? i : tiles.length - 1 - i]; - // Subtract height of tile as if it were unpaginated - excessHeight -= tile.clientHeight; - //If removing the tile would lead to future pagination, break before setting scroll token - if (tile.clientHeight > excessHeight) { - break; - } - // The tile may not have a scroll token, so guard it - if (tile.dataset.scrollTokens) { - markerScrollToken = tile.dataset.scrollTokens.split(',')[0]; - } - } - - if (markerScrollToken) { - // Use a debouncer to prevent multiple unfill calls in quick succession - // This is to make the unfilling process less aggressive - if (this._unfillDebouncer) { - clearTimeout(this._unfillDebouncer); - } - this._unfillDebouncer = setTimeout(() => { - this._unfillDebouncer = null; - debuglog("unfilling now", backwards, origExcessHeight); - this.props.onUnfillRequest(backwards, markerScrollToken); - }, UNFILL_REQUEST_DEBOUNCE_MS); - } - } - - // check if there is already a pending fill request. If not, set one off. - _maybeFill(depth, backwards) { - const dir = backwards ? 'b' : 'f'; - if (this._pendingFillRequests[dir]) { - debuglog("Already a "+dir+" fill in progress - not starting another"); - return; - } - - debuglog("starting "+dir+" fill"); - - // onFillRequest can end up calling us recursively (via onScroll - // events) so make sure we set this before firing off the call. - this._pendingFillRequests[dir] = true; - - // wait 1ms before paginating, because otherwise - // this will block the scroll event handler for +700ms - // if messages are already cached in memory, - // This would cause jumping to happen on Chrome/macOS. - return new Promise(resolve => setTimeout(resolve, 1)).then(() => { - return this.props.onFillRequest(backwards); - }).finally(() => { - this._pendingFillRequests[dir] = false; - }).then((hasMoreResults) => { - if (this.unmounted) { - return; - } - // Unpaginate once filling is complete - this._checkUnfillState(!backwards); - - debuglog(""+dir+" fill complete; hasMoreResults:"+hasMoreResults); - if (hasMoreResults) { - // further pagination requests have been disabled until now, so - // it's time to check the fill state again in case the pagination - // was insufficient. - return this.checkFillState(depth + 1); - } - }); - } - - /* get the current scroll state. This returns an object with the following - * properties: - * - * boolean stuckAtBottom: true if we are tracking the bottom of the - * scroll. false if we are tracking a particular child. - * - * string trackedScrollToken: undefined if stuckAtBottom is true; if it is - * false, the first token in data-scroll-tokens of the child which we are - * tracking. - * - * number bottomOffset: undefined if stuckAtBottom is true; if it is false, - * the number of pixels the bottom of the tracked child is above the - * bottom of the scroll panel. - */ - getScrollState = () => this.scrollState; - - /* reset the saved scroll state. - * - * This is useful if the list is being replaced, and you don't want to - * preserve scroll even if new children happen to have the same scroll - * tokens as old ones. - * - * This will cause the viewport to be scrolled down to the bottom on the - * next update of the child list. This is different to scrollToBottom(), - * which would save the current bottom-most child as the active one (so is - * no use if no children exist yet, or if you are about to replace the - * child list.) - */ - resetScrollState = () => { - this.scrollState = { - stuckAtBottom: this.props.startAtBottom, - }; - this._bottomGrowth = 0; - this._pages = 0; - this._scrollTimeout = new Timer(100); - this._heightUpdateInProgress = false; - }; - - /** - * jump to the top of the content. - */ - scrollToTop = () => { - this._getScrollNode().scrollTop = 0; - this._saveScrollState(); - }; - - /** - * jump to the bottom of the content. - */ - scrollToBottom = () => { - // the easiest way to make sure that the scroll state is correctly - // saved is to do the scroll, then save the updated state. (Calculating - // it ourselves is hard, and we can't rely on an onScroll callback - // happening, since there may be no user-visible change here). - const sn = this._getScrollNode(); - sn.scrollTop = sn.scrollHeight; - this._saveScrollState(); - }; - - /** - * Page up/down. - * - * @param {number} mult: -1 to page up, +1 to page down - */ - scrollRelative = mult => { - const scrollNode = this._getScrollNode(); - const delta = mult * scrollNode.clientHeight * 0.5; - scrollNode.scrollBy(0, delta); - this._saveScrollState(); - }; - - /** - * Scroll up/down in response to a scroll key - * @param {object} ev the keyboard event - */ - handleScrollKey = ev => { -<<<<<<< HEAD - let isScrolling = false; - switch (ev.key) { - case Key.PAGE_UP: - if (!ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) { - isScrolling = true; - this.scrollRelative(-1); - } - break; - - case Key.PAGE_DOWN: - if (!ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) { - isScrolling = true; - this.scrollRelative(1); - } - break; - - case Key.HOME: - if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) { - isScrolling = true; - this.scrollToTop(); - } - break; - - case Key.END: - if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) { - isScrolling = true; - this.scrollToBottom(); - } -======= - const roomAction = getKeyBindingsManager().getRoomAction(ev); - switch (roomAction) { - case RoomAction.ScrollUp: - this.scrollRelative(-1); - break; - case RoomAction.RoomScrollDown: - this.scrollRelative(1); - break; - case RoomAction.JumpToFirstMessage: - this.scrollToTop(); - break; - case RoomAction.JumpToLatestMessage: - this.scrollToBottom(); ->>>>>>> develop - break; - } - if (isScrolling && this.props.onUserScroll) { - this.props.onUserScroll(ev); - } - }; - - /* Scroll the panel to bring the DOM node with the scroll token - * `scrollToken` into view. - * - * offsetBase gives the reference point for the pixelOffset. 0 means the - * top of the container, 1 means the bottom, and fractional values mean - * somewhere in the middle. If omitted, it defaults to 0. - * - * pixelOffset gives the number of pixels *above* the offsetBase that the - * node (specifically, the bottom of it) will be positioned. If omitted, it - * defaults to 0. - */ - scrollToToken = (scrollToken, pixelOffset, offsetBase) => { - pixelOffset = pixelOffset || 0; - offsetBase = offsetBase || 0; - - // set the trackedScrollToken so we can get the node through _getTrackedNode - this.scrollState = { - stuckAtBottom: false, - trackedScrollToken: scrollToken, - }; - const trackedNode = this._getTrackedNode(); - const scrollNode = this._getScrollNode(); - if (trackedNode) { - // set the scrollTop to the position we want. - // note though, that this might not succeed if the combination of offsetBase and pixelOffset - // would position the trackedNode towards the top of the viewport. - // This because when setting the scrollTop only 10 or so events might be loaded, - // not giving enough content below the trackedNode to scroll downwards - // enough so it ends up in the top of the viewport. - debuglog("scrollToken: setting scrollTop", {offsetBase, pixelOffset, offsetTop: trackedNode.offsetTop}); - scrollNode.scrollTop = (trackedNode.offsetTop - (scrollNode.clientHeight * offsetBase)) + pixelOffset; - this._saveScrollState(); - } - }; - - _saveScrollState() { - if (this.props.stickyBottom && this.isAtBottom()) { - this.scrollState = { stuckAtBottom: true }; - debuglog("saved stuckAtBottom state"); - return; - } - - const scrollNode = this._getScrollNode(); - const viewportBottom = scrollNode.scrollHeight - (scrollNode.scrollTop + scrollNode.clientHeight); - - const itemlist = this._itemlist.current; - const messages = itemlist.children; - let node = null; - - // TODO: do a binary search here, as items are sorted by offsetTop - // loop backwards, from bottom-most message (as that is the most common case) - for (let i = messages.length-1; i >= 0; --i) { - if (!messages[i].dataset.scrollTokens) { - continue; - } - node = messages[i]; - // break at the first message (coming from the bottom) - // that has it's offsetTop above the bottom of the viewport. - if (this._topFromBottom(node) > viewportBottom) { - // Use this node as the scrollToken - break; - } - } - - if (!node) { - debuglog("unable to save scroll state: found no children in the viewport"); - return; - } - const scrollToken = node.dataset.scrollTokens.split(',')[0]; - debuglog("saving anchored scroll state to message", node && node.innerText, scrollToken); - const bottomOffset = this._topFromBottom(node); - this.scrollState = { - stuckAtBottom: false, - trackedNode: node, - trackedScrollToken: scrollToken, - bottomOffset: bottomOffset, - pixelOffset: bottomOffset - viewportBottom, //needed for restoring the scroll position when coming back to the room - }; - } - - async _restoreSavedScrollState() { - const scrollState = this.scrollState; - - if (scrollState.stuckAtBottom) { - const sn = this._getScrollNode(); - if (sn.scrollTop !== sn.scrollHeight) { - sn.scrollTop = sn.scrollHeight; - } - } else if (scrollState.trackedScrollToken) { - const itemlist = this._itemlist.current; - const trackedNode = this._getTrackedNode(); - if (trackedNode) { - const newBottomOffset = this._topFromBottom(trackedNode); - const bottomDiff = newBottomOffset - scrollState.bottomOffset; - this._bottomGrowth += bottomDiff; - scrollState.bottomOffset = newBottomOffset; - const newHeight = `${this._getListHeight()}px`; - if (itemlist.style.height !== newHeight) { - itemlist.style.height = newHeight; - } - debuglog("balancing height because messages below viewport grew by", bottomDiff); - } - } - if (!this._heightUpdateInProgress) { - this._heightUpdateInProgress = true; - try { - await this._updateHeight(); - } finally { - this._heightUpdateInProgress = false; - } - } else { - debuglog("not updating height because request already in progress"); - } - } - - // need a better name that also indicates this will change scrollTop? Rebalance height? Reveal content? - async _updateHeight() { - // wait until user has stopped scrolling - if (this._scrollTimeout.isRunning()) { - debuglog("updateHeight waiting for scrolling to end ... "); - await this._scrollTimeout.finished(); - } else { - debuglog("updateHeight getting straight to business, no scrolling going on."); - } - - // We might have unmounted since the timer finished, so abort if so. - if (this.unmounted) { - return; - } - - const sn = this._getScrollNode(); - const itemlist = this._itemlist.current; - const contentHeight = this._getMessagesHeight(); - const minHeight = sn.clientHeight; - const height = Math.max(minHeight, contentHeight); - this._pages = Math.ceil(height / PAGE_SIZE); - this._bottomGrowth = 0; - const newHeight = `${this._getListHeight()}px`; - - const scrollState = this.scrollState; - if (scrollState.stuckAtBottom) { - if (itemlist.style.height !== newHeight) { - itemlist.style.height = newHeight; - } - if (sn.scrollTop !== sn.scrollHeight) { - sn.scrollTop = sn.scrollHeight; - } - debuglog("updateHeight to", newHeight); - } else if (scrollState.trackedScrollToken) { - const trackedNode = this._getTrackedNode(); - // if the timeline has been reloaded - // this can be called before scrollToBottom or whatever has been called - // so don't do anything if the node has disappeared from - // the currently filled piece of the timeline - if (trackedNode) { - const oldTop = trackedNode.offsetTop; - if (itemlist.style.height !== newHeight) { - itemlist.style.height = newHeight; - } - const newTop = trackedNode.offsetTop; - const topDiff = newTop - oldTop; - // important to scroll by a relative amount as - // reading scrollTop and then setting it might - // yield out of date values and cause a jump - // when setting it - sn.scrollBy(0, topDiff); - debuglog("updateHeight to", {newHeight, topDiff}); - } - } - } - - _getTrackedNode() { - const scrollState = this.scrollState; - const trackedNode = scrollState.trackedNode; - - if (!trackedNode || !trackedNode.parentElement) { - let node; - const messages = this._itemlist.current.children; - const scrollToken = scrollState.trackedScrollToken; - - for (let i = messages.length-1; i >= 0; --i) { - const m = messages[i]; - // 'data-scroll-tokens' is a DOMString of comma-separated scroll tokens - // There might only be one scroll token - if (m.dataset.scrollTokens && - m.dataset.scrollTokens.split(',').indexOf(scrollToken) !== -1) { - node = m; - break; - } - } - if (node) { - debuglog("had to find tracked node again for " + scrollState.trackedScrollToken); - } - scrollState.trackedNode = node; - } - - if (!scrollState.trackedNode) { - debuglog("No node with ; '"+scrollState.trackedScrollToken+"'"); - return; - } - - return scrollState.trackedNode; - } - - _getListHeight() { - return this._bottomGrowth + (this._pages * PAGE_SIZE); - } - - _getMessagesHeight() { - const itemlist = this._itemlist.current; - const lastNode = itemlist.lastElementChild; - const lastNodeBottom = lastNode ? lastNode.offsetTop + lastNode.clientHeight : 0; - const firstNodeTop = itemlist.firstElementChild ? itemlist.firstElementChild.offsetTop : 0; - // 18 is itemlist padding - return lastNodeBottom - firstNodeTop + (18 * 2); - } - - _topFromBottom(node) { - // current capped height - distance from top = distance from bottom of container to top of tracked element - return this._itemlist.current.clientHeight - node.offsetTop; - } - - /* get the DOM node which has the scrollTop property we care about for our - * message panel. - */ - _getScrollNode() { - if (this.unmounted) { - // this shouldn't happen, but when it does, turn the NPE into - // something more meaningful. - throw new Error("ScrollPanel._getScrollNode called when unmounted"); - } - - if (!this._divScroll) { - // Likewise, we should have the ref by this point, but if not - // turn the NPE into something meaningful. - throw new Error("ScrollPanel._getScrollNode called before AutoHideScrollbar ref collected"); - } - - return this._divScroll; - } - - _collectScroll = divScroll => { - this._divScroll = divScroll; - }; - - /** - Mark the bottom offset of the last tile so we can balance it out when - anything below it changes, by calling updatePreventShrinking, to keep - the same minimum bottom offset, effectively preventing the timeline to shrink. - */ - preventShrinking = () => { - const messageList = this._itemlist.current; - const tiles = messageList && messageList.children; - if (!messageList) { - return; - } - let lastTileNode; - for (let i = tiles.length - 1; i >= 0; i--) { - const node = tiles[i]; - if (node.dataset.scrollTokens) { - lastTileNode = node; - break; - } - } - if (!lastTileNode) { - return; - } - this.clearPreventShrinking(); - const offsetFromBottom = messageList.clientHeight - (lastTileNode.offsetTop + lastTileNode.clientHeight); - this.preventShrinkingState = { - offsetFromBottom: offsetFromBottom, - offsetNode: lastTileNode, - }; - debuglog("prevent shrinking, last tile ", offsetFromBottom, "px from bottom"); - }; - - /** Clear shrinking prevention. Used internally, and when the timeline is reloaded. */ - clearPreventShrinking = () => { - const messageList = this._itemlist.current; - const balanceElement = messageList && messageList.parentElement; - if (balanceElement) balanceElement.style.paddingBottom = null; - this.preventShrinkingState = null; - debuglog("prevent shrinking cleared"); - }; - - /** - update the container padding to balance - the bottom offset of the last tile since - preventShrinking was called. - Clears the prevent-shrinking state ones the offset - from the bottom of the marked tile grows larger than - what it was when marking. - */ - updatePreventShrinking = () => { - if (this.preventShrinkingState) { - const sn = this._getScrollNode(); - const scrollState = this.scrollState; - const messageList = this._itemlist.current; - const {offsetNode, offsetFromBottom} = this.preventShrinkingState; - // element used to set paddingBottom to balance the typing notifs disappearing - const balanceElement = messageList.parentElement; - // if the offsetNode got unmounted, clear - let shouldClear = !offsetNode.parentElement; - // also if 200px from bottom - if (!shouldClear && !scrollState.stuckAtBottom) { - const spaceBelowViewport = sn.scrollHeight - (sn.scrollTop + sn.clientHeight); - shouldClear = spaceBelowViewport >= 200; - } - // try updating if not clearing - if (!shouldClear) { - const currentOffset = messageList.clientHeight - (offsetNode.offsetTop + offsetNode.clientHeight); - const offsetDiff = offsetFromBottom - currentOffset; - if (offsetDiff > 0) { - balanceElement.style.paddingBottom = `${offsetDiff}px`; - debuglog("update prevent shrinking ", offsetDiff, "px from bottom"); - } else if (offsetDiff < 0) { - shouldClear = true; - } - } - if (shouldClear) { - this.clearPreventShrinking(); - } - } - }; - - render() { - // TODO: the classnames on the div and ol could do with being updated to - // reflect the fact that we don't necessarily contain a list of messages. - // it's not obvious why we have a separate div and ol anyway. - - // give the
      an explicit role=list because Safari+VoiceOver seems to think an ordered-list with - // list-style-type: none; is no longer a list - return ( - { this.props.fixedChildren } -
      -
        - { this.props.children } -
      -
      -
      - ); - } -} From d450822bfd99824bc0aa8b61aec603c71ddd4dd0 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Fri, 9 Apr 2021 11:17:50 +0100 Subject: [PATCH 010/999] fix linting issues in ScrollPanel --- src/components/structures/ScrollPanel.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/structures/ScrollPanel.js b/src/components/structures/ScrollPanel.js index 80447fd556..3c305524b8 100644 --- a/src/components/structures/ScrollPanel.js +++ b/src/components/structures/ScrollPanel.js @@ -539,24 +539,24 @@ export default class ScrollPanel extends React.Component { * @param {object} ev the keyboard event */ handleScrollKey = ev => { - let isScrolling = false; + let isScrolling = false; const roomAction = getKeyBindingsManager().getRoomAction(ev); switch (roomAction) { case RoomAction.ScrollUp: this.scrollRelative(-1); - isScrolling = true; + isScrolling = true; break; case RoomAction.RoomScrollDown: this.scrollRelative(1); - isScrolling = true; + isScrolling = true; break; case RoomAction.JumpToFirstMessage: this.scrollToTop(); - isScrolling = true; + isScrolling = true; break; case RoomAction.JumpToLatestMessage: this.scrollToBottom(); - isScrolling = true; + isScrolling = true; break; } if (isScrolling && this.props.onUserScroll) { From d148b521f5ab71498ba5a63559871273c6334d49 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Fri, 9 Apr 2021 11:23:41 +0100 Subject: [PATCH 011/999] Revert JumpToBottom to button and use dispatcher to view room --- src/components/structures/RoomView.tsx | 6 ++++-- src/components/views/rooms/JumpToBottomButton.js | 2 -- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 8d815c79a1..52608d1db1 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -1509,8 +1509,10 @@ export default class RoomView extends React.Component { // jump down to the bottom of this room, where new events are arriving private jumpToLiveTimeline = () => { - this.messagePanel.jumpToLiveTimeline(); - dis.fire(Action.FocusComposer); + dis.dispatch({ + action: 'view_room', + room_id: this.state.room.roomId, + }); }; // jump up to wherever our read marker is diff --git a/src/components/views/rooms/JumpToBottomButton.js b/src/components/views/rooms/JumpToBottomButton.js index 2c62877dc3..b6cefc1231 100644 --- a/src/components/views/rooms/JumpToBottomButton.js +++ b/src/components/views/rooms/JumpToBottomButton.js @@ -29,8 +29,6 @@ export default (props) => { } return (
      From ace3a62bacefe27dbbe77a2aeb3633528353d361 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 13 Apr 2021 14:52:26 +0100 Subject: [PATCH 012/999] Allow click inserting mentions into the edit composer too --- src/components/structures/TimelinePanel.js | 48 +++++++++++++------ .../views/rooms/BasicMessageComposer.tsx | 19 ++++++++ .../views/rooms/EditMessageComposer.js | 8 ++++ .../views/rooms/SendMessageComposer.js | 23 +-------- 4 files changed, 63 insertions(+), 35 deletions(-) diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 12f5d6e890..093e20b8b6 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -450,21 +450,41 @@ class TimelinePanel extends React.Component { }; onAction = payload => { - if (payload.action === 'ignore_state_changed') { - this.forceUpdate(); - } - if (payload.action === "edit_event") { - const editState = payload.event ? new EditorStateTransfer(payload.event) : null; - this.setState({editState}, () => { - if (payload.event && this._messagePanel.current) { - this._messagePanel.current.scrollToEventIfNeeded( - payload.event.getId(), - ); + switch (payload.action) { + case "ignore_state_changed": + this.forceUpdate(); + break; + + case "edit_event": { + const editState = payload.event ? new EditorStateTransfer(payload.event) : null; + this.setState({editState}, () => { + if (payload.event && this._messagePanel.current) { + this._messagePanel.current.scrollToEventIfNeeded( + payload.event.getId(), + ); + } + }); + break; + } + + case "insert_mention": { + if (this.state.editState) { + dis.dispatch({ + ...payload, + action: "insert_mention_edit_composer", + }); + } else { + dis.dispatch({ + ...payload, + action: "insert_mention_send_composer", + }); } - }); - } - if (payload.action === "scroll_to_bottom") { - this.jumpToLiveTimeline(); + break; + } + + case "scroll_to_bottom": + this.jumpToLiveTimeline(); + break; } }; diff --git a/src/components/views/rooms/BasicMessageComposer.tsx b/src/components/views/rooms/BasicMessageComposer.tsx index 9d9e3a1ba0..b8ebde75cc 100644 --- a/src/components/views/rooms/BasicMessageComposer.tsx +++ b/src/components/views/rooms/BasicMessageComposer.tsx @@ -713,4 +713,23 @@ export default class BasicMessageEditor extends React.Component focus() { this.editorRef.current.focus(); } + + public insertMention(userId: string) { + const {model} = this.props; + const {partCreator} = model; + const member = this.props.room.getMember(userId); + const displayName = member ? + member.rawDisplayName : userId; + const caret = this.getCaret(); + const position = model.positionForOffset(caret.offset, caret.atNodeEnd); + // index is -1 if there are no parts but we only care for if this would be the part in position 0 + const insertIndex = position.index > 0 ? position.index : 0; + const parts = partCreator.createMentionParts(insertIndex, displayName, userId); + model.transform(() => { + const addedLen = model.insert(parts, position); + return model.positionForOffset(caret.offset + addedLen, true); + }); + // refocus on composer, as we just clicked "Mention" + this.focus(); + } } diff --git a/src/components/views/rooms/EditMessageComposer.js b/src/components/views/rooms/EditMessageComposer.js index b006fe8c8d..1c2b3aed1c 100644 --- a/src/components/views/rooms/EditMessageComposer.js +++ b/src/components/views/rooms/EditMessageComposer.js @@ -120,6 +120,7 @@ export default class EditMessageComposer extends React.Component { saveDisabled: true, }; this._createEditorModel(); + this.dispatcherRef = dis.register(this.onAction); } _setEditorRef = ref => { @@ -235,6 +236,7 @@ export default class EditMessageComposer extends React.Component { // 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); + dis.unregister(this.dispatcherRef); } _createEditorModel() { @@ -278,6 +280,12 @@ export default class EditMessageComposer extends React.Component { }); }; + onAction = payload => { + if (payload.action === "insert_mention_edit_composer" && this._editorRef) { + this._editorRef.insertMention(payload.user_id); + } + }; + render() { const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); return (
      diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js index 75bc943146..aaeadffae1 100644 --- a/src/components/views/rooms/SendMessageComposer.js +++ b/src/components/views/rooms/SendMessageComposer.js @@ -482,8 +482,8 @@ export default class SendMessageComposer extends React.Component { case Action.FocusComposer: this._editorRef && this._editorRef.focus(); break; - case 'insert_mention': - this._insertMention(payload.user_id); + case 'insert_mention_send_composer': + this._editorRef && this._editorRef.insertMention(payload.user_id); break; case 'quote': this._insertQuotedMessage(payload.event); @@ -494,25 +494,6 @@ export default class SendMessageComposer extends React.Component { } }; - _insertMention(userId) { - const {model} = this; - const {partCreator} = model; - const member = this.props.room.getMember(userId); - const displayName = member ? - member.rawDisplayName : userId; - const caret = this._editorRef.getCaret(); - const position = model.positionForOffset(caret.offset, caret.atNodeEnd); - // index is -1 if there are no parts but we only care for if this would be the part in position 0 - const insertIndex = position.index > 0 ? position.index : 0; - const parts = partCreator.createMentionParts(insertIndex, displayName, userId); - model.transform(() => { - const addedLen = model.insert(parts, position); - return model.positionForOffset(caret.offset + addedLen, true); - }); - // refocus on composer, as we just clicked "Mention" - this._editorRef && this._editorRef.focus(); - } - _insertQuotedMessage(event) { const {model} = this; const {partCreator} = model; From 5f59e399582a958374a8bf7d634bb1ea19d77f95 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 13 Apr 2021 15:09:37 +0100 Subject: [PATCH 013/999] Apply the same to quoting & inserting of emoji then consolidate --- src/components/structures/TimelinePanel.js | 7 ++-- .../views/context_menus/MessageContextMenu.js | 2 +- src/components/views/messages/TextualBody.js | 4 +- src/components/views/right_panel/UserInfo.tsx | 4 +- .../views/rooms/BasicMessageComposer.tsx | 29 ++++++++++++- .../views/rooms/EditMessageComposer.js | 10 ++++- src/components/views/rooms/EventTile.js | 4 +- src/components/views/rooms/MessageComposer.js | 4 +- .../views/rooms/SendMessageComposer.js | 42 ++++--------------- src/dispatcher/actions.ts | 5 +++ .../payloads/ComposerInsertPayload.ts | 42 +++++++++++++++++++ src/editor/model.ts | 2 +- 12 files changed, 105 insertions(+), 50 deletions(-) create mode 100644 src/dispatcher/payloads/ComposerInsertPayload.ts diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 093e20b8b6..c7e252d667 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -467,16 +467,17 @@ class TimelinePanel extends React.Component { break; } - case "insert_mention": { + case "composer_insert": { + // re-dispatch to the correct composer if (this.state.editState) { dis.dispatch({ ...payload, - action: "insert_mention_edit_composer", + action: "edit_composer_insert", }); } else { dis.dispatch({ ...payload, - action: "insert_mention_send_composer", + action: "send_composer_insert", }); } break; diff --git a/src/components/views/context_menus/MessageContextMenu.js b/src/components/views/context_menus/MessageContextMenu.js index 56f070ba36..8c23906c68 100644 --- a/src/components/views/context_menus/MessageContextMenu.js +++ b/src/components/views/context_menus/MessageContextMenu.js @@ -233,7 +233,7 @@ export default class MessageContextMenu extends React.Component { onQuoteClick = () => { dis.dispatch({ - action: 'quote', + action: "composer_insert", event: this.props.mxEvent, }); this.closeMenu(); diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js index 353f40b6a9..83f6c80937 100644 --- a/src/components/views/messages/TextualBody.js +++ b/src/components/views/messages/TextualBody.js @@ -390,8 +390,8 @@ export default class TextualBody extends React.Component { onEmoteSenderClick = event => { const mxEvent = this.props.mxEvent; dis.dispatch({ - action: 'insert_mention', - user_id: mxEvent.getSender(), + action: "composer_insert", + userId: mxEvent.getSender(), }); }; diff --git a/src/components/views/right_panel/UserInfo.tsx b/src/components/views/right_panel/UserInfo.tsx index b862cc84d4..61eaa54639 100644 --- a/src/components/views/right_panel/UserInfo.tsx +++ b/src/components/views/right_panel/UserInfo.tsx @@ -360,8 +360,8 @@ const UserOptionsSection: React.FC<{ const onInsertPillButton = function() { dis.dispatch({ - action: 'insert_mention', - user_id: member.userId, + action: "composer_insert", + userId: member.userId, }); }; diff --git a/src/components/views/rooms/BasicMessageComposer.tsx b/src/components/views/rooms/BasicMessageComposer.tsx index b8ebde75cc..ebf97a1d09 100644 --- a/src/components/views/rooms/BasicMessageComposer.tsx +++ b/src/components/views/rooms/BasicMessageComposer.tsx @@ -18,6 +18,7 @@ limitations under the License. import classNames from 'classnames'; import React, {createRef, ClipboardEvent} from 'react'; import {Room} from 'matrix-js-sdk/src/models/room'; +import {MatrixEvent} from 'matrix-js-sdk/src/models/event'; import EMOTICON_REGEX from 'emojibase-regex/emoticon'; import EditorModel from '../../../editor/model'; @@ -32,7 +33,7 @@ import { import {getCaretOffsetAndText, getRangeForSelection} from '../../../editor/dom'; import Autocomplete, {generateCompletionDomId} from '../rooms/Autocomplete'; import {getAutoCompleteCreator} from '../../../editor/parts'; -import {parsePlainTextMessage} from '../../../editor/deserialize'; +import {parseEvent, parsePlainTextMessage} from '../../../editor/deserialize'; import {renderModel} from '../../../editor/render'; import TypingStore from "../../../stores/TypingStore"; import SettingsStore from "../../../settings/SettingsStore"; @@ -732,4 +733,30 @@ export default class BasicMessageEditor extends React.Component // refocus on composer, as we just clicked "Mention" this.focus(); } + + public insertQuotedMessage(event: MatrixEvent) { + const {model} = this.props; + const {partCreator} = model; + const quoteParts = parseEvent(event, partCreator, {isQuotedMessage: true}); + // add two newlines + quoteParts.push(partCreator.newline()); + quoteParts.push(partCreator.newline()); + model.transform(() => { + const addedLen = model.insert(quoteParts, model.positionForOffset(0)); + return model.positionForOffset(addedLen, true); + }); + // refocus on composer, as we just clicked "Quote" + this.focus(); + } + + public insertPlaintext(text: string) { + const {model} = this.props; + const {partCreator} = model; + const caret = this.getCaret(); + const position = model.positionForOffset(caret.offset, caret.atNodeEnd); + model.transform(() => { + const addedLen = model.insert([partCreator.plain(text)], position); + return model.positionForOffset(caret.offset + addedLen, true); + }); + } } diff --git a/src/components/views/rooms/EditMessageComposer.js b/src/components/views/rooms/EditMessageComposer.js index 1c2b3aed1c..39fafe486c 100644 --- a/src/components/views/rooms/EditMessageComposer.js +++ b/src/components/views/rooms/EditMessageComposer.js @@ -281,8 +281,14 @@ export default class EditMessageComposer extends React.Component { }; onAction = payload => { - if (payload.action === "insert_mention_edit_composer" && this._editorRef) { - this._editorRef.insertMention(payload.user_id); + if (payload.action === "edit_composer_insert" && this._editorRef) { + if (payload.user_id) { + this._editorRef.insertMention(payload.userId); + } else if (payload.event) { + this._editorRef.insertQuotedMessage(payload.event); + } else if (payload.text) { + this._editorRef.insertPlaintext(payload.text); + } } }; diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index d51f4c00f1..fe4fd95d24 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -644,8 +644,8 @@ export default class EventTile extends React.Component { onSenderProfileClick = event => { const mxEvent = this.props.mxEvent; dis.dispatch({ - action: 'insert_mention', - user_id: mxEvent.getSender(), + action: "composer_insert", + userId: mxEvent.getSender(), }); }; diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index b7078766fb..09b9fb4a36 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -312,8 +312,8 @@ export default class MessageComposer extends React.Component { addEmoji(emoji) { dis.dispatch({ - action: "insert_emoji", - emoji, + action: "composer_insert", + text: emoji, }); } diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js index aaeadffae1..ab6aac8be8 100644 --- a/src/components/views/rooms/SendMessageComposer.js +++ b/src/components/views/rooms/SendMessageComposer.js @@ -482,44 +482,18 @@ export default class SendMessageComposer extends React.Component { case Action.FocusComposer: this._editorRef && this._editorRef.focus(); break; - case 'insert_mention_send_composer': - this._editorRef && this._editorRef.insertMention(payload.user_id); - break; - case 'quote': - this._insertQuotedMessage(payload.event); - break; - case 'insert_emoji': - this._insertEmoji(payload.emoji); + case "send_composer_insert": + if (payload.userId) { + this._editorRef && this._editorRef.insertMention(payload.userId); + } else if (payload.event) { + this._editorRef && this._editorRef.insertQuotedMessage(payload.event); + } else if (payload.text) { + this._editorRef && this._editorRef.insertPlaintext(payload.emoji); + } break; } }; - _insertQuotedMessage(event) { - const {model} = this; - const {partCreator} = model; - const quoteParts = parseEvent(event, partCreator, {isQuotedMessage: true}); - // add two newlines - quoteParts.push(partCreator.newline()); - quoteParts.push(partCreator.newline()); - model.transform(() => { - const addedLen = model.insert(quoteParts, model.positionForOffset(0)); - return model.positionForOffset(addedLen, true); - }); - // refocus on composer, as we just clicked "Quote" - this._editorRef && this._editorRef.focus(); - } - - _insertEmoji = (emoji) => { - const {model} = this; - const {partCreator} = model; - const caret = this._editorRef.getCaret(); - const position = model.positionForOffset(caret.offset, caret.atNodeEnd); - model.transform(() => { - const addedLen = model.insert([partCreator.plain(emoji)], position); - return model.positionForOffset(caret.offset + addedLen, true); - }); - }; - _onPaste = (event) => { const {clipboardData} = event; // Prioritize text on the clipboard over files as Office on macOS puts a bitmap diff --git a/src/dispatcher/actions.ts b/src/dispatcher/actions.ts index cd32c3743f..0a01e02b9a 100644 --- a/src/dispatcher/actions.ts +++ b/src/dispatcher/actions.ts @@ -138,4 +138,9 @@ export enum Action { * Fired when an upload is cancelled by the user. Should be used with UploadCanceledPayload. */ UploadCanceled = "upload_canceled", + + /** + * Inserts content into the active composer. Should be used with ComposerInsertPayload + */ + ComposerInsert = "composer_insert", } diff --git a/src/dispatcher/payloads/ComposerInsertPayload.ts b/src/dispatcher/payloads/ComposerInsertPayload.ts new file mode 100644 index 0000000000..9702855432 --- /dev/null +++ b/src/dispatcher/payloads/ComposerInsertPayload.ts @@ -0,0 +1,42 @@ +/* +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 { MatrixEvent } from "matrix-js-sdk/src/models/event"; + +import { ActionPayload } from "../payloads"; +import { Action } from "../actions"; + +interface IBaseComposerInsertPayload extends ActionPayload { + action: Action.ComposerInsert, +} + +interface IComposerInsertMentionPayload extends IBaseComposerInsertPayload { + userId: string; +} + +interface IComposerInsertQuotePayload extends IBaseComposerInsertPayload { + event: MatrixEvent; +} + +interface IComposerInsertPlaintextPayload extends IBaseComposerInsertPayload { + text: string; +} + +export type ComposerInsertPayload = + IComposerInsertMentionPayload | + IComposerInsertQuotePayload | + IComposerInsertPlaintextPayload; + diff --git a/src/editor/model.ts b/src/editor/model.ts index 2e70b872e9..8c4a2dbd0d 100644 --- a/src/editor/model.ts +++ b/src/editor/model.ts @@ -390,7 +390,7 @@ export default class EditorModel { return addLen; } - positionForOffset(totalOffset: number, atPartEnd: boolean) { + positionForOffset(totalOffset: number, atPartEnd = false) { let currentOffset = 0; const index = this._parts.findIndex(part => { const partLen = part.text.length; From 4af7935e35ebb78b128550931c9fe635a90547b4 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 13 Apr 2021 15:11:46 +0100 Subject: [PATCH 014/999] fix typos --- src/components/views/rooms/EditMessageComposer.js | 2 +- src/components/views/rooms/SendMessageComposer.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/EditMessageComposer.js b/src/components/views/rooms/EditMessageComposer.js index 39fafe486c..628e030ddb 100644 --- a/src/components/views/rooms/EditMessageComposer.js +++ b/src/components/views/rooms/EditMessageComposer.js @@ -282,7 +282,7 @@ export default class EditMessageComposer extends React.Component { onAction = payload => { if (payload.action === "edit_composer_insert" && this._editorRef) { - if (payload.user_id) { + if (payload.userId) { this._editorRef.insertMention(payload.userId); } else if (payload.event) { this._editorRef.insertQuotedMessage(payload.event); diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js index ab6aac8be8..14893a6bd2 100644 --- a/src/components/views/rooms/SendMessageComposer.js +++ b/src/components/views/rooms/SendMessageComposer.js @@ -488,7 +488,7 @@ export default class SendMessageComposer extends React.Component { } else if (payload.event) { this._editorRef && this._editorRef.insertQuotedMessage(payload.event); } else if (payload.text) { - this._editorRef && this._editorRef.insertPlaintext(payload.emoji); + this._editorRef && this._editorRef.insertPlaintext(payload.text); } break; } From 86b6fc3473279e56905a9190abe689f620bd3a92 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 13 Apr 2021 15:15:11 +0100 Subject: [PATCH 015/999] delint --- src/components/views/rooms/SendMessageComposer.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js index 14893a6bd2..efb3dd3754 100644 --- a/src/components/views/rooms/SendMessageComposer.js +++ b/src/components/views/rooms/SendMessageComposer.js @@ -30,7 +30,6 @@ import { import {CommandPartCreator} from '../../../editor/parts'; import BasicMessageComposer from "./BasicMessageComposer"; import ReplyThread from "../elements/ReplyThread"; -import {parseEvent} from '../../../editor/deserialize'; import {findEditableEvent} from '../../../utils/EventUtils'; import SendHistoryManager from "../../../SendHistoryManager"; import {CommandCategories, getCommand} from '../../../SlashCommands'; From 61a260cd405eacac4c52777bd048657a9fda3702 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 18 Apr 2021 15:40:56 +0200 Subject: [PATCH 016/999] Format mxid --- res/css/views/messages/_SenderProfile.scss | 12 ++++++ .../views/messages/SenderProfile.js | 43 ++++++++++++++----- 2 files changed, 45 insertions(+), 10 deletions(-) diff --git a/res/css/views/messages/_SenderProfile.scss b/res/css/views/messages/_SenderProfile.scss index 655cb39489..e3a38bad6d 100644 --- a/res/css/views/messages/_SenderProfile.scss +++ b/res/css/views/messages/_SenderProfile.scss @@ -15,6 +15,18 @@ limitations under the License. */ .mx_SenderProfile_name { + display: flex; + align-items: center; + gap: 5px; +} + +.mx_SenderProfile_displayName { font-weight: 600; } +.mx_SenderProfile_mxid { + font-weight: 600; + font-family: monospace; + font-size: 1rem; + color: gray; +} diff --git a/src/components/views/messages/SenderProfile.js b/src/components/views/messages/SenderProfile.js index bd10526799..264d4ed3de 100644 --- a/src/components/views/messages/SenderProfile.js +++ b/src/components/views/messages/SenderProfile.js @@ -89,7 +89,21 @@ export default class SenderProfile extends React.Component { render() { const {mxEvent} = this.props; const colorClass = getUserNameColorClass(mxEvent.getSender()); - const name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender(); + + let disambiguate; + let displayName; + let mxid; + + const sender = mxEvent.sender; + if (sender) { + disambiguate = sender.disambiguate; + displayName = sender.rawDisplayName; + mxid = sender.userId; + } else { + disambiguate = false; + displayName = mxEvent.getSender(); + mxid = mxEvent.getSender(); + } const {msgtype} = mxEvent.getContent(); if (msgtype === 'm.emote') { @@ -108,20 +122,29 @@ export default class SenderProfile extends React.Component { />; } - const nameElem = name || ''; - - // Name + flair - const nameFlair = - - { nameElem } + const displayNameElement = ( + + { displayName || '' } - { flair } - ; + ); + + let mxidElement; + if (disambiguate) { + mxidElement = ( + + { `[${mxid || ""}]` } + + ); + } return (
      - { nameFlair } +
      + { displayNameElement } + { mxidElement } + { flair } +
      ); From 39eb487f49718453ac152d97119f764527a67821 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 20 Apr 2021 11:09:03 +0200 Subject: [PATCH 017/999] TS conversion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- .../{SenderProfile.js => SenderProfile.tsx} | 58 +++++++++---------- 1 file changed, 29 insertions(+), 29 deletions(-) rename src/components/views/messages/{SenderProfile.js => SenderProfile.tsx} (82%) diff --git a/src/components/views/messages/SenderProfile.js b/src/components/views/messages/SenderProfile.tsx similarity index 82% rename from src/components/views/messages/SenderProfile.js rename to src/components/views/messages/SenderProfile.tsx index 264d4ed3de..fa635eaedb 100644 --- a/src/components/views/messages/SenderProfile.js +++ b/src/components/views/messages/SenderProfile.tsx @@ -15,26 +15,37 @@ */ import React from 'react'; -import PropTypes from 'prop-types'; import Flair from '../elements/Flair.js'; import FlairStore from '../../../stores/FlairStore'; import {getUserNameColorClass} from '../../../utils/FormattingUtils'; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import MatrixEvent from "matrix-js-sdk/src/models/event"; + +interface IProps { + mxEvent: MatrixEvent; + onClick(): void; + enableFlair: boolean; +} + +interface IState { + userGroups; + relatedGroups; +} @replaceableComponent("views.messages.SenderProfile") -export default class SenderProfile extends React.Component { - static propTypes = { - mxEvent: PropTypes.object.isRequired, // event whose sender we're showing - onClick: PropTypes.func, - }; - +export default class SenderProfile extends React.Component { static contextType = MatrixClientContext; + unmounted: boolean; - state = { - userGroups: null, - relatedGroups: [], - }; + constructor(props: IProps) { + super(props) + + this.state = { + userGroups: null, + relatedGroups: [], + }; + } componentDidMount() { this.unmounted = false; @@ -89,28 +100,17 @@ export default class SenderProfile extends React.Component { render() { const {mxEvent} = this.props; const colorClass = getUserNameColorClass(mxEvent.getSender()); - - let disambiguate; - let displayName; - let mxid; - - const sender = mxEvent.sender; - if (sender) { - disambiguate = sender.disambiguate; - displayName = sender.rawDisplayName; - mxid = sender.userId; - } else { - disambiguate = false; - displayName = mxEvent.getSender(); - mxid = mxEvent.getSender(); - } const {msgtype} = mxEvent.getContent(); + const disambiguate = mxEvent.sender?.disambiguate; + const displayName = mxEvent.sender?.rawDisplayName || mxEvent.getSender() || ""; + const mxid = mxEvent.sender?.userId || mxEvent.getSender() || ""; + if (msgtype === 'm.emote') { return ; // emote message must include the name so don't duplicate it } - let flair =
      ; + let flair; if (this.props.enableFlair) { const displayedGroups = this._getDisplayedGroups( this.state.userGroups, this.state.relatedGroups, @@ -124,7 +124,7 @@ export default class SenderProfile extends React.Component { const displayNameElement = ( - { displayName || '' } + { displayName } ); @@ -132,7 +132,7 @@ export default class SenderProfile extends React.Component { if (disambiguate) { mxidElement = ( - { `[${mxid || ""}]` } + { `[${mxid}]` } ); } From faca498523460e59215bbe819521fcda01069729 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 20 Apr 2021 11:12:16 +0200 Subject: [PATCH 018/999] Don't use gap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/messages/_SenderProfile.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/views/messages/_SenderProfile.scss b/res/css/views/messages/_SenderProfile.scss index e3a38bad6d..d7763cda08 100644 --- a/res/css/views/messages/_SenderProfile.scss +++ b/res/css/views/messages/_SenderProfile.scss @@ -17,7 +17,6 @@ limitations under the License. .mx_SenderProfile_name { display: flex; align-items: center; - gap: 5px; } .mx_SenderProfile_displayName { @@ -29,4 +28,5 @@ limitations under the License. font-family: monospace; font-size: 1rem; color: gray; + margin-left: 5px; } From 4e1409dc2c7258816e51efb204a09ef608055117 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 20 Apr 2021 11:40:50 +0200 Subject: [PATCH 019/999] Add private Co-authored-by: Michael Telatynski <7t3chguy@googlemail.com> --- src/components/views/messages/SenderProfile.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/messages/SenderProfile.tsx b/src/components/views/messages/SenderProfile.tsx index fa635eaedb..f0dd77f746 100644 --- a/src/components/views/messages/SenderProfile.tsx +++ b/src/components/views/messages/SenderProfile.tsx @@ -36,7 +36,7 @@ interface IState { @replaceableComponent("views.messages.SenderProfile") export default class SenderProfile extends React.Component { static contextType = MatrixClientContext; - unmounted: boolean; + private unmounted: boolean; constructor(props: IProps) { super(props) From ffcd79f4a3215c1153e50ec599a8cd18b2d1e5a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 21 Apr 2021 17:34:03 +0200 Subject: [PATCH 020/999] Remove brackets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/messages/SenderProfile.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/messages/SenderProfile.tsx b/src/components/views/messages/SenderProfile.tsx index f0dd77f746..b2bfe00168 100644 --- a/src/components/views/messages/SenderProfile.tsx +++ b/src/components/views/messages/SenderProfile.tsx @@ -132,7 +132,7 @@ export default class SenderProfile extends React.Component { if (disambiguate) { mxidElement = ( - { `[${mxid}]` } + { mxid } ); } From eee1294374c568ee3e9c956b5fb3acaf537918a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 21 Apr 2021 17:37:25 +0200 Subject: [PATCH 021/999] Make both have the same baseline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/messages/_SenderProfile.scss | 5 ----- src/components/views/messages/SenderProfile.tsx | 8 +++----- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/res/css/views/messages/_SenderProfile.scss b/res/css/views/messages/_SenderProfile.scss index d7763cda08..14e8f6583f 100644 --- a/res/css/views/messages/_SenderProfile.scss +++ b/res/css/views/messages/_SenderProfile.scss @@ -14,11 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_SenderProfile_name { - display: flex; - align-items: center; -} - .mx_SenderProfile_displayName { font-weight: 600; } diff --git a/src/components/views/messages/SenderProfile.tsx b/src/components/views/messages/SenderProfile.tsx index b2bfe00168..70040e4ef7 100644 --- a/src/components/views/messages/SenderProfile.tsx +++ b/src/components/views/messages/SenderProfile.tsx @@ -140,11 +140,9 @@ export default class SenderProfile extends React.Component { return (
      -
      - { displayNameElement } - { mxidElement } - { flair } -
      + { displayNameElement } + { mxidElement } + { flair }
      ); From 9658a760c8bd96dd91498d064e7898c5a70fe19f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 21 Apr 2021 17:41:21 +0200 Subject: [PATCH 022/999] Use timestamp color MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/messages/_SenderProfile.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/views/messages/_SenderProfile.scss b/res/css/views/messages/_SenderProfile.scss index 14e8f6583f..378d9e6f1a 100644 --- a/res/css/views/messages/_SenderProfile.scss +++ b/res/css/views/messages/_SenderProfile.scss @@ -22,6 +22,6 @@ limitations under the License. font-weight: 600; font-family: monospace; font-size: 1rem; - color: gray; + color: $event-timestamp-color; margin-left: 5px; } From 55365e632b524015f3dca528e18d2c593861bf0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 22 Apr 2021 07:52:39 +0200 Subject: [PATCH 023/999] Use the correct selector in E2EE tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- test/end-to-end-tests/src/usecases/timeline.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/end-to-end-tests/src/usecases/timeline.js b/test/end-to-end-tests/src/usecases/timeline.js index 3889dce108..01dc618571 100644 --- a/test/end-to-end-tests/src/usecases/timeline.js +++ b/test/end-to-end-tests/src/usecases/timeline.js @@ -122,7 +122,7 @@ function getAllEventTiles(session) { } async function getMessageFromEventTile(eventTile) { - const senderElement = await eventTile.$(".mx_SenderProfile_name"); + const senderElement = await eventTile.$(".mx_SenderProfile_displayName"); const className = await (await eventTile.getProperty("className")).jsonValue(); const classNames = className.split(" "); const bodyElement = await eventTile.$(".mx_EventTile_body"); From 3201ed2f0fe0e378c741d57d9a147d79b267b842 Mon Sep 17 00:00:00 2001 From: Ayush PS Date: Mon, 26 Apr 2021 01:40:10 +0530 Subject: [PATCH 024/999] Added color scheme for the numbers --- res/css/views/voip/_DialPadContextMenu.scss | 2 +- res/themes/dark/css/_dark.scss | 3 ++- res/themes/legacy-dark/css/_legacy-dark.scss | 3 ++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/res/css/views/voip/_DialPadContextMenu.scss b/res/css/views/voip/_DialPadContextMenu.scss index c400060b6c..9879b7da1c 100644 --- a/res/css/views/voip/_DialPadContextMenu.scss +++ b/res/css/views/voip/_DialPadContextMenu.scss @@ -30,7 +30,7 @@ limitations under the License. height: 1.5em; font-size: 18px; font-weight: 600; - max-width: 150px; + max-width: 155px; overflow: auto; } diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index 42d592c1e1..b83bd52f76 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -114,7 +114,8 @@ $voipcall-plinth-color: #21262c; // ******************** $theme-button-bg-color: #e3e8f0; -$dialpad-button-bg-color: #545454; +$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 diff --git a/res/themes/legacy-dark/css/_legacy-dark.scss b/res/themes/legacy-dark/css/_legacy-dark.scss index ae98141d06..ff85375d35 100644 --- a/res/themes/legacy-dark/css/_legacy-dark.scss +++ b/res/themes/legacy-dark/css/_legacy-dark.scss @@ -111,7 +111,8 @@ $voipcall-plinth-color: #f2f5f8; // ******************** $theme-button-bg-color: #e3e8f0; -$dialpad-button-bg-color: #545454; +$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; From fa534e4755df403ee23f82ecd420b94c8c184d79 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Tue, 27 Apr 2021 14:47:56 +0100 Subject: [PATCH 025/999] Add room intro warning when e2ee is not enabled --- res/css/views/rooms/_NewRoomIntro.scss | 20 ++++++++++++++++++++ src/components/views/rooms/NewRoomIntro.tsx | 20 ++++++++++++++++++++ src/i18n/strings/en_EN.json | 1 + 3 files changed, 41 insertions(+) diff --git a/res/css/views/rooms/_NewRoomIntro.scss b/res/css/views/rooms/_NewRoomIntro.scss index 9c2a428cb3..e9d80c48c9 100644 --- a/res/css/views/rooms/_NewRoomIntro.scss +++ b/res/css/views/rooms/_NewRoomIntro.scss @@ -69,4 +69,24 @@ limitations under the License. font-size: $font-15px; color: $secondary-fg-color; } + + .mx_NewRoomIntro_message:not(:first-child) { + padding-top: 1em; + margin-top: 1em; + border-top: 1px solid currentColor; + } + + .mx_NewRoomIntro_message[role=alert]::before { + --size: 25px; + content: "!"; + float: left; + border-radius: 50%; + width: var(--size); + height: var(--size); + line-height: var(--size); + text-align: center; + background: $button-danger-bg-color; + color: #fff; + margin-right: calc(var(--size) / 2); + } } diff --git a/src/components/views/rooms/NewRoomIntro.tsx b/src/components/views/rooms/NewRoomIntro.tsx index 3f6054304d..9b003ade89 100644 --- a/src/components/views/rooms/NewRoomIntro.tsx +++ b/src/components/views/rooms/NewRoomIntro.tsx @@ -31,6 +31,14 @@ import dis from "../../../dispatcher/dispatcher"; import SpaceStore from "../../../stores/SpaceStore"; import {showSpaceInvite} from "../../../utils/space"; +import { privateShouldBeEncrypted } from "../../../createRoom"; + +function hasExpectedEncryptionSettings(room): boolean { + const isEncrypted: boolean = room._client.isRoomEncrypted(room.roomId); + const isPublic: boolean = room.getJoinRule() === "public"; + return isPublic || !privateShouldBeEncrypted() || isEncrypted; +} + const NewRoomIntro = () => { const cli = useContext(MatrixClientContext); const {room, roomId} = useContext(RoomContext); @@ -168,6 +176,18 @@ const NewRoomIntro = () => { return
      { body } + + { !hasExpectedEncryptionSettings(room) &&

      + {_t("Messages in this room are not end-to-end encrypted. " + + "Messages sent in an unencrypted room can be seen by the server and third parties. " + + "Learn more about encryption.", {}, + { + a: sub => {sub}, + })} + +

      }
      ; }; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index c6f7a8d25e..7ea9227938 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1490,6 +1490,7 @@ "Invite to just this room": "Invite to just this room", "Add a photo, so people can easily spot your room.": "Add a photo, so people can easily spot your room.", "This is the start of .": "This is the start of .", + "Messages in this room are not end-to-end encrypted. Messages sent in an unencrypted room can be seen by the server and third parties. Learn more about encryption.": "Messages in this room are not end-to-end encrypted. Messages sent in an unencrypted room can be seen by the server and third parties. Learn more about encryption.", "No pinned messages.": "No pinned messages.", "Loading...": "Loading...", "Pinned Messages": "Pinned Messages", From 7509481bb9339e038027efa6564df9746b73518a Mon Sep 17 00:00:00 2001 From: Ayush PS Date: Wed, 28 Apr 2021 02:46:43 +0530 Subject: [PATCH 026/999] Added the LTR support for the dialpad --- res/css/views/voip/_DialPadContextMenu.scss | 14 ++++++++++++-- .../views/context_menus/DialpadContextMenu.tsx | 13 ++++++++++++- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/res/css/views/voip/_DialPadContextMenu.scss b/res/css/views/voip/_DialPadContextMenu.scss index 9879b7da1c..c01ce0f2d9 100644 --- a/res/css/views/voip/_DialPadContextMenu.scss +++ b/res/css/views/voip/_DialPadContextMenu.scss @@ -30,8 +30,18 @@ limitations under the License. height: 1.5em; font-size: 18px; font-weight: 600; - max-width: 155px; - overflow: auto; + max-width: 150px; + border: none; + margin: 0px; + +} +.mx_DialPadContextMenu_dialled input{ + font-size: 18px; + font-weight: 600; + overflow: hidden; + text-align: left; + direction: rtl; + background-color: rgb(0, 0, 0, 0); } .mx_DialPadContextMenu_dialPad { diff --git a/src/components/views/context_menus/DialpadContextMenu.tsx b/src/components/views/context_menus/DialpadContextMenu.tsx index 17abce0c61..0a1d8184f2 100644 --- a/src/components/views/context_menus/DialpadContextMenu.tsx +++ b/src/components/views/context_menus/DialpadContextMenu.tsx @@ -18,6 +18,7 @@ import React from 'react'; import { _t } from '../../../languageHandler'; import { ContextMenu, IProps as IContextMenuProps } from '../../structures/ContextMenu'; import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call'; +import Field from "../elements/Field"; import Dialpad from '../voip/DialPad'; import {replaceableComponent} from "../../../utils/replaceableComponent"; @@ -44,13 +45,23 @@ export default class DialpadContextMenu extends React.Component this.setState({value: this.state.value + digit}); } + onChange = (ev) => { + this.setState({value: ev.target.value}); + } + + render() { return
      {_t("Dial pad")}
      -
      {this.state.value}
      +
      + +
      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 027/999] 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 028/999] 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 029/999] 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 030/999] 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 031/999] 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 032/999] 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 033/999] 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 034/999] 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 035/999] 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 036/999] 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 b42872daa409d27ad4634d2418fee2bfa7dccfea Mon Sep 17 00:00:00 2001 From: Ayush PS Date: Sun, 2 May 2021 22:10:15 +0530 Subject: [PATCH 037/999] Fixed the Dial Pad and finalized the changes --- res/css/views/voip/_DialPadContextMenu.scss | 5 +++-- .../views/context_menus/DialpadContextMenu.tsx | 12 +++++------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/res/css/views/voip/_DialPadContextMenu.scss b/res/css/views/voip/_DialPadContextMenu.scss index c01ce0f2d9..31327113cf 100644 --- a/res/css/views/voip/_DialPadContextMenu.scss +++ b/res/css/views/voip/_DialPadContextMenu.scss @@ -33,14 +33,15 @@ limitations under the License. max-width: 150px; border: none; margin: 0px; - } -.mx_DialPadContextMenu_dialled input{ +.mx_DialPadContextMenu_dialled input { font-size: 18px; font-weight: 600; overflow: hidden; + max-width: 150px; text-align: left; direction: rtl; + padding: 8px 0px; background-color: rgb(0, 0, 0, 0); } diff --git a/src/components/views/context_menus/DialpadContextMenu.tsx b/src/components/views/context_menus/DialpadContextMenu.tsx index 0a1d8184f2..8879629055 100644 --- a/src/components/views/context_menus/DialpadContextMenu.tsx +++ b/src/components/views/context_menus/DialpadContextMenu.tsx @@ -48,7 +48,7 @@ export default class DialpadContextMenu extends React.Component onChange = (ev) => { this.setState({value: ev.target.value}); } - + render() { return @@ -56,12 +56,10 @@ export default class DialpadContextMenu extends React.Component
      {_t("Dial pad")}
      -
      - - +
      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 038/999] 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 039/999] 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 040/999] 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 041/999] 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 042/999] 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 24bf1b22a440ef810fac33b975015c65d1a3f03d Mon Sep 17 00:00:00 2001 From: Ayush PS Date: Mon, 3 May 2021 13:54:42 +0530 Subject: [PATCH 043/999] Removes the override on the Bubble Container --- res/css/views/rooms/_EventTile.scss | 2 -- 1 file changed, 2 deletions(-) diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 2b3e179c54..5e110f9c6d 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -123,8 +123,6 @@ $left-gutter: 64px; .mx_EventTile_line { margin-right: 0; grid-column: 1 / 3; - // override default padding of mx_EventTile_line so that we can be centered - padding: 0 !important; } .mx_EventTile_msgOption { 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 044/999] 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 045/999] 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 046/999] 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 047/999] 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 048/999] 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 049/999] 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 050/999] 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 051/999] 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 052/999] 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 053/999] 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 054/999] 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 055/999] 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 056/999] 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 057/999] 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 058/999] 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 f367d617c506325237b4f56b76a97819e5f715f8 Mon Sep 17 00:00:00 2001 From: Ayush PS Date: Fri, 7 May 2021 14:37:28 +0530 Subject: [PATCH 059/999] Added the extra classes --- res/css/views/rooms/_EventTile.scss | 7 +++++++ src/components/views/rooms/EventTile.js | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 5e110f9c6d..35a5d8fc15 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -123,11 +123,18 @@ $left-gutter: 64px; .mx_EventTile_line { margin-right: 0; grid-column: 1 / 3; + // override default padding of mx_EventTile_line so that we can be centered + padding: 0 !important; } .mx_EventTile_msgOption { grid-column: 2; } + + .hidden { + // override the overriden padding for hidden events + padding-left: 64px !important; + } } .mx_EventTile_reply { diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index f6fb83c064..9e8f66705c 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -1101,7 +1101,7 @@ export default class EventTile extends React.Component { { ircTimestamp } { sender } { ircPadlock } -
      +
      { groupTimestamp } { groupPadlock } { thread } From c84338704393d0598eaa30a29fda21ec52a950de Mon Sep 17 00:00:00 2001 From: Ayush PS Date: Fri, 7 May 2021 18:32:38 +0530 Subject: [PATCH 060/999] Merge branch 'develop' into Bubble-bericht --- .eslintrc.js | 1 - .gitignore | 1 + CHANGELOG.md | 118 +++ README.md | 2 +- package.json | 19 +- res/css/_components.scss | 3 + res/css/structures/_RoomStatusBar.scss | 97 ++- res/css/structures/_SpacePanel.scss | 33 +- res/css/structures/_SpaceRoomDirectory.scss | 108 +-- res/css/structures/_SpaceRoomView.scss | 108 +-- .../dialogs/_AddExistingToSpaceDialog.scss | 195 +++-- res/css/views/elements/_AccessibleButton.scss | 8 +- res/css/views/elements/_ImageView.scss | 3 +- res/css/views/elements/_ProgressBar.scss | 2 +- res/css/views/messages/_MFileBody.scss | 8 +- .../views/messages/_MVoiceMessageBody.scss | 19 + res/css/views/messages/_MessageActionBar.scss | 8 + .../views/messages/_ReactionsRowButton.scss | 4 + res/css/views/rooms/_EventTile.scss | 4 - .../views/rooms/_VoiceRecordComposerTile.scss | 62 +- .../tabs/user/_HelpUserSettingsTab.scss | 31 + .../voice_messages/_PlayPauseButton.scss | 51 ++ .../voice_messages/_PlaybackContainer.scss | 53 ++ res/css/views/voip/_CallView.scss | 16 +- res/css/views/voip/_VideoFeed.scss | 24 +- res/img/element-icons/pause.svg | 4 + res/img/element-icons/play.svg | 3 + res/img/element-icons/retry.svg | 3 + res/img/element-icons/trashcan.svg | 3 + res/themes/dark/css/_dark.scss | 20 +- res/themes/legacy-dark/css/_legacy-dark.scss | 20 +- .../legacy-light/css/_legacy-light.scss | 29 +- res/themes/light/css/_light.scss | 31 +- scripts/compare-file.js | 10 - scripts/gen-i18n.js | 304 -------- scripts/prune-i18n.js | 68 -- src/@types/global.d.ts | 22 +- src/Avatar.ts | 15 +- src/BasePlatform.ts | 2 +- src/CallHandler.tsx | 119 +-- src/CallMediaHandler.js | 5 +- src/GroupAddressPicker.js | 18 +- src/IdentityAuthClient.js | 2 +- src/KeyBindingsManager.ts | 6 +- src/Login.ts | 7 +- src/PasswordReset.js | 2 +- src/Resend.js | 10 +- ...calarAuthClient.js => ScalarAuthClient.ts} | 64 +- src/SlashCommands.tsx | 4 +- src/{Terms.js => Terms.ts} | 46 +- src/TextForEvent.js | 18 +- src/Unread.js | 2 +- src/VoipUserMapper.ts | 6 +- ...exDialog.js => ManageEventIndexDialog.tsx} | 34 +- .../dialogs/security/CreateKeyBackupDialog.js | 8 +- .../security/CreateSecretStorageDialog.js | 10 +- .../dialogs/security/ExportE2eKeysDialog.js | 7 +- .../dialogs/security/ImportE2eKeysDialog.js | 56 +- src/components/structures/FilePanel.js | 8 +- src/components/structures/GroupFilterPanel.js | 20 +- src/components/structures/GroupView.js | 95 +-- src/components/structures/LeftPanel.tsx | 4 +- src/components/structures/LoggedInView.tsx | 20 + src/components/structures/MatrixChat.tsx | 4 +- src/components/structures/MessagePanel.js | 26 +- src/components/structures/RightPanel.js | 5 +- src/components/structures/RoomSearch.tsx | 21 +- src/components/structures/RoomStatusBar.js | 155 ++-- src/components/structures/RoomView.tsx | 12 +- src/components/structures/ScrollPanel.js | 26 +- .../structures/SpaceRoomDirectory.tsx | 190 +++-- src/components/structures/SpaceRoomView.tsx | 189 ++++- src/components/structures/TimelinePanel.js | 20 +- src/components/structures/auth/Login.tsx | 4 +- .../structures/auth/Registration.tsx | 4 +- .../auth/{SoftLogout.js => SoftLogout.tsx} | 49 +- .../auth/InteractiveAuthEntryComponents.js | 4 +- src/components/views/avatars/MemberAvatar.tsx | 4 +- src/components/views/avatars/RoomAvatar.tsx | 11 +- .../views/context_menus/MessageContextMenu.js | 93 +-- .../dialogs/AddExistingToSpaceDialog.tsx | 310 +++++--- .../views/dialogs/BugReportDialog.js | 2 +- .../views/dialogs/ChangelogDialog.js | 2 +- .../views/dialogs/ConfirmWipeDeviceDialog.js | 9 +- .../views/dialogs/DevtoolsDialog.js | 94 +-- .../views/dialogs/IncomingSasDialog.js | 2 +- .../dialogs/IntegrationsDisabledDialog.js | 9 +- .../dialogs/IntegrationsImpossibleDialog.js | 9 +- src/components/views/dialogs/InviteDialog.tsx | 2 +- .../dialogs/KeySignatureUploadFailedDialog.js | 10 +- .../views/dialogs/MessageEditHistoryDialog.js | 8 +- .../views/dialogs/RoomSettingsDialog.js | 8 +- .../views/dialogs/ServerPickerDialog.tsx | 6 +- .../dialogs/SessionRestoreErrorDialog.js | 2 +- .../views/dialogs/StorageEvictedDialog.js | 10 +- .../views/dialogs/UserSettingsDialog.js | 8 +- .../dialogs/VerificationRequestDialog.js | 12 +- .../dialogs/WidgetOpenIDPermissionsDialog.js | 9 +- .../ConfirmDestroyCrossSigningDialog.js | 9 +- .../security/RestoreKeyBackupDialog.js | 59 +- src/components/views/elements/ActionButton.js | 4 +- src/components/views/elements/AppTile.js | 2 +- .../views/elements/EditableItemList.js | 26 +- src/components/views/elements/EditableText.js | 16 +- src/components/views/elements/ImageView.tsx | 197 +++-- .../views/elements/LabelledToggleSwitch.js | 8 +- .../views/elements/LanguageDropdown.js | 6 +- src/components/views/elements/Pill.js | 24 +- .../views/elements/PowerSelector.js | 15 +- .../views/elements/RoomAliasField.js | 23 +- .../views/elements/ServerPicker.tsx | 4 +- src/components/views/elements/TintableSvg.js | 14 +- .../views/groups/GroupMemberList.js | 12 +- .../views/groups/GroupPublicityToggle.js | 6 +- src/components/views/groups/GroupRoomList.js | 13 +- .../views/messages/EditHistoryMessage.js | 1 - src/components/views/messages/MImageBody.js | 35 +- .../messages/MKeyVerificationConclusion.js | 4 +- .../views/messages/MVoiceMessageBody.tsx | 106 +++ .../views/messages/MVoiceOrAudioBody.tsx | 39 + .../views/messages/MessageActionBar.js | 125 ++- src/components/views/messages/MessageEvent.js | 6 +- .../views/messages/ReactionsRowButton.js | 3 +- src/components/views/messages/TextualBody.js | 11 +- src/components/views/right_panel/UserInfo.tsx | 18 +- .../views/room_settings/AliasSettings.js | 17 +- .../room_settings/RoomProfileSettings.js | 34 +- .../views/room_settings/UrlPreviewSettings.js | 14 +- src/components/views/rooms/EntityTile.js | 7 +- .../rooms/{EventTile.js => EventTile.tsx} | 461 ++++++----- .../views/rooms/LinkPreviewWidget.js | 4 +- src/components/views/rooms/MemberList.js | 5 +- ...MessageComposer.js => MessageComposer.tsx} | 160 ++-- .../views/rooms/NotificationBadge.tsx | 2 +- src/components/views/rooms/PinnedEventTile.js | 13 +- .../views/rooms/PinnedEventsPanel.js | 20 +- .../views/rooms/ReadReceiptMarker.js | 3 +- src/components/views/rooms/ReplyPreview.js | 9 +- src/components/views/rooms/RoomDetailRow.js | 8 +- src/components/views/rooms/RoomList.tsx | 8 +- .../views/rooms/RoomListNumResults.tsx | 6 +- src/components/views/rooms/RoomSublist.tsx | 8 +- src/components/views/rooms/RoomTile.tsx | 69 +- .../views/rooms/SendMessageComposer.js | 5 +- src/components/views/rooms/Stickerpicker.js | 38 +- ...MemberInfo.js => ThirdPartyMemberInfo.tsx} | 26 +- .../views/rooms/VoiceRecordComposerTile.tsx | 231 ++++-- src/components/views/settings/ChangeAvatar.js | 2 +- .../views/settings/ChangePassword.js | 4 +- .../views/settings/CrossSigningPanel.js | 2 +- src/components/views/settings/DevicesPanel.js | 2 +- ...eAdvancedPanel.js => E2eAdvancedPanel.tsx} | 0 ...EventIndexPanel.js => EventIndexPanel.tsx} | 140 ++-- .../views/settings/Notifications.js | 34 +- .../views/settings/ProfileSettings.js | 8 +- .../{SetIdServer.js => SetIdServer.tsx} | 78 +- .../views/settings/account/EmailAddresses.js | 39 +- .../views/settings/account/PhoneNumbers.js | 21 +- .../tabs/room/GeneralRoomSettingsTab.js | 12 +- .../tabs/room/NotificationSettingsTab.js | 6 +- ...ettingsTab.js => RolesRoomSettingsTab.tsx} | 71 +- ...ingsTab.js => SecurityRoomSettingsTab.tsx} | 144 ++-- .../tabs/user/GeneralUserSettingsTab.js | 19 +- ...SettingsTab.js => HelpUserSettingsTab.tsx} | 159 ++-- ...tingsTab.js => MjolnirUserSettingsTab.tsx} | 54 +- ...sTab.js => PreferencesUserSettingsTab.tsx} | 64 +- .../tabs/user/SecurityUserSettingsTab.js | 21 +- .../tabs/user/VoiceUserSettingsTab.js | 12 +- .../views/spaces/SpaceTreeLevel.tsx | 6 +- .../verification/VerificationCancelled.js | 16 +- src/components/views/voice_messages/Clock.tsx | 10 +- .../voice_messages/LiveRecordingClock.tsx | 8 +- .../voice_messages/LiveRecordingWaveform.tsx | 8 +- .../views/voice_messages/PlayPauseButton.tsx | 61 ++ .../views/voice_messages/PlaybackClock.tsx | 71 ++ .../views/voice_messages/PlaybackWaveform.tsx | 68 ++ .../voice_messages/RecordingPlayback.tsx | 62 ++ .../views/voice_messages/Waveform.tsx | 17 +- src/components/views/voip/AudioFeed.tsx | 97 +++ .../views/voip/AudioFeedArrayForCall.tsx | 60 ++ src/components/views/voip/CallPreview.tsx | 24 +- src/components/views/voip/CallView.tsx | 162 +++- src/components/views/voip/CallViewForRoom.tsx | 16 +- src/components/views/voip/IncomingCallBox.tsx | 6 +- src/components/views/voip/VideoFeed.tsx | 125 ++- src/customisations/Media.ts | 7 + src/editor/autocomplete.ts | 6 +- src/editor/model.ts | 2 +- src/editor/parts.ts | 43 +- src/editor/serialize.ts | 8 +- src/i18n/strings/cs.json | 163 ++-- src/i18n/strings/da.json | 6 +- src/i18n/strings/de_DE.json | 140 ++-- src/i18n/strings/en_EN.json | 68 +- src/i18n/strings/en_US.json | 10 +- src/i18n/strings/eo.json | 32 +- src/i18n/strings/es.json | 25 +- src/i18n/strings/et.json | 25 +- src/i18n/strings/fa.json | 322 +++++++- src/i18n/strings/fr.json | 37 +- src/i18n/strings/gl.json | 25 +- src/i18n/strings/hu.json | 25 +- src/i18n/strings/it.json | 25 +- src/i18n/strings/nl.json | 25 +- src/i18n/strings/ru.json | 3 +- src/i18n/strings/sq.json | 24 +- src/i18n/strings/sv.json | 25 +- src/i18n/strings/tzm.json | 121 ++- src/i18n/strings/zh_Hant.json | 25 +- src/indexing/BaseEventIndexManager.ts | 6 +- src/indexing/EventIndex.js | 37 +- .../{EventIndexPeg.js => EventIndexPeg.ts} | 19 +- src/linkify-matrix.js | 2 +- src/mjolnir/{BanList.js => BanList.ts} | 0 src/mjolnir/{ListRule.js => ListRule.ts} | 0 src/mjolnir/{Mjolnir.js => Mjolnir.ts} | 0 src/settings/Settings.ts | 5 +- src/stores/BreadcrumbsStore.ts | 2 +- src/stores/CustomRoomTagStore.js | 4 +- src/stores/GroupFilterOrderStore.js | 8 +- src/stores/RoomViewStore.tsx | 19 +- src/stores/SpaceStore.tsx | 172 +++-- src/stores/{TypingStore.js => TypingStore.ts} | 24 +- src/stores/VoiceRecordingStore.ts | 2 + ...{WidgetEchoStore.js => WidgetEchoStore.ts} | 39 +- .../notifications/StaticNotificationState.ts | 2 + src/stores/room-list/RoomListStore.ts | 22 +- src/stores/room-list/algorithms/Algorithm.ts | 9 +- .../algorithms/tag-sorting/RecentAlgorithm.ts | 140 ++-- .../room-list/filters/VisibilityProvider.ts | 8 +- ...scoveryUtils.js => AutoDiscoveryUtils.tsx} | 32 +- src/utils/DMRoomMap.ts | 9 + src/utils/ErrorUtils.js | 6 - src/utils/{MatrixGlob.js => MatrixGlob.ts} | 0 src/utils/MegolmExportEncryption.js | 3 +- .../{StorageManager.js => StorageManager.ts} | 12 +- src/utils/{Timer.js => Timer.ts} | 70 +- src/utils/{TypeUtils.js => TypeUtils.ts} | 0 src/utils/arrays.ts | 26 +- ...ctor.js => ElementPermalinkConstructor.ts} | 24 +- ...Constructor.js => PermalinkConstructor.ts} | 0 .../{Permalinks.js => Permalinks.ts} | 167 ++-- ...tructor.js => SpecPermalinkConstructor.ts} | 0 src/utils/space.tsx | 1 + src/voice/Playback.ts | 147 ++++ src/voice/PlaybackClock.ts | 78 ++ src/voice/VoiceRecording.ts | 69 +- test/CallHandler-test.ts | 214 ++++++ test/ScalarAuthClient-test.js | 2 +- test/autocomplete/QueryMatcher-test.js | 2 +- .../dialogs/AccessSecretStorageDialog-test.js | 24 +- .../elements/MemberEventListSummary-test.js | 9 +- .../components/views/rooms/MemberList-test.js | 2 +- test/components/views/rooms/RoomList-test.js | 10 +- test/editor/deserialize-test.js | 2 +- test/end-to-end-tests/src/session.js | 8 +- test/end-to-end-tests/yarn.lock | 6 +- test/stores/SpaceStore-test.ts | 714 ++++++++++++++++++ test/test-utils.js | 32 +- test/utils/MegolmExportEncryption-test.js | 30 +- test/utils/ShieldUtils-test.js | 6 +- test/utils/arrays-test.ts | 33 + test/utils/test-utils.ts | 33 + yarn.lock | 113 ++- 264 files changed, 7383 insertions(+), 3268 deletions(-) create mode 100644 res/css/views/messages/_MVoiceMessageBody.scss create mode 100644 res/css/views/voice_messages/_PlayPauseButton.scss create mode 100644 res/css/views/voice_messages/_PlaybackContainer.scss create mode 100644 res/img/element-icons/pause.svg create mode 100644 res/img/element-icons/play.svg create mode 100644 res/img/element-icons/retry.svg create mode 100644 res/img/element-icons/trashcan.svg delete mode 100644 scripts/compare-file.js delete mode 100755 scripts/gen-i18n.js delete mode 100755 scripts/prune-i18n.js rename src/{ScalarAuthClient.js => ScalarAuthClient.ts} (84%) rename src/{Terms.js => Terms.ts} (87%) rename src/async-components/views/dialogs/eventindex/{ManageEventIndexDialog.js => ManageEventIndexDialog.tsx} (90%) rename src/components/structures/auth/{SoftLogout.js => SoftLogout.tsx} (92%) create mode 100644 src/components/views/messages/MVoiceMessageBody.tsx create mode 100644 src/components/views/messages/MVoiceOrAudioBody.tsx rename src/components/views/rooms/{EventTile.js => EventTile.tsx} (79%) rename src/components/views/rooms/{MessageComposer.js => MessageComposer.tsx} (80%) rename src/components/views/rooms/{ThirdPartyMemberInfo.js => ThirdPartyMemberInfo.tsx} (91%) rename src/components/views/settings/{E2eAdvancedPanel.js => E2eAdvancedPanel.tsx} (100%) rename src/components/views/settings/{EventIndexPanel.js => EventIndexPanel.tsx} (65%) rename src/components/views/settings/{SetIdServer.js => SetIdServer.tsx} (89%) rename src/components/views/settings/tabs/room/{RolesRoomSettingsTab.js => RolesRoomSettingsTab.tsx} (89%) rename src/components/views/settings/tabs/room/{SecurityRoomSettingsTab.js => SecurityRoomSettingsTab.tsx} (79%) rename src/components/views/settings/tabs/user/{HelpUserSettingsTab.js => HelpUserSettingsTab.tsx} (64%) rename src/components/views/settings/tabs/user/{MjolnirUserSettingsTab.js => MjolnirUserSettingsTab.tsx} (90%) rename src/components/views/settings/tabs/user/{PreferencesUserSettingsTab.js => PreferencesUserSettingsTab.tsx} (80%) create mode 100644 src/components/views/voice_messages/PlayPauseButton.tsx create mode 100644 src/components/views/voice_messages/PlaybackClock.tsx create mode 100644 src/components/views/voice_messages/PlaybackWaveform.tsx create mode 100644 src/components/views/voice_messages/RecordingPlayback.tsx create mode 100644 src/components/views/voip/AudioFeed.tsx create mode 100644 src/components/views/voip/AudioFeedArrayForCall.tsx rename src/indexing/{EventIndexPeg.js => EventIndexPeg.ts} (94%) rename src/mjolnir/{BanList.js => BanList.ts} (100%) rename src/mjolnir/{ListRule.js => ListRule.ts} (100%) rename src/mjolnir/{Mjolnir.js => Mjolnir.ts} (100%) rename src/stores/{TypingStore.js => TypingStore.ts} (84%) rename src/stores/{WidgetEchoStore.js => WidgetEchoStore.ts} (71%) rename src/utils/{AutoDiscoveryUtils.js => AutoDiscoveryUtils.tsx} (91%) rename src/utils/{MatrixGlob.js => MatrixGlob.ts} (100%) rename src/utils/{StorageManager.js => StorageManager.ts} (96%) rename src/utils/{Timer.js => Timer.ts} (60%) rename src/utils/{TypeUtils.js => TypeUtils.ts} (100%) rename src/utils/permalinks/{ElementPermalinkConstructor.js => ElementPermalinkConstructor.ts} (82%) rename src/utils/permalinks/{PermalinkConstructor.js => PermalinkConstructor.ts} (100%) rename src/utils/permalinks/{Permalinks.js => Permalinks.ts} (76%) rename src/utils/permalinks/{SpecPermalinkConstructor.js => SpecPermalinkConstructor.ts} (100%) create mode 100644 src/voice/Playback.ts create mode 100644 src/voice/PlaybackClock.ts create mode 100644 test/CallHandler-test.ts create mode 100644 test/stores/SpaceStore-test.ts create mode 100644 test/utils/test-utils.ts diff --git a/.eslintrc.js b/.eslintrc.js index 99695b7a03..4959b133a0 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -15,7 +15,6 @@ module.exports = { "prefer-promise-reject-errors": "off", "no-async-promise-executor": "off", "quotes": "off", - "indent": "off", }, overrides: [{ diff --git a/.gitignore b/.gitignore index e1dd7726e1..50aa10fbfd 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ /*.log package-lock.json +/coverage /node_modules /lib diff --git a/CHANGELOG.md b/CHANGELOG.md index ec73756ff9..d459b4e94a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,121 @@ +Changes in [3.19.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.19.0) (2021-04-26) +===================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.19.0-rc.1...v3.19.0) + + * Upgrade to JS SDK 10.0.0 + * [Release] Dynamic max and min zoom in the new ImageView + [\#5927](https://github.com/matrix-org/matrix-react-sdk/pull/5927) + * [Release] Add a WheelEvent normalization function + [\#5911](https://github.com/matrix-org/matrix-react-sdk/pull/5911) + * Add a WheelEvent normalization function + [\#5904](https://github.com/matrix-org/matrix-react-sdk/pull/5904) + * [Release] Use floats for image background opacity + [\#5907](https://github.com/matrix-org/matrix-react-sdk/pull/5907) + +Changes in [3.19.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.19.0-rc.1) (2021-04-21) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.18.0...v3.19.0-rc.1) + + * Upgrade to JS SDK 10.0.0-rc.1 + * Translations update from Weblate + [\#5896](https://github.com/matrix-org/matrix-react-sdk/pull/5896) + * Fix sticky tags header in room list + [\#5895](https://github.com/matrix-org/matrix-react-sdk/pull/5895) + * Fix spaces filtering sometimes lagging behind or behaving oddly + [\#5893](https://github.com/matrix-org/matrix-react-sdk/pull/5893) + * Fix issue with spaces context switching looping and breaking + [\#5894](https://github.com/matrix-org/matrix-react-sdk/pull/5894) + * Improve RoomList render time when filtering + [\#5874](https://github.com/matrix-org/matrix-react-sdk/pull/5874) + * Avoid being stuck in a space + [\#5891](https://github.com/matrix-org/matrix-react-sdk/pull/5891) + * [Spaces] Context switching + [\#5795](https://github.com/matrix-org/matrix-react-sdk/pull/5795) + * Warn when you attempt to leave room that you are the only member of + [\#5415](https://github.com/matrix-org/matrix-react-sdk/pull/5415) + * Ensure PersistedElement are unmounted on application logout + [\#5884](https://github.com/matrix-org/matrix-react-sdk/pull/5884) + * Add missing space in seshat dialog and the corresponding string + [\#5866](https://github.com/matrix-org/matrix-react-sdk/pull/5866) + * A tiny change to make the Add existing rooms dialog a little nicer + [\#5885](https://github.com/matrix-org/matrix-react-sdk/pull/5885) + * Remove weird margin from the file panel + [\#5889](https://github.com/matrix-org/matrix-react-sdk/pull/5889) + * Trigger lazy loading when filtering using spaces + [\#5882](https://github.com/matrix-org/matrix-react-sdk/pull/5882) + * Fix typo in method call in add existing to space dialog + [\#5883](https://github.com/matrix-org/matrix-react-sdk/pull/5883) + * New Image View fixes/improvements + [\#5872](https://github.com/matrix-org/matrix-react-sdk/pull/5872) + * Limit voice recording length + [\#5871](https://github.com/matrix-org/matrix-react-sdk/pull/5871) + * Clean up add existing to space dialog and include DMs in it too + [\#5881](https://github.com/matrix-org/matrix-react-sdk/pull/5881) + * Fix unknown slash command error exploding + [\#5853](https://github.com/matrix-org/matrix-react-sdk/pull/5853) + * Switch to a spec conforming email validation Regexp + [\#5852](https://github.com/matrix-org/matrix-react-sdk/pull/5852) + * Cleanup unused state in MessageComposer + [\#5877](https://github.com/matrix-org/matrix-react-sdk/pull/5877) + * Pulse animation for voice messages recording state + [\#5869](https://github.com/matrix-org/matrix-react-sdk/pull/5869) + * Don't include invisible rooms in notify summary + [\#5875](https://github.com/matrix-org/matrix-react-sdk/pull/5875) + * Properly disable composer access when recording a voice message + [\#5870](https://github.com/matrix-org/matrix-react-sdk/pull/5870) + * Stabilise starting a DM with multiple people flow + [\#5862](https://github.com/matrix-org/matrix-react-sdk/pull/5862) + * Render msgOption only if showReadReceipts is enabled + [\#5864](https://github.com/matrix-org/matrix-react-sdk/pull/5864) + * Labs: Add quick/cheap "do not disturb" flag + [\#5873](https://github.com/matrix-org/matrix-react-sdk/pull/5873) + * Fix ReadReceipts animations + [\#5836](https://github.com/matrix-org/matrix-react-sdk/pull/5836) + * Add tooltips to message previews + [\#5859](https://github.com/matrix-org/matrix-react-sdk/pull/5859) + * IRC Layout fix layout spacing in replies + [\#5855](https://github.com/matrix-org/matrix-react-sdk/pull/5855) + * Move user to welcome_page if continuing with previous session + [\#5849](https://github.com/matrix-org/matrix-react-sdk/pull/5849) + * Improve image view + [\#5521](https://github.com/matrix-org/matrix-react-sdk/pull/5521) + * Add a button to reset personal encryption state during login + [\#5819](https://github.com/matrix-org/matrix-react-sdk/pull/5819) + * Fix js-sdk import in SlashCommands + [\#5850](https://github.com/matrix-org/matrix-react-sdk/pull/5850) + * Fix useRoomPowerLevels hook + [\#5854](https://github.com/matrix-org/matrix-react-sdk/pull/5854) + * Prevent state events being rendered with invalid state keys + [\#5851](https://github.com/matrix-org/matrix-react-sdk/pull/5851) + * Give server ACLs a name in 'roles & permissions' tab + [\#5838](https://github.com/matrix-org/matrix-react-sdk/pull/5838) + * Don't hide notification badge on the home space button as it has no menu + [\#5845](https://github.com/matrix-org/matrix-react-sdk/pull/5845) + * User Info hide disambiguation as we always show MXID anyway + [\#5843](https://github.com/matrix-org/matrix-react-sdk/pull/5843) + * Improve kick state to not show if the target was not joined to begin with + [\#5846](https://github.com/matrix-org/matrix-react-sdk/pull/5846) + * Fix space store wrongly switching to a non-space filter + [\#5844](https://github.com/matrix-org/matrix-react-sdk/pull/5844) + * Tweak appearance of invite reason + [\#5847](https://github.com/matrix-org/matrix-react-sdk/pull/5847) + * Update Inter font to v3.18 + [\#5840](https://github.com/matrix-org/matrix-react-sdk/pull/5840) + * Enable sharing historical keys on invite + [\#5839](https://github.com/matrix-org/matrix-react-sdk/pull/5839) + * Add ability to hide post-login encryption setup with customisation point + [\#5834](https://github.com/matrix-org/matrix-react-sdk/pull/5834) + * Use LaTeX and TeX delimiters by default + [\#5515](https://github.com/matrix-org/matrix-react-sdk/pull/5515) + * Clone author's deps fork for Netlify previews + [\#5837](https://github.com/matrix-org/matrix-react-sdk/pull/5837) + * Show drop file UI only if dragging a file + [\#5827](https://github.com/matrix-org/matrix-react-sdk/pull/5827) + * Ignore punctuation when filtering rooms + [\#5824](https://github.com/matrix-org/matrix-react-sdk/pull/5824) + * Resizable CallView + [\#5710](https://github.com/matrix-org/matrix-react-sdk/pull/5710) + Changes in [3.18.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.18.0) (2021-04-12) ===================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.18.0-rc.1...v3.18.0) diff --git a/README.md b/README.md index 73afe34df0..b3e96ef001 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ Platform Targets: * WebRTC features (VoIP and Video calling) are only available in Chrome & Firefox. * Mobile Web is not currently a target platform - instead please use the native iOS (https://github.com/matrix-org/matrix-ios-kit) and Android - (https://github.com/matrix-org/matrix-android-sdk) SDKs. + (https://github.com/matrix-org/matrix-android-sdk2) SDKs. All code lands on the `develop` branch - `master` is only used for stable releases. **Please file PRs against `develop`!!** diff --git a/package.json b/package.json index 7c190c68bf..be195e2e9e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "3.18.0", + "version": "3.19.0", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { @@ -23,9 +23,7 @@ "package.json" ], "bin": { - "reskindex": "scripts/reskindex.js", - "matrix-gen-i18n": "scripts/gen-i18n.js", - "matrix-prune-i18n": "scripts/prune-i18n.js" + "reskindex": "scripts/reskindex.js" }, "main": "./src/index.js", "matrix_src_main": "./src/index.js", @@ -35,7 +33,7 @@ "prepublishOnly": "yarn build", "i18n": "matrix-gen-i18n", "prunei18n": "matrix-prune-i18n", - "diff-i18n": "cp src/i18n/strings/en_EN.json src/i18n/strings/en_EN_orig.json && ./scripts/gen-i18n.js && node scripts/compare-file.js src/i18n/strings/en_EN_orig.json src/i18n/strings/en_EN.json", + "diff-i18n": "cp src/i18n/strings/en_EN.json src/i18n/strings/en_EN_orig.json && matrix-gen-i18n && matrix-compare-i18n-files src/i18n/strings/en_EN_orig.json src/i18n/strings/en_EN.json", "reskindex": "node scripts/reskindex.js -h header", "reskindex:watch": "node scripts/reskindex.js -h header -w", "rethemendex": "res/css/rethemendex.sh", @@ -51,7 +49,8 @@ "lint:types": "tsc --noEmit --jsx react", "lint:style": "stylelint 'res/css/**/*.scss'", "test": "jest", - "test:e2e": "./test/end-to-end-tests/run.sh --app-url http://localhost:8080" + "test:e2e": "./test/end-to-end-tests/run.sh --app-url http://localhost:8080", + "coverage": "yarn test --coverage" }, "dependencies": { "@babel/runtime": "^7.12.5", @@ -133,6 +132,7 @@ "@types/modernizr": "^3.5.3", "@types/node": "^14.14.22", "@types/pako": "^1.0.1", + "@types/parse5": "^6.0.0", "@types/qrcode": "^1.3.5", "@types/react": "^16.9", "@types/react-dom": "^16.9.10", @@ -160,6 +160,7 @@ "jest-fetch-mock": "^3.0.3", "matrix-mock-request": "^1.2.3", "matrix-react-test-utils": "^0.2.2", + "matrix-web-i18n": "github:matrix-org/matrix-web-i18n", "olm": "https://packages.matrix.org/npm/olm/olm-3.2.1.tgz", "react-test-renderer": "^16.14.0", "rimraf": "^3.0.2", @@ -189,6 +190,12 @@ }, "transformIgnorePatterns": [ "/node_modules/(?!matrix-js-sdk).+$" + ], + "collectCoverageFrom": [ + "/src/**/*.{js,ts,tsx}" + ], + "coverageReporters": [ + "text" ] } } diff --git a/res/css/_components.scss b/res/css/_components.scss index 253f97bf42..ec9592f3a1 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -161,6 +161,7 @@ @import "./views/messages/_MStickerBody.scss"; @import "./views/messages/_MTextBody.scss"; @import "./views/messages/_MVideoBody.scss"; +@import "./views/messages/_MVoiceMessageBody.scss"; @import "./views/messages/_MessageActionBar.scss"; @import "./views/messages/_MessageTimestamp.scss"; @import "./views/messages/_MjolnirBody.scss"; @@ -248,6 +249,8 @@ @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"; diff --git a/res/css/structures/_RoomStatusBar.scss b/res/css/structures/_RoomStatusBar.scss index 5bf2aee3ae..8cc00aba0f 100644 --- a/res/css/structures/_RoomStatusBar.scss +++ b/res/css/structures/_RoomStatusBar.scss @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_RoomStatusBar { +.mx_RoomStatusBar:not(.mx_RoomStatusBar_unsentMessages) { margin-left: 65px; min-height: 50px; } @@ -68,6 +68,99 @@ limitations under the License. min-height: 58px; } +.mx_RoomStatusBar_unsentMessages { + > div[role="alert"] { + // cheat some basic alignment + display: flex; + align-items: center; + min-height: 70px; + margin: 12px; + padding-left: 16px; + background-color: $header-panel-bg-color; + border-radius: 4px; + } + + .mx_RoomStatusBar_unsentBadge { + margin-right: 12px; + + .mx_NotificationBadge { + // Override sizing from the default badge + width: 24px !important; + height: 24px !important; + border-radius: 24px !important; + + .mx_NotificationBadge_count { + font-size: $font-16px !important; // override default + } + } + } + + .mx_RoomStatusBar_unsentTitle { + color: $warning-color; + font-size: $font-15px; + } + + .mx_RoomStatusBar_unsentDescription { + font-size: $font-12px; + } + + .mx_RoomStatusBar_unsentButtonBar { + flex-grow: 1; + text-align: right; + margin-right: 22px; + color: $muted-fg-color; + + .mx_AccessibleButton { + padding: 5px 10px; + padding-left: 28px; // 16px for the icon, 2px margin to text, 10px regular padding + display: inline-block; + position: relative; + + &:nth-child(2) { + border-left: 1px solid $resend-button-divider-color; + } + + &::before { + content: ''; + position: absolute; + left: 10px; // inset for regular button padding + background-color: $muted-fg-color; + mask-repeat: no-repeat; + mask-position: center; + mask-size: contain; + } + + &.mx_RoomStatusBar_unsentCancelAllBtn::before { + mask-image: url('$(res)/img/element-icons/trashcan.svg'); + width: 12px; + height: 16px; + top: calc(50% - 8px); // text sizes are dynamic + } + + &.mx_RoomStatusBar_unsentResendAllBtn { + padding-left: 34px; // 28px from above, but +6px to account for the wider icon + + &::before { + mask-image: url('$(res)/img/element-icons/retry.svg'); + width: 18px; + height: 18px; + top: calc(50% - 9px); // text sizes are dynamic + } + } + } + + .mx_InlineSpinner { + vertical-align: middle; + margin-right: 5px; + top: 1px; // just to help the vertical alignment be slightly better + + & + span { + margin-right: 10px; // same margin/padding as the rightmost button + } + } + } +} + .mx_RoomStatusBar_connectionLostBar img { padding-left: 10px; padding-right: 10px; @@ -103,7 +196,7 @@ limitations under the License. } .mx_MatrixChat_useCompactLayout { - .mx_RoomStatusBar { + .mx_RoomStatusBar:not(.mx_RoomStatusBar_unsentMessages) { min-height: 40px; } diff --git a/res/css/structures/_SpacePanel.scss b/res/css/structures/_SpacePanel.scss index 59f2ea947c..c433ccf275 100644 --- a/res/css/structures/_SpacePanel.scss +++ b/res/css/structures/_SpacePanel.scss @@ -79,6 +79,10 @@ $activeBorderColor: $secondary-fg-color; .mx_SpaceItem { display: inline-flex; flex-flow: wrap; + + &.mx_SpaceItem_narrow { + align-self: baseline; + } } .mx_SpaceItem.collapsed { @@ -233,7 +237,6 @@ $activeBorderColor: $secondary-fg-color; .mx_SpacePanel_badgeContainer { position: absolute; - height: 16px; // Create a flexbox to make aligning dot badges easier display: flex; @@ -245,23 +248,37 @@ $activeBorderColor: $secondary-fg-color; .mx_NotificationBadge_dot { // make the smaller dot occupy the same width for centering - margin-left: 7px; - margin-right: 7px; + margin: 0 7px; } } &.collapsed { .mx_SpaceButton { .mx_SpacePanel_badgeContainer { - right: -3px; - top: -3px; + right: 0; + top: 0; + + .mx_NotificationBadge { + background-clip: padding-box; + } + + .mx_NotificationBadge_dot { + margin: 0 -1px 0 0; + border: 3px solid $groupFilterPanel-bg-color; + } + + .mx_NotificationBadge_2char, + .mx_NotificationBadge_3char { + margin: -5px -5px 0 0; + border: 2px solid $groupFilterPanel-bg-color; + } } &.mx_SpaceButton_active .mx_SpacePanel_badgeContainer { // when we draw the selection border we move the relative bounds of our parent // so update our position within the bounds of the parent to maintain position overall - right: -6px; - top: -6px; + right: -3px; + top: -3px; } } } @@ -275,7 +292,7 @@ $activeBorderColor: $secondary-fg-color; .mx_SpaceButton:hover, .mx_SpaceButton:focus-within, .mx_SpaceButton_hasMenuOpen { - &:not(.mx_SpaceButton_home) { + &:not(.mx_SpaceButton_home):not(.mx_SpaceButton_invite) { // Hide the badge container on hover because it'll be a menu button .mx_SpacePanel_badgeContainer { width: 0; diff --git a/res/css/structures/_SpaceRoomDirectory.scss b/res/css/structures/_SpaceRoomDirectory.scss index dcceee6371..7925686bf1 100644 --- a/res/css/structures/_SpaceRoomDirectory.scss +++ b/res/css/structures/_SpaceRoomDirectory.scss @@ -26,7 +26,10 @@ limitations under the License. word-break: break-word; display: flex; flex-direction: column; +} +.mx_SpaceRoomDirectory, +.mx_SpaceRoomView_landing { .mx_Dialog_title { display: flex; @@ -56,65 +59,68 @@ limitations under the License. } } - .mx_Dialog_content { - .mx_AccessibleButton_kind_link { - padding: 0; - } + .mx_AccessibleButton_kind_link { + padding: 0; + } - .mx_SearchBox { - margin: 24px 0 16px; - } + .mx_SearchBox { + margin: 24px 0 16px; + } - .mx_SpaceRoomDirectory_noResults { - text-align: center; + .mx_SpaceRoomDirectory_noResults { + text-align: center; - > div { - font-size: $font-15px; - line-height: $font-24px; - color: $secondary-fg-color; - } - } - - .mx_SpaceRoomDirectory_listHeader { - display: flex; - min-height: 32px; - align-items: center; + > div { font-size: $font-15px; line-height: $font-24px; - color: $primary-fg-color; + color: $secondary-fg-color; + } + } - .mx_AccessibleButton { - padding: 2px 8px; - font-weight: normal; + .mx_SpaceRoomDirectory_listHeader { + display: flex; + min-height: 32px; + align-items: center; + font-size: $font-15px; + line-height: $font-24px; + color: $primary-fg-color; - & + .mx_AccessibleButton { - margin-left: 16px; - } - } + .mx_AccessibleButton { + padding: 4px 12px; + font-weight: normal; - > span { - margin-left: auto; + & + .mx_AccessibleButton { + margin-left: 16px; } } - .mx_SpaceRoomDirectory_error { - position: relative; - font-weight: $font-semi-bold; - color: $notice-primary-color; - font-size: $font-15px; - line-height: $font-18px; - margin: 20px auto 12px; - padding-left: 24px; - width: max-content; + .mx_AccessibleButton_kind_danger_outline, + .mx_AccessibleButton_kind_primary_outline { + padding: 3px 12px; // to account for the 1px border + } - &::before { - content: ""; - position: absolute; - height: 16px; - width: 16px; - left: 0; - background-image: url("$(res)/img/element-icons/warning-badge.svg"); - } + > span { + margin-left: auto; + } + } + + .mx_SpaceRoomDirectory_error { + position: relative; + font-weight: $font-semi-bold; + color: $notice-primary-color; + font-size: $font-15px; + line-height: $font-18px; + margin: 20px auto 12px; + padding-left: 24px; + width: max-content; + + &::before { + content: ""; + position: absolute; + height: 16px; + width: 16px; + left: 0; + background-image: url("$(res)/img/element-icons/warning-badge.svg"); } } } @@ -245,11 +251,17 @@ limitations under the License. grid-row: 1/3; .mx_AccessibleButton { - padding: 8px 18px; + line-height: $font-24px; + padding: 4px 16px; display: inline-block; visibility: hidden; } + .mx_AccessibleButton_kind_danger_outline, + .mx_AccessibleButton_kind_primary_outline { + padding: 3px 16px; // to account for the 1px border + } + .mx_Checkbox { display: inline-flex; vertical-align: middle; diff --git a/res/css/structures/_SpaceRoomView.scss b/res/css/structures/_SpaceRoomView.scss index 269f16beb7..ff51e28b7b 100644 --- a/res/css/structures/_SpaceRoomView.scss +++ b/res/css/structures/_SpaceRoomView.scss @@ -81,6 +81,16 @@ $SpaceRoomViewInnerWidth: 428px; color: $secondary-fg-color; margin-top: 12px; margin-bottom: 24px; + max-width: $SpaceRoomViewInnerWidth; + } + + .mx_AddExistingToSpace { + max-width: $SpaceRoomViewInnerWidth; + + .mx_AddExistingToSpace_content { + height: calc(100vh - 360px); + max-height: 400px; + } } .mx_SpaceRoomView_buttons { @@ -228,7 +238,8 @@ $SpaceRoomViewInnerWidth: 428px; .mx_SpaceRoomView_landing_inviteButton { position: relative; - padding-left: 40px; + padding: 4px 18px 4px 40px; + line-height: $font-24px; height: min-content; &::before { @@ -244,6 +255,27 @@ $SpaceRoomViewInnerWidth: 428px; mask-image: url('$(res)/img/element-icons/room/invite.svg'); } } + + .mx_SpaceRoomView_landing_settingsButton { + position: relative; + margin-left: 16px; + width: 24px; + height: 24px; + + &::before { + position: absolute; + content: ""; + left: 0; + top: 0; + height: 24px; + width: 24px; + background: $tertiary-fg-color; + mask-position: center; + mask-size: contain; + mask-repeat: no-repeat; + mask-image: url('$(res)/img/element-icons/settings.svg'); + } + } } .mx_SpaceRoomView_landing_topic { @@ -258,80 +290,6 @@ $SpaceRoomViewInnerWidth: 428px; background-color: $groupFilterPanel-bg-color; } - .mx_SpaceRoomView_landing_adminButtons { - margin-top: 24px; - - .mx_AccessibleButton { - position: relative; - width: 160px; - height: 124px; - box-sizing: border-box; - padding: 72px 16px 0; - border-radius: 12px; - border: 1px solid $input-border-color; - margin-right: 28px; - margin-bottom: 20px; - font-size: $font-14px; - display: inline-block; - vertical-align: bottom; - - &:last-child { - margin-right: 0; - } - - &:hover { - background-color: rgba(141, 151, 165, 0.1); - } - - &::before, &::after { - position: absolute; - content: ""; - left: 16px; - top: 16px; - height: 40px; - width: 40px; - border-radius: 20px; - } - - &::after { - mask-position: center; - mask-size: 30px; - mask-repeat: no-repeat; - background: #ffffff; // white icon fill - } - - &.mx_SpaceRoomView_landing_addButton { - &::before { - background-color: #ac3ba8; - } - - &::after { - mask-image: url('$(res)/img/element-icons/roomlist/explore.svg'); - } - } - - &.mx_SpaceRoomView_landing_createButton { - &::before { - background-color: #368bd6; - } - - &::after { - mask-image: url('$(res)/img/element-icons/roomlist/explore.svg'); - } - } - - &.mx_SpaceRoomView_landing_settingsButton { - &::before { - background-color: #5c56f5; - } - - &::after { - mask-image: url('$(res)/img/element-icons/settings.svg'); - } - } - } - } - .mx_SearchBox { margin: 0 0 20px; } diff --git a/res/css/views/dialogs/_AddExistingToSpaceDialog.scss b/res/css/views/dialogs/_AddExistingToSpaceDialog.scss index 247df52b4a..524f107165 100644 --- a/res/css/views/dialogs/_AddExistingToSpaceDialog.scss +++ b/res/css/views/dialogs/_AddExistingToSpaceDialog.scss @@ -21,6 +21,66 @@ limitations under the License. } } +.mx_AddExistingToSpace { + .mx_SearchBox { + // To match the space around the title + margin: 0 0 15px 0; + flex-grow: 0; + } + + .mx_AddExistingToSpace_content { + flex-grow: 1; + } + + .mx_AddExistingToSpace_noResults { + display: block; + margin-top: 24px; + } + + .mx_AddExistingToSpace_section { + &:not(:first-child) { + margin-top: 24px; + } + + > h3 { + margin: 0; + color: $secondary-fg-color; + font-size: $font-12px; + font-weight: $font-semi-bold; + line-height: $font-15px; + } + + .mx_AddExistingToSpace_entry { + display: flex; + margin-top: 12px; + + .mx_BaseAvatar { + margin-right: 12px; + } + + .mx_AddExistingToSpace_entry_name { + font-size: $font-15px; + line-height: 30px; + flex-grow: 1; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + margin-right: 12px; + } + + .mx_Checkbox { + align-items: center; + } + } + } + + .mx_AddExistingToSpace_section_spaces { + .mx_BaseAvatar_image { + border-radius: 8px; + } + } +} + .mx_AddExistingToSpaceDialog { width: 480px; color: $primary-fg-color; @@ -41,7 +101,7 @@ limitations under the License. .mx_BaseAvatar { display: inline-flex; - margin: 5px 16px 5px 5px; + margin: auto 16px auto 5px; vertical-align: middle; } @@ -100,85 +160,32 @@ limitations under the License. } } - .mx_SearchBox { - // To match the space around the title - margin: 0 0 15px 0; - flex-grow: 0; - } - - .mx_AddExistingToSpaceDialog_errorText { - font-weight: $font-semi-bold; - font-size: $font-12px; - line-height: $font-15px; - color: $notice-primary-color; - margin-bottom: 28px; - } - - .mx_AddExistingToSpaceDialog_content { - flex-grow: 1; - - .mx_AddExistingToSpaceDialog_noResults { - display: block; - margin-top: 24px; - } - } - - .mx_AddExistingToSpaceDialog_section { - &:not(:first-child) { - margin-top: 24px; - } - - > h3 { - margin: 0; - color: $secondary-fg-color; - font-size: $font-12px; - font-weight: $font-semi-bold; - line-height: $font-15px; - } - - .mx_AddExistingToSpaceDialog_entry { - display: flex; - margin-top: 12px; - - .mx_BaseAvatar { - margin-right: 12px; - } - - .mx_AddExistingToSpaceDialog_entry_name { - font-size: $font-15px; - line-height: 30px; - flex-grow: 1; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - margin-right: 12px; - } - - .mx_Checkbox { - align-items: center; - } - } - } - - .mx_AddExistingToSpaceDialog_section_spaces { - .mx_BaseAvatar_image { - border-radius: 8px; - } + .mx_AddExistingToSpace { + display: contents; } .mx_AddExistingToSpaceDialog_footer { display: flex; - margin-top: 32px; + margin-top: 20px; > span { flex-grow: 1; - font-size: $font-14px; + font-size: $font-12px; line-height: $font-15px; - font-weight: $font-semi-bold; + color: $secondary-fg-color; - .mx_AccessibleButton { - font-size: inherit; - display: inline-block; + .mx_ProgressBar { + height: 8px; + width: 100%; + + @mixin ProgressBarBorderRadius 8px; + } + + .mx_AddExistingToSpaceDialog_progressText { + margin-top: 8px; + font-size: $font-15px; + line-height: $font-24px; + color: $primary-fg-color; } > * { @@ -186,8 +193,54 @@ limitations under the License. } } + .mx_AddExistingToSpaceDialog_error { + padding-left: 12px; + + > img { + align-self: center; + } + + .mx_AddExistingToSpaceDialog_errorHeading { + font-weight: $font-semi-bold; + font-size: $font-15px; + line-height: $font-18px; + color: $notice-primary-color; + } + + .mx_AddExistingToSpaceDialog_errorCaption { + margin-top: 4px; + font-size: $font-12px; + line-height: $font-15px; + color: $primary-fg-color; + } + } + .mx_AccessibleButton { display: inline-block; + align-self: center; + } + + .mx_AccessibleButton_kind_primary { + padding: 8px 36px; + } + + .mx_AddExistingToSpaceDialog_retryButton { + margin-left: 12px; + padding-left: 24px; + position: relative; + + &::before { + content: ''; + position: absolute; + background-color: $primary-fg-color; + mask-repeat: no-repeat; + mask-position: center; + mask-size: contain; + mask-image: url('$(res)/img/element-icons/retry.svg'); + width: 18px; + height: 18px; + left: 0; + } } .mx_AccessibleButton_kind_link { diff --git a/res/css/views/elements/_AccessibleButton.scss b/res/css/views/elements/_AccessibleButton.scss index 0075dcb511..2997c83cfd 100644 --- a/res/css/views/elements/_AccessibleButton.scss +++ b/res/css/views/elements/_AccessibleButton.scss @@ -76,12 +76,16 @@ limitations under the License. border: 1px solid $button-danger-bg-color; } -.mx_AccessibleButton_kind_danger.mx_AccessibleButton_disabled, -.mx_AccessibleButton_kind_danger_outline.mx_AccessibleButton_disabled { +.mx_AccessibleButton_kind_danger.mx_AccessibleButton_disabled { color: $button-danger-disabled-fg-color; background-color: $button-danger-disabled-bg-color; } +.mx_AccessibleButton_kind_danger_outline.mx_AccessibleButton_disabled { + color: $button-danger-disabled-bg-color; + border-color: $button-danger-disabled-bg-color; +} + .mx_AccessibleButton_hasKind.mx_AccessibleButton_kind_danger_sm { padding: 5px 12px; color: $button-danger-fg-color; diff --git a/res/css/views/elements/_ImageView.scss b/res/css/views/elements/_ImageView.scss index 93ebcc2d56..71035dadc3 100644 --- a/res/css/views/elements/_ImageView.scss +++ b/res/css/views/elements/_ImageView.scss @@ -31,8 +31,7 @@ limitations under the License. .mx_ImageView_image { pointer-events: all; - max-width: 95%; - max-height: 95%; + flex-shrink: 0; } .mx_ImageView_panel { diff --git a/res/css/views/elements/_ProgressBar.scss b/res/css/views/elements/_ProgressBar.scss index 770978e921..c075ac74ff 100644 --- a/res/css/views/elements/_ProgressBar.scss +++ b/res/css/views/elements/_ProgressBar.scss @@ -21,7 +21,7 @@ progress.mx_ProgressBar { appearance: none; border: none; - @mixin ProgressBarBorderRadius "6px"; + @mixin ProgressBarBorderRadius 6px; @mixin ProgressBarColour $progressbar-fg-color; @mixin ProgressBarBgColour $progressbar-bg-color; ::-webkit-progress-value { diff --git a/res/css/views/messages/_MFileBody.scss b/res/css/views/messages/_MFileBody.scss index b45126acf8..c215d69ec2 100644 --- a/res/css/views/messages/_MFileBody.scss +++ b/res/css/views/messages/_MFileBody.scss @@ -61,9 +61,9 @@ limitations under the License. .mx_MFileBody_info { background-color: $message-body-panel-bg-color; - border-radius: 4px; - width: 270px; - padding: 8px; + border-radius: 12px; + width: 243px; // same width as a playable voice message, accounting for padding + padding: 6px 12px; color: $message-body-panel-fg-color; .mx_MFileBody_info_icon { @@ -82,7 +82,7 @@ limitations under the License. mask-position: center; mask-size: cover; mask-image: url('$(res)/img/element-icons/room/composer/attach.svg'); - background-color: $message-body-panel-fg-color; + background-color: $message-body-panel-icon-fg-color; width: 13px; height: 15px; diff --git a/res/css/views/messages/_MVoiceMessageBody.scss b/res/css/views/messages/_MVoiceMessageBody.scss new file mode 100644 index 0000000000..3dfb98f778 --- /dev/null +++ b/res/css/views/messages/_MVoiceMessageBody.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_MVoiceMessageBody { + display: inline-block; // makes the playback controls magically line up +} diff --git a/res/css/views/messages/_MessageActionBar.scss b/res/css/views/messages/_MessageActionBar.scss index 1254b496b5..3ecbef0d1f 100644 --- a/res/css/views/messages/_MessageActionBar.scss +++ b/res/css/views/messages/_MessageActionBar.scss @@ -105,3 +105,11 @@ limitations under the License. .mx_MessageActionBar_optionsButton::after { mask-image: url('$(res)/img/element-icons/context-menu.svg'); } + +.mx_MessageActionBar_resendButton::after { + mask-image: url('$(res)/img/element-icons/retry.svg'); +} + +.mx_MessageActionBar_cancelButton::after { + mask-image: url('$(res)/img/element-icons/trashcan.svg'); +} diff --git a/res/css/views/messages/_ReactionsRowButton.scss b/res/css/views/messages/_ReactionsRowButton.scss index 7158ffc027..c132fa5a0f 100644 --- a/res/css/views/messages/_ReactionsRowButton.scss +++ b/res/css/views/messages/_ReactionsRowButton.scss @@ -34,6 +34,10 @@ limitations under the License. border-color: $reaction-row-button-selected-border-color; } + &.mx_AccessibleButton_disabled { + cursor: not-allowed; + } + .mx_ReactionsRowButton_content { max-width: 100px; overflow: hidden; diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 35a5d8fc15..a6ccde93d3 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -219,10 +219,6 @@ $left-gutter: 64px; color: $accent-fg-color; } -.mx_EventTile_notSent { - color: $event-notsent-color; -} - .mx_EventTile_receiptSent, .mx_EventTile_receiptSending { // We don't use `position: relative` on the element because then it won't line diff --git a/res/css/views/rooms/_VoiceRecordComposerTile.scss b/res/css/views/rooms/_VoiceRecordComposerTile.scss index 8100a03890..b87211a847 100644 --- a/res/css/views/rooms/_VoiceRecordComposerTile.scss +++ b/res/css/views/rooms/_VoiceRecordComposerTile.scss @@ -35,44 +35,42 @@ limitations under the License. } } -.mx_VoiceRecordComposerTile_waveformContainer { - padding: 5px; - padding-right: 4px; // there's 1px from the waveform itself, so account for that - padding-left: 15px; // +10px for the live circle, +5px for regular padding - background-color: $voice-record-waveform-bg-color; - border-radius: 12px; - margin-right: 12px; // isolate from stop button +.mx_VoiceRecordComposerTile_delete { + width: 14px; // w&h are size of icon + height: 18px; + vertical-align: middle; + margin-right: 11px; // distance from left edge of waveform container (container has some margin too) + background-color: $voice-record-icon-color; + mask-repeat: no-repeat; + mask-size: contain; + mask-image: url('$(res)/img/element-icons/trashcan.svg'); +} - // Cheat at alignment a bit - display: flex; - align-items: center; +.mx_VoiceRecordComposerTile_recording.mx_VoiceMessagePrimaryContainer { + // Note: remaining class properties are in the PlayerContainer CSS. + + margin: 6px; // force the composer area to put a gutter around us + margin-right: 12px; // isolate from stop button position: relative; // important for the live circle - color: $voice-record-waveform-fg-color; - font-size: $font-14px; + &.mx_VoiceRecordComposerTile_recording { + // We are putting the circle in this padding, so we need +10px from the regular + // padding on the left side. + padding-left: 22px; - &::before { - animation: recording-pulse 2s infinite; + &::before { + animation: recording-pulse 2s infinite; - content: ''; - background-color: $voice-record-live-circle-color; - width: 10px; - height: 10px; - position: absolute; - left: 8px; - top: 16px; // vertically center - border-radius: 10px; - } - - .mx_Waveform_bar { - background-color: $voice-record-waveform-fg-color; - } - - .mx_Clock { - padding-right: 8px; // isolate from waveform - padding-left: 10px; // isolate from live circle - width: 42px; // we're not using a monospace font, so fake it + content: ''; + background-color: $voice-record-live-circle-color; + width: 10px; + height: 10px; + position: absolute; + left: 12px; // 12px from the left edge for container padding + top: 18px; // vertically center (middle align with clock) + border-radius: 10px; + } } } diff --git a/res/css/views/settings/tabs/user/_HelpUserSettingsTab.scss b/res/css/views/settings/tabs/user/_HelpUserSettingsTab.scss index 109edfff81..0f879d209e 100644 --- a/res/css/views/settings/tabs/user/_HelpUserSettingsTab.scss +++ b/res/css/views/settings/tabs/user/_HelpUserSettingsTab.scss @@ -22,3 +22,34 @@ limitations under the License. .mx_HelpUserSettingsTab span.mx_AccessibleButton { word-break: break-word; } + +.mx_HelpUserSettingsTab code { + word-break: break-all; + user-select: all; +} + +.mx_HelpUserSettingsTab_accessToken { + display: flex; + justify-content: space-between; + border-radius: 5px; + border: solid 1px $light-fg-color; + margin-bottom: 10px; + margin-top: 10px; + padding: 10px; +} + +.mx_HelpUserSettingsTab_accessToken_copy { + flex-shrink: 0; + cursor: pointer; + margin-left: 20px; + display: inherit; +} + +.mx_HelpUserSettingsTab_accessToken_copy > div { + mask-image: url($copy-button-url); + background-color: $message-action-bar-fg-color; + margin-left: 5px; + width: 20px; + height: 20px; + background-repeat: no-repeat; +} diff --git a/res/css/views/voice_messages/_PlayPauseButton.scss b/res/css/views/voice_messages/_PlayPauseButton.scss new file mode 100644 index 0000000000..6caedafa29 --- /dev/null +++ b/res/css/views/voice_messages/_PlayPauseButton.scss @@ -0,0 +1,51 @@ +/* +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_PlayPauseButton { + position: relative; + width: 32px; + height: 32px; + border-radius: 32px; + background-color: $voice-playback-button-bg-color; + + &::before { + content: ''; + position: absolute; // sizing varies by icon + background-color: $voice-playback-button-fg-color; + mask-repeat: no-repeat; + mask-size: contain; + } + + &.mx_PlayPauseButton_disabled::before { + opacity: 0.5; + } + + &.mx_PlayPauseButton_play::before { + width: 13px; + height: 16px; + top: 8px; // center + left: 12px; // center + mask-image: url('$(res)/img/element-icons/play.svg'); + } + + &.mx_PlayPauseButton_pause::before { + width: 10px; + height: 12px; + top: 10px; // center + left: 11px; // center + mask-image: url('$(res)/img/element-icons/pause.svg'); + } +} diff --git a/res/css/views/voice_messages/_PlaybackContainer.scss b/res/css/views/voice_messages/_PlaybackContainer.scss new file mode 100644 index 0000000000..64e8f445e1 --- /dev/null +++ b/res/css/views/voice_messages/_PlaybackContainer.scss @@ -0,0 +1,53 @@ +/* +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. +*/ + +// Dev note: there's no actual component called . These classes +// are shared amongst multiple voice message components. + +// Container for live recording and playback controls +.mx_VoiceMessagePrimaryContainer { + // 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; + border-radius: 12px; + + // Cheat at alignment a bit + display: flex; + align-items: center; + + color: $voice-record-waveform-fg-color; + font-size: $font-14px; + line-height: $font-24px; + + .mx_Waveform { + .mx_Waveform_bar { + background-color: $voice-record-waveform-incomplete-fg-color; + + &.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; + } + } + } + + .mx_Clock { + width: 42px; // we're not using a monospace font, so fake it + padding-right: 6px; // with the fixed width this ends up as a visual 8px most of the time, as intended. + padding-left: 8px; // isolate from recording circle / play control + } +} diff --git a/res/css/views/voip/_CallView.scss b/res/css/views/voip/_CallView.scss index d13272c8c0..0be75be28c 100644 --- a/res/css/views/voip/_CallView.scss +++ b/res/css/views/voip/_CallView.scss @@ -17,7 +17,7 @@ limitations under the License. .mx_CallView { border-radius: 8px; - background-color: $voipcall-plinth-color; + background-color: $dark-panel-bg-color; padding-left: 8px; padding-right: 8px; // XXX: CallContainer sets pointer-events: none - should probably be set back in a better place @@ -40,7 +40,8 @@ limitations under the License. width: 320px; padding-bottom: 8px; margin-top: 10px; - box-shadow: 0px 14px 24px rgba(0, 0, 0, 0.08); + background-color: $voipcall-plinth-color; + box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.20); border-radius: 8px; .mx_CallView_voice { @@ -64,14 +65,17 @@ limitations under the License. } } -.mx_CallView_voice { +.mx_CallView_content { position: relative; display: flex; - flex-direction: column; + border-radius: 8px; +} + +.mx_CallView_voice { align-items: center; justify-content: center; + flex-direction: column; background-color: $inverted-bg-color; - border-radius: 8px; } .mx_CallView_voice_avatarsContainer { @@ -108,9 +112,7 @@ limitations under the License. .mx_CallView_video { width: 100%; height: 100%; - position: relative; z-index: 30; - border-radius: 8px; overflow: hidden; } diff --git a/res/css/views/voip/_VideoFeed.scss b/res/css/views/voip/_VideoFeed.scss index 8ead8bba3e..7d85ac264e 100644 --- a/res/css/views/voip/_VideoFeed.scss +++ b/res/css/views/voip/_VideoFeed.scss @@ -14,21 +14,37 @@ See the License for the specific language governing permissions and 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; +} + + .mx_VideoFeed_remote { width: 100%; height: 100%; - background-color: #000; - z-index: 50; + display: flex; + justify-content: center; + align-items: center; + + &.mx_VideoFeed_video { + background-color: #000; + } } .mx_VideoFeed_local { - width: 25%; - height: 25%; + max-width: 25%; + max-height: 25%; position: absolute; right: 10px; top: 10px; z-index: 100; border-radius: 4px; + + &.mx_VideoFeed_video { + background-color: transparent; + } } .mx_VideoFeed_mirror { diff --git a/res/img/element-icons/pause.svg b/res/img/element-icons/pause.svg new file mode 100644 index 0000000000..293c0a10d8 --- /dev/null +++ b/res/img/element-icons/pause.svg @@ -0,0 +1,4 @@ + + + + diff --git a/res/img/element-icons/play.svg b/res/img/element-icons/play.svg new file mode 100644 index 0000000000..339e20b729 --- /dev/null +++ b/res/img/element-icons/play.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/element-icons/retry.svg b/res/img/element-icons/retry.svg new file mode 100644 index 0000000000..09448d6458 --- /dev/null +++ b/res/img/element-icons/retry.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/element-icons/trashcan.svg b/res/img/element-icons/trashcan.svg new file mode 100644 index 0000000000..f8fb8b5c46 --- /dev/null +++ b/res/img/element-icons/trashcan.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index 925d268eb0..2d0e3d2a8b 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -9,6 +9,7 @@ $header-panel-text-primary-color: #B9BEC6; $header-panel-text-secondary-color: #c8c8cd; $text-primary-color: #ffffff; $text-secondary-color: #B9BEC6; +$quaternary-fg-color: #6F7882; $search-bg-color: #181b21; $search-placeholder-color: #61708b; $room-highlight-color: #343a46; @@ -63,6 +64,8 @@ $input-invalid-border-color: $warning-color; $field-focused-label-bg-color: $bg-color; +$resend-button-divider-color: #b9bec64a; // muted-text with a 4A opacity. + // scrollbars $scrollbar-thumb-color: rgba(255, 255, 255, 0.2); $scrollbar-track-color: transparent; @@ -110,7 +113,7 @@ $header-divider-color: $header-panel-text-primary-color; $composer-e2e-icon-color: $header-panel-text-primary-color; // this probably shouldn't have it's own colour -$voipcall-plinth-color: #21262c; +$voipcall-plinth-color: #394049; // ******************** @@ -203,9 +206,18 @@ $breadcrumb-placeholder-bg-color: #272c35; $user-tile-hover-bg-color: $header-panel-bg-color; -$message-body-panel-bg-color: #21262c82; -$message-body-panel-icon-bg-color: #8e99a4; -$message-body-panel-fg-color: $primary-fg-color; +$message-body-panel-fg-color: $secondary-fg-color; +$message-body-panel-bg-color: #394049; // "Dark Tile" +$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; +$voice-playback-button-fg-color: $message-body-panel-icon-fg-color; // Appearance tab colors $appearance-tab-border-color: $room-highlight-color; diff --git a/res/themes/legacy-dark/css/_legacy-dark.scss b/res/themes/legacy-dark/css/_legacy-dark.scss index 28e6e22326..a852ad94e9 100644 --- a/res/themes/legacy-dark/css/_legacy-dark.scss +++ b/res/themes/legacy-dark/css/_legacy-dark.scss @@ -61,6 +61,8 @@ $input-invalid-border-color: $warning-color; $field-focused-label-bg-color: $bg-color; +$resend-button-divider-color: $muted-fg-color; + // scrollbars $scrollbar-thumb-color: rgba(255, 255, 255, 0.2); $scrollbar-track-color: transparent; @@ -107,7 +109,7 @@ $header-divider-color: $header-panel-text-primary-color; $composer-e2e-icon-color: $header-panel-text-primary-color; // this probably shouldn't have it's own colour -$voipcall-plinth-color: #f2f5f8; +$voipcall-plinth-color: #394049; // ******************** @@ -198,9 +200,19 @@ $breadcrumb-placeholder-bg-color: #272c35; $user-tile-hover-bg-color: $header-panel-bg-color; -$message-body-panel-bg-color: #21262c82; -$message-body-panel-icon-bg-color: #8e99a4; -$message-body-panel-fg-color: $primary-fg-color; +$message-body-panel-fg-color: $secondary-fg-color; +$message-body-panel-bg-color: #394049; +$message-body-panel-icon-fg-color: $primary-bg-color; +$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; +$voice-playback-button-fg-color: #21262C; // Appearance tab colors $appearance-tab-border-color: $room-highlight-color; diff --git a/res/themes/legacy-light/css/_legacy-light.scss b/res/themes/legacy-light/css/_legacy-light.scss index 7b6bdad4a4..84666bc662 100644 --- a/res/themes/legacy-light/css/_legacy-light.scss +++ b/res/themes/legacy-light/css/_legacy-light.scss @@ -97,6 +97,8 @@ $input-invalid-border-color: $warning-color; $field-focused-label-bg-color: #ffffff; +$resend-button-divider-color: $input-darker-bg-color; + $button-bg-color: $accent-color; $button-fg-color: white; @@ -174,7 +176,7 @@ $composer-e2e-icon-color: #91a1c0; $header-divider-color: #91a1c0; // this probably shouldn't have it's own colour -$voipcall-plinth-color: #f2f5f8; +$voipcall-plinth-color: #F4F6FA; // ******************** @@ -189,13 +191,6 @@ $roomsublist-skeleton-ui-bg: linear-gradient(180deg, #ffffff 0%, #ffffff00 100%) $groupFilterPanel-divider-color: $roomlist-header-color; -// See non-legacy _light for variable information -$voice-record-stop-border-color: #E3E8F0; -$voice-record-stop-symbol-color: #ff4b55; -$voice-record-waveform-bg-color: #E3E8F0; -$voice-record-waveform-fg-color: $muted-fg-color; -$voice-record-live-circle-color: #ff4b55; - $roomtile-preview-color: #9e9e9e; $roomtile-default-badge-bg-color: #61708b; $roomtile-selected-bg-color: #fff; @@ -328,9 +323,21 @@ $breadcrumb-placeholder-bg-color: #e8eef5; $user-tile-hover-bg-color: $header-panel-bg-color; -$message-body-panel-bg-color: #e3e8f082; -$message-body-panel-icon-bg-color: #ffffff; -$message-body-panel-fg-color: $muted-fg-color; +$message-body-panel-fg-color: $secondary-fg-color; +$message-body-panel-bg-color: #E3E8F0; +$message-body-panel-icon-fg-color: $secondary-fg-color; +$message-body-panel-icon-bg-color: $primary-bg-color; + +// See non-legacy _light for variable information +$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; +$voice-playback-button-fg-color: $message-body-panel-icon-fg-color; // FontSlider colors $appearance-tab-border-color: $input-darker-bg-color; diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index 5b46138dae..c889f43d0b 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -21,6 +21,7 @@ $notice-primary-bg-color: rgba(255, 75, 85, 0.16); $primary-fg-color: #2e2f32; $secondary-fg-color: #737D8C; $tertiary-fg-color: #8D99A5; +$quaternary-fg-color: #C1C6CD; $header-panel-bg-color: #f3f8fd; // typical text (dark-on-white in light skin) @@ -91,6 +92,8 @@ $field-focused-label-bg-color: #ffffff; $button-bg-color: $accent-color; $button-fg-color: white; +$resend-button-divider-color: $input-darker-bg-color; + // apart from login forms, which have stronger border $strong-input-border-color: #c7c7c7; @@ -165,7 +168,7 @@ $composer-e2e-icon-color: #91A1C0; $header-divider-color: #91A1C0; // this probably shouldn't have it's own colour -$voipcall-plinth-color: #f2f5f8; +$voipcall-plinth-color: #F4F6FA; // ******************** @@ -180,12 +183,6 @@ $roomsublist-skeleton-ui-bg: linear-gradient(180deg, #ffffff 0%, #ffffff00 100%) $groupFilterPanel-divider-color: $roomlist-header-color; -$voice-record-stop-border-color: #E3E8F0; -$voice-record-stop-symbol-color: #ff4b55; // $warning-color, but without letting people change it in themes -$voice-record-waveform-bg-color: #E3E8F0; -$voice-record-waveform-fg-color: $muted-fg-color; -$voice-record-live-circle-color: #ff4b55; // $warning-color, but without letting people change it in themes - $roomtile-preview-color: $secondary-fg-color; $roomtile-default-badge-bg-color: #61708b; $roomtile-selected-bg-color: #FFF; @@ -325,9 +322,23 @@ $breadcrumb-placeholder-bg-color: #e8eef5; $user-tile-hover-bg-color: $header-panel-bg-color; -$message-body-panel-bg-color: #e3e8f082; -$message-body-panel-icon-bg-color: #ffffff; -$message-body-panel-fg-color: $muted-fg-color; +$message-body-panel-fg-color: $secondary-fg-color; +$message-body-panel-bg-color: #E3E8F0; // "Separator" +$message-body-panel-icon-fg-color: $secondary-fg-color; +$message-body-panel-icon-bg-color: $primary-bg-color; + +// These two don't change between themes. They are the $warning-color, but we don't +// want custom themes to affect them by accident. +$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; +$voice-playback-button-fg-color: $message-body-panel-icon-fg-color; // FontSlider colors $appearance-tab-border-color: $input-darker-bg-color; diff --git a/scripts/compare-file.js b/scripts/compare-file.js deleted file mode 100644 index f53275ebfa..0000000000 --- a/scripts/compare-file.js +++ /dev/null @@ -1,10 +0,0 @@ -const fs = require("fs"); - -if (process.argv.length < 4) throw new Error("Missing source and target file arguments"); - -const sourceFile = fs.readFileSync(process.argv[2], 'utf8'); -const targetFile = fs.readFileSync(process.argv[3], 'utf8'); - -if (sourceFile !== targetFile) { - throw new Error("Files do not match"); -} diff --git a/scripts/gen-i18n.js b/scripts/gen-i18n.js deleted file mode 100755 index 91733469f7..0000000000 --- a/scripts/gen-i18n.js +++ /dev/null @@ -1,304 +0,0 @@ -#!/usr/bin/env node - -/* -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. -*/ - -/** - * Regenerates the translations en_EN file by walking the source tree and - * parsing each file with the appropriate parser. Emits a JSON file with the - * translatable strings mapped to themselves in the order they appeared - * in the files and grouped by the file they appeared in. - * - * Usage: node scripts/gen-i18n.js - */ -const fs = require('fs'); -const path = require('path'); - -const walk = require('walk'); - -const parser = require("@babel/parser"); -const traverse = require("@babel/traverse"); - -const TRANSLATIONS_FUNCS = ['_t', '_td']; - -const INPUT_TRANSLATIONS_FILE = 'src/i18n/strings/en_EN.json'; -const OUTPUT_FILE = 'src/i18n/strings/en_EN.json'; - -// NB. The sync version of walk is broken for single files so we walk -// all of res rather than just res/home.html. -// https://git.daplie.com/Daplie/node-walk/merge_requests/1 fixes it, -// or if we get bored waiting for it to be merged, we could switch -// to a project that's actively maintained. -const SEARCH_PATHS = ['src', 'res']; - -function getObjectValue(obj, key) { - for (const prop of obj.properties) { - if (prop.key.type === 'Identifier' && prop.key.name === key) { - return prop.value; - } - } - return null; -} - -function getTKey(arg) { - if (arg.type === 'Literal' || arg.type === "StringLiteral") { - return arg.value; - } else if (arg.type === 'BinaryExpression' && arg.operator === '+') { - return getTKey(arg.left) + getTKey(arg.right); - } else if (arg.type === 'TemplateLiteral') { - return arg.quasis.map((q) => { - return q.value.raw; - }).join(''); - } - return null; -} - -function getFormatStrings(str) { - // Match anything that starts with % - // We could make a regex that matched the full placeholder, but this - // would just not match invalid placeholders and so wouldn't help us - // detect the invalid ones. - // Also note that for simplicity, this just matches a % character and then - // anything up to the next % character (or a single %, or end of string). - const formatStringRe = /%([^%]+|%|$)/g; - const formatStrings = new Set(); - - let match; - while ( (match = formatStringRe.exec(str)) !== null ) { - const placeholder = match[1]; // Minus the leading '%' - if (placeholder === '%') continue; // Literal % is %% - - const placeholderMatch = placeholder.match(/^\((.*?)\)(.)/); - if (placeholderMatch === null) { - throw new Error("Invalid format specifier: '"+match[0]+"'"); - } - if (placeholderMatch.length < 3) { - throw new Error("Malformed format specifier"); - } - const placeholderName = placeholderMatch[1]; - const placeholderFormat = placeholderMatch[2]; - - if (placeholderFormat !== 's') { - throw new Error(`'${placeholderFormat}' used as format character: you probably meant 's'`); - } - - formatStrings.add(placeholderName); - } - - return formatStrings; -} - -function getTranslationsJs(file) { - const contents = fs.readFileSync(file, { encoding: 'utf8' }); - - const trs = new Set(); - - try { - const plugins = [ - // https://babeljs.io/docs/en/babel-parser#plugins - "classProperties", - "objectRestSpread", - "throwExpressions", - "exportDefaultFrom", - "decorators-legacy", - ]; - - if (file.endsWith(".js") || file.endsWith(".jsx")) { - // all JS is assumed to be flow or react - plugins.push("flow", "jsx"); - } else if (file.endsWith(".ts")) { - // TS can't use JSX unless it's a TSX file (otherwise angle casts fail) - plugins.push("typescript"); - } else if (file.endsWith(".tsx")) { - // When the file is a TSX file though, enable JSX parsing - plugins.push("typescript", "jsx"); - } - - const babelParsed = parser.parse(contents, { - allowImportExportEverywhere: true, - errorRecovery: true, - sourceFilename: file, - tokens: true, - plugins, - }); - traverse.default(babelParsed, { - enter: (p) => { - const node = p.node; - if (p.isCallExpression() && node.callee && TRANSLATIONS_FUNCS.includes(node.callee.name)) { - const tKey = getTKey(node.arguments[0]); - - // This happens whenever we call _t with non-literals (ie. whenever we've - // had to use a _td to compensate) so is expected. - if (tKey === null) return; - - // check the format string against the args - // We only check _t: _td has no args - if (node.callee.name === '_t') { - try { - const placeholders = getFormatStrings(tKey); - for (const placeholder of placeholders) { - if (node.arguments.length < 2) { - throw new Error(`Placeholder found ('${placeholder}') but no substitutions given`); - } - const value = getObjectValue(node.arguments[1], placeholder); - if (value === null) { - throw new Error(`No value found for placeholder '${placeholder}'`); - } - } - - // Validate tag replacements - if (node.arguments.length > 2) { - const tagMap = node.arguments[2]; - for (const prop of tagMap.properties || []) { - if (prop.key.type === 'Literal') { - const tag = prop.key.value; - // RegExp same as in src/languageHandler.js - const regexp = new RegExp(`(<${tag}>(.*?)<\\/${tag}>|<${tag}>|<${tag}\\s*\\/>)`); - if (!tKey.match(regexp)) { - throw new Error(`No match for ${regexp} in ${tKey}`); - } - } - } - } - - } catch (e) { - console.log(); - console.error(`ERROR: ${file}:${node.loc.start.line} ${tKey}`); - console.error(e); - process.exit(1); - } - } - - let isPlural = false; - if (node.arguments.length > 1 && node.arguments[1].type === 'ObjectExpression') { - const countVal = getObjectValue(node.arguments[1], 'count'); - if (countVal) { - isPlural = true; - } - } - - if (isPlural) { - trs.add(tKey + "|other"); - const plurals = enPlurals[tKey]; - if (plurals) { - for (const pluralType of Object.keys(plurals)) { - trs.add(tKey + "|" + pluralType); - } - } - } else { - trs.add(tKey); - } - } - }, - }); - } catch (e) { - console.error(e); - process.exit(1); - } - - return trs; -} - -function getTranslationsOther(file) { - const contents = fs.readFileSync(file, { encoding: 'utf8' }); - - const trs = new Set(); - - // Taken from element-web src/components/structures/HomePage.js - const translationsRegex = /_t\(['"]([\s\S]*?)['"]\)/mg; - let matches; - while (matches = translationsRegex.exec(contents)) { - trs.add(matches[1]); - } - return trs; -} - -// gather en_EN plural strings from the input translations file: -// the en_EN strings are all in the source with the exception of -// pluralised strings, which we need to pull in from elsewhere. -const inputTranslationsRaw = JSON.parse(fs.readFileSync(INPUT_TRANSLATIONS_FILE, { encoding: 'utf8' })); -const enPlurals = {}; - -for (const key of Object.keys(inputTranslationsRaw)) { - const parts = key.split("|"); - if (parts.length > 1) { - const plurals = enPlurals[parts[0]] || {}; - plurals[parts[1]] = inputTranslationsRaw[key]; - enPlurals[parts[0]] = plurals; - } -} - -const translatables = new Set(); - -const walkOpts = { - listeners: { - names: function(root, nodeNamesArray) { - // Sort the names case insensitively and alphabetically to - // maintain some sense of order between the different strings. - nodeNamesArray.sort((a, b) => { - a = a.toLowerCase(); - b = b.toLowerCase(); - if (a > b) return 1; - if (a < b) return -1; - return 0; - }); - }, - file: function(root, fileStats, next) { - const fullPath = path.join(root, fileStats.name); - - let trs; - if (fileStats.name.endsWith('.js') || fileStats.name.endsWith('.ts') || fileStats.name.endsWith('.tsx')) { - trs = getTranslationsJs(fullPath); - } else if (fileStats.name.endsWith('.html')) { - trs = getTranslationsOther(fullPath); - } else { - return; - } - console.log(`${fullPath} (${trs.size} strings)`); - for (const tr of trs.values()) { - // Convert DOS line endings to unix - translatables.add(tr.replace(/\r\n/g, "\n")); - } - }, - } -}; - -for (const path of SEARCH_PATHS) { - if (fs.existsSync(path)) { - walk.walkSync(path, walkOpts); - } -} - -const trObj = {}; -for (const tr of translatables) { - if (tr.includes("|")) { - if (inputTranslationsRaw[tr]) { - trObj[tr] = inputTranslationsRaw[tr]; - } else { - trObj[tr] = tr.split("|")[0]; - } - } else { - trObj[tr] = tr; - } -} - -fs.writeFileSync( - OUTPUT_FILE, - JSON.stringify(trObj, translatables.values(), 4) + "\n" -); - -console.log(); -console.log(`Wrote ${translatables.size} strings to ${OUTPUT_FILE}`); diff --git a/scripts/prune-i18n.js b/scripts/prune-i18n.js deleted file mode 100755 index b4fe8d69f5..0000000000 --- a/scripts/prune-i18n.js +++ /dev/null @@ -1,68 +0,0 @@ -#!/usr/bin/env node - -/* -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. -*/ - -/* - * Looks through all the translation files and removes any strings - * which don't appear in en_EN.json. - * Use this if you remove a translation, but merge any outstanding changes - * from weblate first or you'll need to resolve the conflict in weblate. - */ - -const fs = require('fs'); -const path = require('path'); - -const I18NDIR = 'src/i18n/strings'; - -const enStringsRaw = JSON.parse(fs.readFileSync(path.join(I18NDIR, 'en_EN.json'))); - -const enStrings = new Set(); -for (const str of Object.keys(enStringsRaw)) { - const parts = str.split('|'); - if (parts.length > 1) { - enStrings.add(parts[0]); - } else { - enStrings.add(str); - } -} - -for (const filename of fs.readdirSync(I18NDIR)) { - if (filename === 'en_EN.json') continue; - if (filename === 'basefile.json') continue; - if (!filename.endsWith('.json')) continue; - - const trs = JSON.parse(fs.readFileSync(path.join(I18NDIR, filename))); - const oldLen = Object.keys(trs).length; - for (const tr of Object.keys(trs)) { - const parts = tr.split('|'); - const trKey = parts.length > 1 ? parts[0] : tr; - if (!enStrings.has(trKey)) { - delete trs[tr]; - } - } - - const removed = oldLen - Object.keys(trs).length; - if (removed > 0) { - console.log(`${filename}: removed ${removed} translations`); - // XXX: This is totally relying on the impl serialising the JSON object in the - // same order as they were parsed from the file. JSON.stringify() has a specific argument - // that can be used to control the order, but JSON.parse() lacks any kind of equivalent. - // Empirically this does maintain the order on my system, so I'm going to leave it like - // this for now. - fs.writeFileSync(path.join(I18NDIR, filename), JSON.stringify(trs, undefined, 4) + "\n"); - } -} diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index 41257c21f0..e8f2f1bc08 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -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. @@ -39,7 +39,9 @@ import {ModalWidgetStore} from "../stores/ModalWidgetStore"; import { WidgetLayoutStore } from "../stores/widgets/WidgetLayoutStore"; import VoipUserMapper from "../VoipUserMapper"; import {SpaceStoreClass} from "../stores/SpaceStore"; -import {VoiceRecording} from "../voice/VoiceRecording"; +import TypingStore from "../stores/TypingStore"; +import { EventIndexPeg } from "../indexing/EventIndexPeg"; +import {VoiceRecordingStore} from "../stores/VoiceRecordingStore"; declare global { interface Window { @@ -71,12 +73,16 @@ declare global { mxModalWidgetStore: ModalWidgetStore; mxVoipUserMapper: VoipUserMapper; mxSpaceStore: SpaceStoreClass; - mxVoiceRecorder: typeof VoiceRecording; + mxVoiceRecordingStore: VoiceRecordingStore; + mxTypingStore: TypingStore; + mxEventIndexPeg: EventIndexPeg; } interface Document { // https://developer.mozilla.org/en-US/docs/Web/API/Document/hasStorageAccess hasStorageAccess?: () => Promise; + // https://developer.mozilla.org/en-US/docs/Web/API/Document/requestStorageAccess + requestStorageAccess?: () => Promise; // Safari & IE11 only have this prefixed: we used prefixed versions // previously so let's continue to support them for now @@ -112,6 +118,16 @@ declare global { interface HTMLAudioElement { type?: string; + // sinkId & setSinkId are experimental and typescript doesn't know about them + sinkId: string; + setSinkId(outputId: string); + } + + interface HTMLVideoElement { + type?: string; + // sinkId & setSinkId are experimental and typescript doesn't know about them + sinkId: string; + setSinkId(outputId: string); } interface Element { diff --git a/src/Avatar.ts b/src/Avatar.ts index 76c88faa1c..a6499c688e 100644 --- a/src/Avatar.ts +++ b/src/Avatar.ts @@ -20,6 +20,7 @@ import {Room} from "matrix-js-sdk/src/models/room"; import DMRoomMap from './utils/DMRoomMap'; import {mediaFromMxc} from "./customisations/Media"; +import SettingsStore from "./settings/SettingsStore"; export type ResizeMethod = "crop" | "scale"; @@ -27,11 +28,7 @@ export type ResizeMethod = "crop" | "scale"; export function avatarUrlForMember(member: RoomMember, width: number, height: number, resizeMethod: ResizeMethod) { let url: string; if (member?.getMxcAvatarUrl()) { - url = mediaFromMxc(member.getMxcAvatarUrl()).getThumbnailOfSourceHttp( - Math.floor(width * window.devicePixelRatio), - Math.floor(height * window.devicePixelRatio), - resizeMethod, - ); + url = mediaFromMxc(member.getMxcAvatarUrl()).getThumbnailOfSourceHttp(width, height, resizeMethod); } if (!url) { // member can be null here currently since on invites, the JS SDK @@ -44,11 +41,7 @@ export function avatarUrlForMember(member: RoomMember, width: number, height: nu export function avatarUrlForUser(user: User, width: number, height: number, resizeMethod?: ResizeMethod) { if (!user.avatarUrl) return null; - return mediaFromMxc(user.avatarUrl).getThumbnailOfSourceHttp( - Math.floor(width * window.devicePixelRatio), - Math.floor(height * window.devicePixelRatio), - resizeMethod, - ); + return mediaFromMxc(user.avatarUrl).getThumbnailOfSourceHttp(width, height, resizeMethod); } function isValidHexColor(color: string): boolean { @@ -151,7 +144,7 @@ export function avatarUrlForRoom(room: Room, width: number, height: number, resi } // space rooms cannot be DMs so skip the rest - if (room.isSpaceRoom()) return null; + if (SettingsStore.getValue("feature_spaces") && room.isSpaceRoom()) return null; let otherMember = null; const otherUserId = DMRoomMap.shared().getUserIdForRoomId(room.roomId); diff --git a/src/BasePlatform.ts b/src/BasePlatform.ts index b6012d7597..5483ea6874 100644 --- a/src/BasePlatform.ts +++ b/src/BasePlatform.ts @@ -258,7 +258,7 @@ export default abstract class BasePlatform { return null; } - setLanguage(preferredLangs: string[]) {} + async setLanguage(preferredLangs: string[]) {} setSpellCheckLanguages(preferredLangs: string[]) {} diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx index be687a4474..0268ebfe46 100644 --- a/src/CallHandler.tsx +++ b/src/CallHandler.tsx @@ -59,7 +59,6 @@ import {MatrixClientPeg} from './MatrixClientPeg'; import PlatformPeg from './PlatformPeg'; import Modal from './Modal'; import { _t } from './languageHandler'; -import { createNewMatrixCall } from 'matrix-js-sdk/src/webrtc/call'; import dis from './dispatcher/dispatcher'; import WidgetUtils from './utils/WidgetUtils'; import WidgetEchoStore from './stores/WidgetEchoStore'; @@ -86,6 +85,9 @@ import { Action } from './dispatcher/actions'; import VoipUserMapper from './VoipUserMapper'; import { addManagedHybridWidget, isManagedHybridWidgetEnabled } from './widgets/ManagedHybrid'; import { randomUppercaseString, randomLowercaseString } from "matrix-js-sdk/src/randomstring"; +import EventEmitter from 'events'; +import SdkConfig from './SdkConfig'; +import { ensureDMExists, findDMForUser } from './createRoom'; export const PROTOCOL_PSTN = 'm.protocol.pstn'; export const PROTOCOL_PSTN_PREFIXED = 'im.vector.protocol.pstn'; @@ -137,22 +139,12 @@ export enum PlaceCallType { ScreenSharing = 'screensharing', } -function getRemoteAudioElement(): HTMLAudioElement { - // this needs to be somewhere at the top of the DOM which - // always exists to avoid audio interruptions. - // Might as well just use DOM. - const remoteAudioElement = document.getElementById("remoteAudio") as HTMLAudioElement; - if (!remoteAudioElement) { - console.error( - "Failed to find remoteAudio element - cannot play audio!" + - "You need to add an
      ), button: _t("Trust"), - }); + }); const [confirmed] = await finished; if (confirmed) { // eslint-disable-next-line react-hooks/rules-of-hooks diff --git a/src/KeyBindingsManager.ts b/src/KeyBindingsManager.ts index d862f10c02..aac14bde20 100644 --- a/src/KeyBindingsManager.ts +++ b/src/KeyBindingsManager.ts @@ -231,8 +231,10 @@ export class KeyBindingsManager { /** * Finds a matching KeyAction for a given KeyboardEvent */ - private getAction(getters: KeyBindingGetter[], ev: KeyboardEvent | React.KeyboardEvent) - : T | undefined { + private getAction( + getters: KeyBindingGetter[], + ev: KeyboardEvent | React.KeyboardEvent, + ): T | undefined { for (const getter of getters) { const bindings = getter(); const binding = bindings.find(it => isKeyComboMatch(ev, it.keyCombo, isMac)); diff --git a/src/Login.ts b/src/Login.ts index db3c4c11e4..d584df7dfe 100644 --- a/src/Login.ts +++ b/src/Login.ts @@ -1,9 +1,6 @@ /* -Copyright 2015, 2016 OpenMarket Ltd -Copyright 2017 Vector Creations Ltd -Copyright 2018 New Vector Ltd +Copyright 2015-2021 The Matrix.org Foundation C.I.C. Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> -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. @@ -59,7 +56,7 @@ export type LoginFlow = ISSOFlow | IPasswordFlow; // TODO: Move this to JS SDK /* eslint-disable camelcase */ interface ILoginParams { - identifier?: string; + identifier?: object; password?: string; token?: string; device_id?: string; diff --git a/src/PasswordReset.js b/src/PasswordReset.js index 6fe6ca82cc..88ae00d088 100644 --- a/src/PasswordReset.js +++ b/src/PasswordReset.js @@ -54,7 +54,7 @@ export default class PasswordReset { return res; }, function(err) { if (err.errcode === 'M_THREEPID_NOT_FOUND') { - err.message = _t('This email address was not found'); + err.message = _t('This email address was not found'); } else if (err.httpStatus) { err.message = err.message + ` (Status ${err.httpStatus})`; } diff --git a/src/Resend.js b/src/Resend.js index bf69e59c1a..f1e5fb38f5 100644 --- a/src/Resend.js +++ b/src/Resend.js @@ -21,11 +21,11 @@ import { EventStatus } from 'matrix-js-sdk/src/models/event'; export default class Resend { static resendUnsentEvents(room) { - room.getPendingEvents().filter(function(ev) { + return Promise.all(room.getPendingEvents().filter(function(ev) { return ev.status === EventStatus.NOT_SENT; - }).forEach(function(event) { - Resend.resend(event); - }); + }).map(function(event) { + return Resend.resend(event); + })); } static cancelUnsentEvents(room) { @@ -38,7 +38,7 @@ export default class Resend { static resend(event) { const room = MatrixClientPeg.get().getRoom(event.getRoomId()); - MatrixClientPeg.get().resendEvent(event, room).then(function(res) { + return MatrixClientPeg.get().resendEvent(event, room).then(function(res) { dis.dispatch({ action: 'message_sent', event: event, diff --git a/src/ScalarAuthClient.js b/src/ScalarAuthClient.ts similarity index 84% rename from src/ScalarAuthClient.js rename to src/ScalarAuthClient.ts index 200b4fd7b9..a09c3494a8 100644 --- a/src/ScalarAuthClient.js +++ b/src/ScalarAuthClient.ts @@ -1,6 +1,5 @@ /* -Copyright 2016 OpenMarket Ltd -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2016, 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. @@ -17,13 +16,14 @@ limitations under the License. import url from 'url'; import SettingsStore from "./settings/SettingsStore"; -import { Service, startTermsFlow, TermsNotSignedError } from './Terms'; +import { Service, startTermsFlow, TermsInteractionCallback, TermsNotSignedError } from './Terms'; import {MatrixClientPeg} from "./MatrixClientPeg"; import request from "browser-request"; import SdkConfig from "./SdkConfig"; import {WidgetType} from "./widgets/WidgetType"; import {SERVICE_TYPES} from "matrix-js-sdk/src/service-types"; +import { Room } from "matrix-js-sdk/src/models/room"; // The version of the integration manager API we're intending to work with const imApiVersion = "1.1"; @@ -31,9 +31,11 @@ const imApiVersion = "1.1"; // TODO: Generify the name of this class and all components within - it's not just for Scalar. export default class ScalarAuthClient { - constructor(apiUrl, uiUrl) { - this.apiUrl = apiUrl; - this.uiUrl = uiUrl; + private scalarToken: string; + private termsInteractionCallback: TermsInteractionCallback; + private isDefaultManager: boolean; + + constructor(private apiUrl: string, private uiUrl: string) { this.scalarToken = null; // `undefined` to allow `startTermsFlow` to fallback to a default // callback if this is unset. @@ -46,7 +48,7 @@ export default class ScalarAuthClient { this.isDefaultManager = apiUrl === configApiUrl && configUiUrl === uiUrl; } - _writeTokenToStore() { + private writeTokenToStore() { window.localStorage.setItem("mx_scalar_token_at_" + this.apiUrl, this.scalarToken); if (this.isDefaultManager) { // We remove the old token from storage to migrate upwards. This is safe @@ -56,7 +58,7 @@ export default class ScalarAuthClient { } } - _readTokenFromStore() { + private readTokenFromStore(): string { let token = window.localStorage.getItem("mx_scalar_token_at_" + this.apiUrl); if (!token && this.isDefaultManager) { token = window.localStorage.getItem("mx_scalar_token"); @@ -64,33 +66,33 @@ export default class ScalarAuthClient { return token; } - _readToken() { + private readToken(): string { if (this.scalarToken) return this.scalarToken; - return this._readTokenFromStore(); + return this.readTokenFromStore(); } setTermsInteractionCallback(callback) { this.termsInteractionCallback = callback; } - connect() { + connect(): Promise { return this.getScalarToken().then((tok) => { this.scalarToken = tok; }); } - hasCredentials() { + hasCredentials(): boolean { return this.scalarToken != null; // undef or null } // Returns a promise that resolves to a scalar_token string - getScalarToken() { - const token = this._readToken(); + getScalarToken(): Promise { + const token = this.readToken(); if (!token) { return this.registerForToken(); } else { - return this._checkToken(token).catch((e) => { + return this.checkToken(token).catch((e) => { if (e instanceof TermsNotSignedError) { // retrying won't help this throw e; @@ -100,7 +102,7 @@ export default class ScalarAuthClient { } } - _getAccountName(token) { + private getAccountName(token: string): Promise { const url = this.apiUrl + "/account"; return new Promise(function(resolve, reject) { @@ -125,8 +127,8 @@ export default class ScalarAuthClient { }); } - _checkToken(token) { - return this._getAccountName(token).then(userId => { + private checkToken(token: string): Promise { + return this.getAccountName(token).then(userId => { const me = MatrixClientPeg.get().getUserId(); if (userId !== me) { throw new Error("Scalar token is owned by someone else: " + me); @@ -154,7 +156,7 @@ export default class ScalarAuthClient { parsedImRestUrl.pathname = ''; return startTermsFlow([new Service( SERVICE_TYPES.IM, - parsedImRestUrl.format(), + url.format(parsedImRestUrl), token, )], this.termsInteractionCallback).then(() => { return token; @@ -165,22 +167,22 @@ export default class ScalarAuthClient { }); } - registerForToken() { + registerForToken(): Promise { // Get openid bearer token from the HS as the first part of our dance return MatrixClientPeg.get().getOpenIdToken().then((tokenObject) => { // Now we can send that to scalar and exchange it for a scalar token return this.exchangeForScalarToken(tokenObject); }).then((token) => { // Validate it (this mostly checks to see if the IM needs us to agree to some terms) - return this._checkToken(token); + return this.checkToken(token); }).then((token) => { this.scalarToken = token; - this._writeTokenToStore(); + this.writeTokenToStore(); return token; }); } - exchangeForScalarToken(openidTokenObject) { + exchangeForScalarToken(openidTokenObject: any): Promise { const scalarRestUrl = this.apiUrl; return new Promise(function(resolve, reject) { @@ -194,7 +196,7 @@ export default class ScalarAuthClient { if (err) { reject(err); } else if (response.statusCode / 100 !== 2) { - reject({statusCode: response.statusCode}); + reject(new Error(`Scalar request failed: ${response.statusCode}`)); } else if (!body || !body.scalar_token) { reject(new Error("Missing scalar_token in response")); } else { @@ -204,7 +206,7 @@ export default class ScalarAuthClient { }); } - getScalarPageTitle(url) { + getScalarPageTitle(url: string): Promise { let scalarPageLookupUrl = this.apiUrl + '/widgets/title_lookup'; scalarPageLookupUrl = this.getStarterLink(scalarPageLookupUrl); scalarPageLookupUrl += '&curl=' + encodeURIComponent(url); @@ -218,7 +220,7 @@ export default class ScalarAuthClient { if (err) { reject(err); } else if (response.statusCode / 100 !== 2) { - reject({statusCode: response.statusCode}); + reject(new Error(`Scalar request failed: ${response.statusCode}`)); } else if (!body) { reject(new Error("Missing page title in response")); } else { @@ -240,10 +242,10 @@ export default class ScalarAuthClient { * @param {string} widgetId The widget ID to disable assets for * @return {Promise} Resolves on completion */ - disableWidgetAssets(widgetType: WidgetType, widgetId) { + disableWidgetAssets(widgetType: WidgetType, widgetId: string): Promise { let url = this.apiUrl + '/widgets/set_assets_state'; url = this.getStarterLink(url); - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { request({ method: 'GET', // XXX: Actions shouldn't be GET requests uri: url, @@ -257,7 +259,7 @@ export default class ScalarAuthClient { if (err) { reject(err); } else if (response.statusCode / 100 !== 2) { - reject({statusCode: response.statusCode}); + reject(new Error(`Scalar request failed: ${response.statusCode}`)); } else if (!body) { reject(new Error("Failed to set widget assets state")); } else { @@ -267,7 +269,7 @@ export default class ScalarAuthClient { }); } - getScalarInterfaceUrlForRoom(room, screen, id) { + getScalarInterfaceUrlForRoom(room: Room, screen: string, id: string): string { const roomId = room.roomId; const roomName = room.name; let url = this.uiUrl; @@ -284,7 +286,7 @@ export default class ScalarAuthClient { return url; } - getStarterLink(starterLinkUrl) { + getStarterLink(starterLinkUrl: string): string { return starterLinkUrl + "?scalar_token=" + encodeURIComponent(this.scalarToken); } } diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx index 6ce1439164..4a7b37b5e5 100644 --- a/src/SlashCommands.tsx +++ b/src/SlashCommands.tsx @@ -38,7 +38,7 @@ import {isPermalinkHost, parsePermalink} from "./utils/permalinks/Permalinks"; import {inviteUsersToRoom} from "./RoomInvite"; import { WidgetType } from "./widgets/WidgetType"; import { Jitsi } from "./widgets/Jitsi"; -import { parseFragment as parseHtml } from "parse5"; +import { parseFragment as parseHtml, Element as ChildElement } from "parse5"; import BugReportDialog from "./components/views/dialogs/BugReportDialog"; import { ensureDMExists } from "./createRoom"; import { ViewUserPayload } from "./dispatcher/payloads/ViewUserPayload"; @@ -856,7 +856,7 @@ export const Commands = [ // some superfast regex over the text so we don't have to. const embed = parseHtml(widgetUrl); if (embed && embed.childNodes && embed.childNodes.length === 1) { - const iframe = embed.childNodes[0]; + const iframe = embed.childNodes[0] as ChildElement; if (iframe.tagName.toLowerCase() === 'iframe' && iframe.attrs) { const srcAttr = iframe.attrs.find(a => a.name === 'src'); console.log("Pulling URL out of iframe (embed code)"); diff --git a/src/Terms.js b/src/Terms.ts similarity index 87% rename from src/Terms.js rename to src/Terms.ts index 6ae89f9a2c..1bdff36cbc 100644 --- a/src/Terms.js +++ b/src/Terms.ts @@ -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. @@ -17,7 +17,7 @@ limitations under the License. import classNames from 'classnames'; import {MatrixClientPeg} from './MatrixClientPeg'; -import * as sdk from './'; +import * as sdk from '.'; import Modal from './Modal'; export class TermsNotSignedError extends Error {} @@ -32,13 +32,30 @@ 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(serviceType, baseUrl, accessToken) { - this.serviceType = serviceType; - this.baseUrl = baseUrl; - this.accessToken = accessToken; + constructor(public serviceType: string, public baseUrl: string, public accessToken: string) { } } +interface Policy { + // @ts-ignore: No great way to express indexed types together with other keys + version: string; + [lang: string]: { + url: string; + }; +} +type Policies = { + [policy: string]: Policy, +}; + +export type TermsInteractionCallback = ( + policiesAndServicePairs: { + service: Service, + policies: Policies, + }[], + agreedUrls: string[], + extraClassNames?: string, +) => Promise; + /** * Start a flow where the user is presented with terms & conditions for some services * @@ -51,8 +68,8 @@ export class Service { * if they cancel. */ export async function startTermsFlow( - services, - interactionCallback = dialogTermsInteractionCallback, + services: Service[], + interactionCallback: TermsInteractionCallback = dialogTermsInteractionCallback, ) { const termsPromises = services.map( (s) => MatrixClientPeg.get().getTerms(s.serviceType, s.baseUrl), @@ -77,7 +94,7 @@ export async function startTermsFlow( * } */ - const terms = await Promise.all(termsPromises); + const terms: { policies: Policies }[] = await Promise.all(termsPromises); const policiesAndServicePairs = terms.map((t, i) => { return { 'service': services[i], 'policies': t.policies }; }); // fetch the set of agreed policy URLs from account data @@ -158,10 +175,13 @@ export async function startTermsFlow( } export function dialogTermsInteractionCallback( - policiesAndServicePairs, - agreedUrls, - extraClassNames, -) { + policiesAndServicePairs: { + service: Service, + policies: { [policy: string]: Policy }, + }[], + agreedUrls: string[], + extraClassNames?: string, +): Promise { return new Promise((resolve, reject) => { console.log("Terms that need agreement", policiesAndServicePairs); const TermsDialog = sdk.getComponent("views.dialogs.TermsDialog"); diff --git a/src/TextForEvent.js b/src/TextForEvent.js index a6787c647d..86f9ff20f4 100644 --- a/src/TextForEvent.js +++ b/src/TextForEvent.js @@ -547,17 +547,23 @@ function textForMjolnirEvent(event) { // else the entity !== prevEntity - count as a removal & add if (USER_RULE_TYPES.includes(event.getType())) { - return _t("%(senderName)s changed a rule that was banning users matching %(oldGlob)s to matching " + + return _t( + "%(senderName)s changed a rule that was banning users matching %(oldGlob)s to matching " + "%(newGlob)s for %(reason)s", - {senderName, oldGlob: prevEntity, newGlob: entity, reason}); + {senderName, oldGlob: prevEntity, newGlob: entity, reason}, + ); } else if (ROOM_RULE_TYPES.includes(event.getType())) { - return _t("%(senderName)s changed a rule that was banning rooms matching %(oldGlob)s to matching " + + return _t( + "%(senderName)s changed a rule that was banning rooms matching %(oldGlob)s to matching " + "%(newGlob)s for %(reason)s", - {senderName, oldGlob: prevEntity, newGlob: entity, reason}); + {senderName, oldGlob: prevEntity, newGlob: entity, reason}, + ); } else if (SERVER_RULE_TYPES.includes(event.getType())) { - return _t("%(senderName)s changed a rule that was banning servers matching %(oldGlob)s to matching " + + return _t( + "%(senderName)s changed a rule that was banning servers matching %(oldGlob)s to matching " + "%(newGlob)s for %(reason)s", - {senderName, oldGlob: prevEntity, newGlob: entity, reason}); + {senderName, oldGlob: prevEntity, newGlob: entity, reason}, + ); } // Unknown type. We'll say something but we shouldn't end up here. diff --git a/src/Unread.js b/src/Unread.js index ddf225ac64..12c15eb6af 100644 --- a/src/Unread.js +++ b/src/Unread.js @@ -45,7 +45,7 @@ export function eventTriggersUnreadCount(ev) { } export function doesRoomHaveUnreadMessages(room) { - const myUserId = MatrixClientPeg.get().credentials.userId; + const myUserId = MatrixClientPeg.get().getUserId(); // get the most recent read receipt sent by our account. // N.B. this is NOT a read marker (RM, aka "read up to marker"), diff --git a/src/VoipUserMapper.ts b/src/VoipUserMapper.ts index 4f5613b4a8..e5bed2e812 100644 --- a/src/VoipUserMapper.ts +++ b/src/VoipUserMapper.ts @@ -57,7 +57,11 @@ export default class VoipUserMapper { if (!virtualRoom) return null; const virtualRoomEvent = virtualRoom.getAccountData(VIRTUAL_ROOM_EVENT_TYPE); if (!virtualRoomEvent || !virtualRoomEvent.getContent()) return null; - return virtualRoomEvent.getContent()['native_room'] || null; + const nativeRoomID = virtualRoomEvent.getContent()['native_room']; + const nativeRoom = MatrixClientPeg.get().getRoom(nativeRoomID); + if (!nativeRoom || nativeRoom.getMyMembership() !== 'join') return null; + + return nativeRoomID; } public isVirtualRoom(room: Room): boolean { diff --git a/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.js b/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.tsx similarity index 90% rename from src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.js rename to src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.tsx index be3368b87b..0710c513da 100644 --- a/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.js +++ b/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.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. @@ -16,7 +16,6 @@ limitations under the License. import React from 'react'; import * as sdk from '../../../../index'; -import PropTypes from 'prop-types'; import { _t } from '../../../../languageHandler'; import SdkConfig from '../../../../SdkConfig'; import SettingsStore from "../../../../settings/SettingsStore"; @@ -26,14 +25,23 @@ import {formatBytes, formatCountLong} from "../../../../utils/FormattingUtils"; import EventIndexPeg from "../../../../indexing/EventIndexPeg"; import {SettingLevel} from "../../../../settings/SettingLevel"; +interface IProps { + onFinished: (confirmed: boolean) => void; +} + +interface IState { + eventIndexSize: number; + eventCount: number; + crawlingRoomsCount: number; + roomCount: number; + currentRoom: string; + crawlerSleepTime: number; +} + /* * Allows the user to introspect the event index state and disable it. */ -export default class ManageEventIndexDialog extends React.Component { - static propTypes = { - onFinished: PropTypes.func.isRequired, - }; - +export default class ManageEventIndexDialog extends React.Component { constructor(props) { super(props); @@ -84,7 +92,7 @@ export default class ManageEventIndexDialog extends React.Component { } } - async componentDidMount(): void { + async componentDidMount(): Promise { let eventIndexSize = 0; let crawlingRoomsCount = 0; let roomCount = 0; @@ -123,14 +131,14 @@ export default class ManageEventIndexDialog extends React.Component { }); } - _onDisable = async () => { + private onDisable = async () => { Modal.createTrackedDialogAsync("Disable message search", "Disable message search", import("./DisableEventIndexDialog"), null, null, /* priority = */ false, /* static = */ true, ); }; - _onCrawlerSleepTimeChange = (e) => { + private onCrawlerSleepTimeChange = (e) => { this.setState({crawlerSleepTime: e.target.value}); SettingsStore.setValue("crawlerSleepTime", null, SettingLevel.DEVICE, e.target.value); }; @@ -144,7 +152,7 @@ export default class ManageEventIndexDialog extends React.Component { crawlerState = _t("Not currently indexing messages for any room."); } else { crawlerState = ( - _t("Currently indexing: %(currentRoom)s", { currentRoom: this.state.currentRoom }) + _t("Currently indexing: %(currentRoom)s", { currentRoom: this.state.currentRoom }) ); } @@ -169,7 +177,7 @@ export default class ManageEventIndexDialog extends React.Component { label={_t('Message downloading sleep time(ms)')} type='number' value={this.state.crawlerSleepTime} - onChange={this._onCrawlerSleepTimeChange} /> + onChange={this.onCrawlerSleepTimeChange} />
      ); @@ -188,7 +196,7 @@ export default class ManageEventIndexDialog extends React.Component { onPrimaryButtonClick={this.props.onFinished} primaryButtonClass="primary" cancelButton={_t("Disable")} - onCancel={this._onDisable} + onCancel={this.onDisable} cancelButtonClass="danger" /> diff --git a/src/async-components/views/dialogs/security/CreateKeyBackupDialog.js b/src/async-components/views/dialogs/security/CreateKeyBackupDialog.js index 863ee2b427..549494b5cb 100644 --- a/src/async-components/views/dialogs/security/CreateKeyBackupDialog.js +++ b/src/async-components/views/dialogs/security/CreateKeyBackupDialog.js @@ -310,7 +310,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent { const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); return

      {_t( - "Please enter your Security Phrase a second time to confirm.", + "Enter your Security Phrase a second time to confirm it.", )}

      @@ -498,9 +498,9 @@ export default class CreateKeyBackupDialog extends React.PureComponent { title={this._titleForPhase(this.state.phase)} hasCancel={[PHASE_PASSPHRASE, PHASE_DONE].includes(this.state.phase)} > -
      - {content} -
      +
      + {content} +
      ); } diff --git a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.js b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.js index 84cb58536a..6d5703a768 100644 --- a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.js +++ b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.js @@ -647,7 +647,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { } return

      {_t( - "Enter your recovery passphrase a second time to confirm it.", + "Enter your Security Phrase a second time to confirm it.", )}

      @@ -856,9 +856,9 @@ export default class CreateSecretStorageDialog extends React.PureComponent { hasCancel={this.props.hasCancel && [PHASE_PASSPHRASE].includes(this.state.phase)} fixedWidth={false} > -
      - {content} -
      +
      + {content} +
      ); } diff --git a/src/async-components/views/dialogs/security/ExportE2eKeysDialog.js b/src/async-components/views/dialogs/security/ExportE2eKeysDialog.js index eeb68b94bd..60f2ca9168 100644 --- a/src/async-components/views/dialogs/security/ExportE2eKeysDialog.js +++ b/src/async-components/views/dialogs/security/ExportE2eKeysDialog.js @@ -170,8 +170,11 @@ export default class ExportE2eKeysDialog extends React.Component {
      -
      -
      - -
      -
      - -
      +
      + +
      +
      + +
      -
      - -
      -
      - -
      +
      + +
      +
      + +
      diff --git a/src/components/structures/FilePanel.js b/src/components/structures/FilePanel.js index 32db5c251c..d5e4b092e2 100644 --- a/src/components/structures/FilePanel.js +++ b/src/components/structures/FilePanel.js @@ -200,10 +200,10 @@ class FilePanel extends React.Component { previousPhase={RightPanelPhases.RoomSummary} >
      - { _t("You must register to use this functionality", - {}, - { 'a': (sub) => { sub } }) - } + { _t("You must register to use this functionality", + {}, + { 'a': (sub) => { sub } }) + }
      ; } else if (this.noRoom) { diff --git a/src/components/structures/GroupFilterPanel.js b/src/components/structures/GroupFilterPanel.js index 976b2d81a5..7c050e7433 100644 --- a/src/components/structures/GroupFilterPanel.js +++ b/src/components/structures/GroupFilterPanel.js @@ -153,17 +153,17 @@ class GroupFilterPanel extends React.Component { type="draggable-TagTile" > { (provided, snapshot) => ( -
      - { this.renderGlobalIcon() } - { tags } -
      - {createButton} -
      - { provided.placeholder } +
      + { this.renderGlobalIcon() } + { tags } +
      + {createButton}
      + { provided.placeholder } +
      ) } diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index ed6167cbe7..3ab009d7b8 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -43,7 +43,7 @@ import {mediaFromMxc} from "../../customisations/Media"; import {replaceableComponent} from "../../utils/replaceableComponent"; const LONG_DESC_PLACEHOLDER = _td( -`

      HTML for your community's page

      + `

      HTML for your community's page

      Use the long description to introduce new members to the community, or distribute some important links @@ -110,14 +110,16 @@ class CategoryRoomList extends React.Component { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createTrackedDialog( 'Failed to add the following room to the group summary', - '', ErrorDialog, - { - title: _t( - "Failed to add the following rooms to the summary of %(groupId)s:", - {groupId: this.props.groupId}, - ), - description: errorList.join(", "), - }); + '', + ErrorDialog, + { + title: _t( + "Failed to add the following rooms to the summary of %(groupId)s:", + {groupId: this.props.groupId}, + ), + description: errorList.join(", "), + }, + ); }); }, }, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true); @@ -146,8 +148,8 @@ class CategoryRoomList extends React.Component { let catHeader =

      ; if (this.props.category && this.props.category.profile) { catHeader =
      - { this.props.category.profile.name } -
      ; + { this.props.category.profile.name } +
      ; } return
      { catHeader } @@ -190,13 +192,14 @@ class FeaturedRoom extends React.Component { Modal.createTrackedDialog( 'Failed to remove room from group summary', '', ErrorDialog, - { - title: _t( - "Failed to remove the room from the summary of %(groupId)s", - {groupId: this.props.groupId}, - ), - description: _t("The room '%(roomName)s' could not be removed from the summary.", {roomName}), - }); + { + title: _t( + "Failed to remove the room from the summary of %(groupId)s", + {groupId: this.props.groupId}, + ), + description: _t("The room '%(roomName)s' could not be removed from the summary.", {roomName}), + }, + ); }); }; @@ -283,13 +286,14 @@ class RoleUserList extends React.Component { Modal.createTrackedDialog( 'Failed to add the following users to the community summary', '', ErrorDialog, - { - title: _t( - "Failed to add the following users to the summary of %(groupId)s:", - {groupId: this.props.groupId}, - ), - description: errorList.join(", "), - }); + { + title: _t( + "Failed to add the following users to the summary of %(groupId)s:", + {groupId: this.props.groupId}, + ), + description: errorList.join(", "), + }, + ); }); }, }, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true); @@ -299,11 +303,11 @@ class RoleUserList extends React.Component { const TintableSvg = sdk.getComponent("elements.TintableSvg"); const addButton = this.props.editing ? ( - -
      - { _t('Add a User') } -
      -
      ) :
      ; + +
      + { _t('Add a User') } +
      + ) :
      ; const userNodes = this.props.users.map((u) => { return - { _t("Leave %(groupName)s?", {groupName: this.props.groupId}) } - { warnings } + { _t("Leave %(groupName)s?", {groupName: this.props.groupId}) } + { warnings } ), button: _t("Leave"), @@ -1055,10 +1061,11 @@ export default class GroupView extends React.Component { return null; } - const membershipButtonClasses = classnames([ - 'mx_RoomHeader_textButton', - 'mx_GroupView_textButton', - ], + const membershipButtonClasses = classnames( + [ + 'mx_RoomHeader_textButton', + 'mx_GroupView_textButton', + ], membershipButtonExtraClasses, ); diff --git a/src/components/structures/LeftPanel.tsx b/src/components/structures/LeftPanel.tsx index e4762e35ad..7f9ef7516e 100644 --- a/src/components/structures/LeftPanel.tsx +++ b/src/components/structures/LeftPanel.tsx @@ -347,7 +347,7 @@ export default class LeftPanel extends React.Component { if (element) { classes = element.classList; } - } while (element && !cssClasses.some(c => classes.contains(c))); + } while (element && (!cssClasses.some(c => classes.contains(c)) || element.offsetParent === null)); if (element) { element.focus(); @@ -416,7 +416,7 @@ export default class LeftPanel extends React.Component { const roomList = ; } /** @@ -160,6 +164,7 @@ class LoggedInView extends React.Component { // use compact timeline view useCompactLayout: SettingsStore.getValue('useCompactLayout'), usageLimitDismissed: false, + activeCalls: [], }; // stash the MatrixClient in case we log out before we are unmounted @@ -175,6 +180,7 @@ class LoggedInView extends React.Component { componentDidMount() { document.addEventListener('keydown', this._onNativeKeyDown, false); + CallHandler.sharedInstance().addListener(CallHandlerEvent.CallsChanged, this.onCallsChanged); this._updateServerNoticeEvents(); @@ -199,6 +205,7 @@ class LoggedInView extends React.Component { componentWillUnmount() { document.removeEventListener('keydown', this._onNativeKeyDown, false); + CallHandler.sharedInstance().removeListener(CallHandlerEvent.CallsChanged, this.onCallsChanged); this._matrixClient.removeListener("accountData", this.onAccountData); this._matrixClient.removeListener("sync", this.onSync); this._matrixClient.removeListener("RoomState.events", this.onRoomStateEvents); @@ -206,6 +213,12 @@ class LoggedInView extends React.Component { this.resizer.detach(); } + private onCallsChanged = () => { + this.setState({ + activeCalls: CallHandler.sharedInstance().getAllActiveCalls(), + }); + }; + // Child components assume that the client peg will not be null, so give them some // sort of assurance here by only allowing a re-render if the client is truthy. // @@ -661,6 +674,12 @@ class LoggedInView extends React.Component { bodyClasses += ' mx_MatrixChat_useCompactLayout'; } + const audioFeedArraysForCalls = this.state.activeCalls.map((call) => { + return ( + + ); + }); + return (
      { + {audioFeedArraysForCalls} ); } diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 078b296295..e330dc7d38 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -1094,7 +1094,7 @@ export default class MatrixChat extends React.PureComponent { private leaveRoomWarnings(roomId: string) { const roomToLeave = MatrixClientPeg.get().getRoom(roomId); - const isSpace = roomToLeave?.isSpaceRoom(); + const isSpace = SettingsStore.getValue("feature_spaces") && roomToLeave?.isSpaceRoom(); // Show a warning if there are additional complications. const warnings = []; @@ -1133,7 +1133,7 @@ export default class MatrixChat extends React.PureComponent { const roomToLeave = MatrixClientPeg.get().getRoom(roomId); const warnings = this.leaveRoomWarnings(roomId); - const isSpace = roomToLeave?.isSpaceRoom(); + const isSpace = SettingsStore.getValue("feature_spaces") && roomToLeave?.isSpaceRoom(); Modal.createTrackedDialog(isSpace ? "Leave space" : "Leave room", '', QuestionDialog, { title: isSpace ? _t("Leave space") : _t("Leave room"), description: ( diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 132d9ab4c3..c93f07fa0f 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -427,8 +427,10 @@ export default class MessagePanel extends React.Component { // we get a new DOM node (restarting the animation) when the ghost // moves to a different event. return ( -
    1. +
    2. { hr }
    3. ); @@ -1014,13 +1016,13 @@ class CreationGrouper { ret.push( - { eventTiles } + { eventTiles } , ); @@ -1222,11 +1224,11 @@ class MemberGrouper { ret.push( - { eventTiles } + { eventTiles } , ); diff --git a/src/components/structures/RightPanel.js b/src/components/structures/RightPanel.js index 5bcb3b2450..d8c763eabd 100644 --- a/src/components/structures/RightPanel.js +++ b/src/components/structures/RightPanel.js @@ -35,6 +35,7 @@ import {Action} from "../../dispatcher/actions"; import RoomSummaryCard from "../views/right_panel/RoomSummaryCard"; import WidgetCard from "../views/right_panel/WidgetCard"; import {replaceableComponent} from "../../utils/replaceableComponent"; +import SettingsStore from "../../settings/SettingsStore"; @replaceableComponent("structures.RightPanel") export default class RightPanel extends React.Component { @@ -85,7 +86,9 @@ export default class RightPanel extends React.Component { return RightPanelPhases.GroupMemberList; } return rps.groupPanelPhase; - } else if (this.props.room?.isSpaceRoom() && !RIGHT_PANEL_SPACE_PHASES.includes(rps.roomPanelPhase)) { + } else if (SettingsStore.getValue("feature_spaces") && this.props.room?.isSpaceRoom() + && !RIGHT_PANEL_SPACE_PHASES.includes(rps.roomPanelPhase) + ) { return RightPanelPhases.SpaceMemberList; } else if (userForPanel) { // XXX FIXME AAAAAARGH: What is going on with this class!? It takes some of its state diff --git a/src/components/structures/RoomSearch.tsx b/src/components/structures/RoomSearch.tsx index a64feed42c..34682877e0 100644 --- a/src/components/structures/RoomSearch.tsx +++ b/src/components/structures/RoomSearch.tsx @@ -17,6 +17,8 @@ limitations under the License. import * as React from "react"; import { createRef } from "react"; import classNames from "classnames"; +import { Room } from "matrix-js-sdk/src/models/room"; + import defaultDispatcher from "../../dispatcher/dispatcher"; import { _t } from "../../languageHandler"; import { ActionPayload } from "../../dispatcher/payloads"; @@ -26,7 +28,7 @@ import RoomListStore from "../../stores/room-list/RoomListStore"; import { NameFilterCondition } from "../../stores/room-list/filters/NameFilterCondition"; import { getKeyBindingsManager, RoomListAction } from "../../KeyBindingsManager"; import {replaceableComponent} from "../../utils/replaceableComponent"; -import SpaceStore, {UPDATE_SELECTED_SPACE} from "../../stores/SpaceStore"; +import SpaceStore, {UPDATE_SELECTED_SPACE, UPDATE_TOP_LEVEL_SPACES} from "../../stores/SpaceStore"; interface IProps { isMinimized: boolean; @@ -40,6 +42,7 @@ interface IProps { interface IState { query: string; focused: boolean; + inSpaces: boolean; } @replaceableComponent("structures.RoomSearch") @@ -54,11 +57,13 @@ export default class RoomSearch extends React.PureComponent { this.state = { query: "", focused: false, + inSpaces: false, }; this.dispatcherRef = defaultDispatcher.register(this.onAction); // clear filter when changing spaces, in future we may wish to maintain a filter per-space SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.clearInput); + SpaceStore.instance.on(UPDATE_TOP_LEVEL_SPACES, this.onSpaces); } public componentDidUpdate(prevProps: Readonly, prevState: Readonly): void { @@ -79,8 +84,15 @@ export default class RoomSearch extends React.PureComponent { public componentWillUnmount() { defaultDispatcher.unregister(this.dispatcherRef); SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.clearInput); + SpaceStore.instance.off(UPDATE_TOP_LEVEL_SPACES, this.onSpaces); } + private onSpaces = (spaces: Room[]) => { + this.setState({ + inSpaces: spaces.length > 0, + }); + }; + private onAction = (payload: ActionPayload) => { if (payload.action === 'view_room' && payload.clear_search) { this.clearInput(); @@ -152,6 +164,11 @@ export default class RoomSearch extends React.PureComponent { 'mx_RoomSearch_inputExpanded': this.state.query || this.state.focused, }); + let placeholder = _t("Filter"); + if (this.state.inSpaces) { + placeholder = _t("Filter all spaces"); + } + let icon = (
      ); @@ -165,7 +182,7 @@ export default class RoomSearch extends React.PureComponent { onBlur={this.onBlur} onChange={this.onChange} onKeyDown={this.onKeyDown} - placeholder={_t("Filter")} + placeholder={placeholder} autoComplete="off" /> ); diff --git a/src/components/structures/RoomStatusBar.js b/src/components/structures/RoomStatusBar.js index 54b6fee233..b2f0c70bd7 100644 --- a/src/components/structures/RoomStatusBar.js +++ b/src/components/structures/RoomStatusBar.js @@ -1,5 +1,5 @@ /* -Copyright 2015-2020 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. @@ -20,16 +20,20 @@ import { _t, _td } from '../../languageHandler'; import {MatrixClientPeg} from '../../MatrixClientPeg'; import Resend from '../../Resend'; import dis from '../../dispatcher/dispatcher'; -import {messageForResourceLimitError, messageForSendError} from '../../utils/ErrorUtils'; +import {messageForResourceLimitError} from '../../utils/ErrorUtils'; import {Action} from "../../dispatcher/actions"; import {replaceableComponent} from "../../utils/replaceableComponent"; import {EventStatus} from "matrix-js-sdk/src/models/event"; +import NotificationBadge from "../views/rooms/NotificationBadge"; +import {StaticNotificationState} from "../../stores/notifications/StaticNotificationState"; +import AccessibleButton from "../views/elements/AccessibleButton"; +import InlineSpinner from "../views/elements/InlineSpinner"; const STATUS_BAR_HIDDEN = 0; const STATUS_BAR_EXPANDED = 1; const STATUS_BAR_EXPANDED_LARGE = 2; -function getUnsentMessages(room) { +export function getUnsentMessages(room) { if (!room) { return []; } return room.getPendingEvents().filter(function(ev) { return ev.status === EventStatus.NOT_SENT; @@ -76,6 +80,7 @@ export default class RoomStatusBar extends React.Component { syncState: MatrixClientPeg.get().getSyncState(), syncStateData: MatrixClientPeg.get().getSyncStateData(), unsentMessages: getUnsentMessages(this.props.room), + isResending: false, }; componentDidMount() { @@ -109,7 +114,10 @@ export default class RoomStatusBar extends React.Component { }; _onResendAllClick = () => { - Resend.resendUnsentEvents(this.props.room); + Resend.resendUnsentEvents(this.props.room).then(() => { + this.setState({isResending: false}); + }); + this.setState({isResending: true}); dis.fire(Action.FocusComposer); }; @@ -120,9 +128,10 @@ export default class RoomStatusBar extends React.Component { _onRoomLocalEchoUpdated = (event, room, oldEventId, oldStatus) => { if (room.roomId !== this.props.room.roomId) return; - + const messages = getUnsentMessages(this.props.room); this.setState({ - unsentMessages: getUnsentMessages(this.props.room), + unsentMessages: messages, + isResending: messages.length > 0 && this.state.isResending, }); }; @@ -141,7 +150,7 @@ export default class RoomStatusBar extends React.Component { _getSize() { if (this._shouldShowConnectionError()) { return STATUS_BAR_EXPANDED; - } else if (this.state.unsentMessages.length > 0) { + } else if (this.state.unsentMessages.length > 0 || this.state.isResending) { return STATUS_BAR_EXPANDED_LARGE; } return STATUS_BAR_HIDDEN; @@ -162,7 +171,6 @@ export default class RoomStatusBar extends React.Component { _getUnsentMessageContent() { const unsentMessages = this.state.unsentMessages; - if (!unsentMessages.length) return null; let title; @@ -192,89 +200,92 @@ export default class RoomStatusBar extends React.Component { } else if (resourceLimitError) { title = messageForResourceLimitError( resourceLimitError.data.limit_type, - resourceLimitError.data.admin_contact, { - 'monthly_active_user': _td( - "Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. " + - "Please contact your service administrator to continue using the service.", - ), - 'hs_disabled': _td( - "Your message wasn't sent because this homeserver has been blocked by it's administrator. " + - "Please contact your service administrator to continue using the service.", - ), - '': _td( - "Your message wasn't sent because this homeserver has exceeded a resource limit. " + - "Please contact your service administrator to continue using the service.", - ), - }); - } else if ( - unsentMessages.length === 1 && - unsentMessages[0].error && - unsentMessages[0].error.data && - unsentMessages[0].error.data.error - ) { - title = messageForSendError(unsentMessages[0].error.data) || unsentMessages[0].error.data.error; + resourceLimitError.data.admin_contact, + { + 'monthly_active_user': _td( + "Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. " + + "Please contact your service administrator to continue using the service.", + ), + 'hs_disabled': _td( + "Your message wasn't sent because this homeserver has been blocked by it's administrator. " + + "Please contact your service administrator to continue using the service.", + ), + '': _td( + "Your message wasn't sent because this homeserver has exceeded a resource limit. " + + "Please contact your service administrator to continue using the service.", + ), + }, + ); } else { - title = _t('%(count)s of your messages have not been sent.', { count: unsentMessages.length }); + title = _t('Some of your messages have not been sent'); } - const content = _t("%(count)s Resend all or cancel all " + - "now. You can also select individual messages to resend or cancel.", - { count: unsentMessages.length }, - { - 'resendText': (sub) => - { sub }, - 'cancelText': (sub) => - { sub }, - }, - ); + let buttonRow = <> + + {_t("Delete all")} + + + {_t("Retry all")} + + ; + if (this.state.isResending) { + buttonRow = <> + + {/* span for css */} + {_t("Sending")} + ; + } - return
      - -
      -
      - { title } -
      -
      - { content } + return <> +
      +
      +
      + +
      +
      +
      + { title } +
      +
      + { _t("You can select all or individual messages to retry or delete") } +
      +
      +
      + {buttonRow} +
      -
      ; + ; } - // return suitable content for the main (text) part of the status bar. - _getContent() { + render() { if (this._shouldShowConnectionError()) { return ( -
      - /!\ -
      -
      - { _t('Connectivity to the server has been lost.') } -
      -
      - { _t('Sent messages will be stored until your connection has returned.') } +
      +
      +
      + /!\ +
      +
      + {_t('Connectivity to the server has been lost.')} +
      +
      + {_t('Sent messages will be stored until your connection has returned.')} +
      +
      ); } - if (this.state.unsentMessages.length > 0) { + if (this.state.unsentMessages.length > 0 || this.state.isResending) { return this._getUnsentMessageContent(); } return null; } - - render() { - const content = this._getContent(); - - return ( -
      -
      - { content } -
      -
      - ); - } } diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 7168b7d139..58a87e6641 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -190,6 +190,9 @@ export interface IState { rejectError?: Error; hasPinnedWidgets?: boolean; dragCounter: number; + // whether or not a spaces context switch brought us here, + // if it did we don't want the room to be marked as read as soon as it is loaded. + wasContextSwitch?: boolean; } @replaceableComponent("structures.RoomView") @@ -326,6 +329,7 @@ export default class RoomView extends React.Component { shouldPeek: this.state.matrixClientIsReady && RoomViewStore.shouldPeek(), showingPinned: SettingsStore.getValue("PinnedEvents.isOpen", roomId), showReadReceipts: SettingsStore.getValue("showReadReceipts", roomId), + wasContextSwitch: RoomViewStore.getWasContextSwitch(), }; if (!initial && this.state.shouldPeek && !newState.shouldPeek) { @@ -1746,7 +1750,10 @@ export default class RoomView extends React.Component { } const myMembership = this.state.room.getMyMembership(); - if (myMembership === "invite" && !this.state.room.isSpaceRoom()) { // SpaceRoomView handles invites itself + if (myMembership === "invite" + // SpaceRoomView handles invites itself + && (!SettingsStore.getValue("feature_spaces") || !this.state.room.isSpaceRoom()) + ) { if (this.state.joining || this.state.rejecting) { return ( @@ -1888,7 +1895,7 @@ export default class RoomView extends React.Component { room={this.state.room} /> ); - if (!this.state.canPeek && !this.state.room?.isSpaceRoom()) { + if (!this.state.canPeek && (!SettingsStore.getValue("feature_spaces") || !this.state.room?.isSpaceRoom())) { return (
      { previewBar } @@ -2014,6 +2021,7 @@ export default class RoomView extends React.Component { timelineSet={this.state.room.getUnfilteredTimelineSet()} showReadReceipts={this.state.showReadReceipts} manageReadReceipts={!this.state.isPeeking} + sendReadReceiptOnLoad={!this.state.wasContextSwitch} manageReadMarkers={!this.state.isPeeking} hidden={hideMessagePanel} highlightedEventId={highlightedEventId} diff --git a/src/components/structures/ScrollPanel.js b/src/components/structures/ScrollPanel.js index 976734680c..5c5062633d 100644 --- a/src/components/structures/ScrollPanel.js +++ b/src/components/structures/ScrollPanel.js @@ -525,7 +525,7 @@ export default class ScrollPanel extends React.Component { */ scrollRelative = mult => { const scrollNode = this._getScrollNode(); - const delta = mult * scrollNode.clientHeight * 0.5; + const delta = mult * scrollNode.clientHeight * 0.9; scrollNode.scrollBy(0, delta); this._saveScrollState(); }; @@ -884,16 +884,20 @@ export default class ScrollPanel extends React.Component { // give the
        an explicit role=list because Safari+VoiceOver seems to think an ordered-list with // list-style-type: none; is no longer a list - return ( - { this.props.fixedChildren } -
        -
          - { this.props.children } -
        -
        -
        - ); + className={`mx_ScrollPanel ${this.props.className}`} + style={this.props.style} + > + { this.props.fixedChildren } +
        +
          + { this.props.children } +
        +
        + + ); } } diff --git a/src/components/structures/SpaceRoomDirectory.tsx b/src/components/structures/SpaceRoomDirectory.tsx index 930cfa15a9..74415cc58f 100644 --- a/src/components/structures/SpaceRoomDirectory.tsx +++ b/src/components/structures/SpaceRoomDirectory.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, {useMemo, useState} from "react"; +import React, {ReactNode, useMemo, useState} from "react"; import {Room} from "matrix-js-sdk/src/models/room"; import {MatrixClient} from "matrix-js-sdk/src/client"; import {EventType, RoomType} from "matrix-js-sdk/src/@types/event"; @@ -24,7 +24,7 @@ import {sortBy} from "lodash"; import {MatrixClientPeg} from "../../MatrixClientPeg"; import dis from "../../dispatcher/dispatcher"; import {_t} from "../../languageHandler"; -import AccessibleButton from "../views/elements/AccessibleButton"; +import AccessibleButton, {ButtonEvent} from "../views/elements/AccessibleButton"; import BaseDialog from "../views/dialogs/BaseDialog"; import Spinner from "../views/elements/Spinner"; import SearchBox from "./SearchBox"; @@ -39,11 +39,14 @@ import {mediaFromMxc} from "../../customisations/Media"; import InfoTooltip from "../views/elements/InfoTooltip"; import TextWithTooltip from "../views/elements/TextWithTooltip"; import {useStateToggle} from "../../hooks/useStateToggle"; +import {getOrder} from "../../stores/SpaceStore"; +import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton"; interface IHierarchyProps { space: Room; initialText?: string; refreshToken?: any; + additionalButtons?: ReactNode; showRoom(room: ISpaceSummaryRoom, viaServers?: string[], autoJoin?: boolean): void; } @@ -106,8 +109,16 @@ const Tile: React.FC = ({ const cliRoom = cli.getRoom(room.room_id); const myMembership = cliRoom?.getMyMembership(); - const onPreviewClick = () => onViewRoomClick(false); - const onJoinClick = () => onViewRoomClick(true); + const onPreviewClick = (ev: ButtonEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + onViewRoomClick(false); + } + const onJoinClick = (ev: ButtonEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + onViewRoomClick(true); + } let button; if (myMembership === "join") { @@ -136,7 +147,7 @@ const Tile: React.FC = ({ let url: string; if (room.avatar_url) { - url = mediaFromMxc(room.avatar_url).getSquareThumbnailHttp(Math.floor(20 * window.devicePixelRatio)); + url = mediaFromMxc(room.avatar_url).getSquareThumbnailHttp(20); } let description = _t("%(count)s members", { count: room.num_joined_members }); @@ -254,7 +265,11 @@ export const HierarchyLevel = ({ const space = cli.getRoom(spaceId); const hasPermissions = space?.currentState.maySendStateEvent(EventType.SpaceChild, cli.getUserId()); - const sortedChildren = sortBy([...(relations.get(spaceId)?.values() || [])], ev => ev.content.order || null); + const children = Array.from(relations.get(spaceId)?.values() || []); + const sortedChildren = sortBy(children, ev => { + // XXX: Space Summary API doesn't give the child origin_server_ts but once it does we should use it for sorting + return getOrder(ev.content.order, null, ev.state_key); + }); const [subspaces, childRooms] = sortedChildren.reduce((result, ev: ISpaceSummaryEvent) => { const roomId = ev.state_key; if (!rooms.has(roomId)) return result; @@ -312,11 +327,12 @@ export const HierarchyLevel = ({ // mutate argument refreshToken to force a reload export const useSpaceSummary = (cli: MatrixClient, space: Room, refreshToken?: any): [ + null, ISpaceSummaryRoom[], - Map>, - Map>, - Map>, -] | [] => { + Map>?, + Map>?, + Map>?, +] | [Error] => { // TODO pagination return useAsyncMemo(async () => { try { @@ -336,13 +352,12 @@ export const useSpaceSummary = (cli: MatrixClient, space: Room, refreshToken?: a } }); - return [data.rooms as ISpaceSummaryRoom[], parentChildRelations, viaMap, childParentRelations]; + return [null, data.rooms as ISpaceSummaryRoom[], parentChildRelations, viaMap, childParentRelations]; } catch (e) { console.error(e); // TODO + return [e]; } - - return []; - }, [space, refreshToken], []); + }, [space, refreshToken], [undefined]); }; export const SpaceHierarchy: React.FC = ({ @@ -350,6 +365,7 @@ export const SpaceHierarchy: React.FC = ({ initialText = "", showRoom, refreshToken, + additionalButtons, children, }) => { const cli = MatrixClientPeg.get(); @@ -358,7 +374,7 @@ export const SpaceHierarchy: React.FC = ({ const [selected, setSelected] = useState(new Map>()); // Map> - const [rooms, parentChildMap, viaMap, childParentMap] = useSpaceSummary(cli, space, refreshToken); + const [summaryError, rooms, parentChildMap, viaMap, childParentMap] = useSpaceSummary(cli, space, refreshToken); const roomsMap = useMemo(() => { if (!rooms) return null; @@ -397,6 +413,10 @@ export const SpaceHierarchy: React.FC = ({ const [removing, setRemoving] = useState(false); const [saving, setSaving] = useState(false); + if (summaryError) { + return

        {_t("Your server does not support showing space hierarchies.")}

        ; + } + let content; if (roomsMap) { const numRooms = Array.from(roomsMap.values()).filter(r => r.room_type !== RoomType.Space).length; @@ -411,78 +431,83 @@ export const SpaceHierarchy: React.FC = ({ countsStr = _t("%(count)s rooms", { count: numRooms, numSpaces }); } - let editSection; + let manageButtons; if (space.getMyMembership() === "join" && space.currentState.maySendStateEvent(EventType.SpaceChild, userId)) { const selectedRelations = Array.from(selected.keys()).flatMap(parentId => { return [...selected.get(parentId).values()].map(childId => [parentId, childId]) as [string, string][]; }); - let buttons; - if (selectedRelations.length) { - const selectionAllSuggested = selectedRelations.every(([parentId, childId]) => { - return parentChildMap.get(parentId)?.get(childId)?.content.suggested; - }); + const selectionAllSuggested = selectedRelations.every(([parentId, childId]) => { + return parentChildMap.get(parentId)?.get(childId)?.content.suggested; + }); - const disabled = removing || saving; + const disabled = !selectedRelations.length || removing || saving; - buttons = <> - { - setRemoving(true); - try { - for (const [parentId, childId] of selectedRelations) { - await cli.sendStateEvent(parentId, EventType.SpaceChild, {}, childId); - parentChildMap.get(parentId).get(childId).content = {}; - parentChildMap.set(parentId, new Map(parentChildMap.get(parentId))); - } - } catch (e) { - setError(_t("Failed to remove some rooms. Try again later")); - } - setRemoving(false); - }} - kind="danger_outline" - disabled={disabled} - > - { removing ? _t("Removing...") : _t("Remove") } - - { - setSaving(true); - try { - for (const [parentId, childId] of selectedRelations) { - const suggested = !selectionAllSuggested; - const existingContent = parentChildMap.get(parentId)?.get(childId)?.content; - if (!existingContent || existingContent.suggested === suggested) continue; - - const content = { - ...existingContent, - suggested: !selectionAllSuggested, - }; - - await cli.sendStateEvent(parentId, EventType.SpaceChild, content, childId); - - parentChildMap.get(parentId).get(childId).content = content; - parentChildMap.set(parentId, new Map(parentChildMap.get(parentId))); - } - } catch (e) { - setError("Failed to update some suggestions. Try again later"); - } - setSaving(false); - }} - kind="primary_outline" - disabled={disabled} - > - { saving - ? _t("Saving...") - : (selectionAllSuggested ? _t("Mark as not suggested") : _t("Mark as suggested")) - } - - ; + let Button: React.ComponentType> = AccessibleButton; + let props = {}; + if (!selectedRelations.length) { + Button = AccessibleTooltipButton; + props = { + tooltip: _t("Select a room below first"), + yOffset: -40, + }; } - editSection = - { buttons } - ; + manageButtons = <> + + + ; } let results; @@ -528,7 +553,10 @@ export const SpaceHierarchy: React.FC = ({ content = <>
        { countsStr } - { editSection } + + { additionalButtons } + { manageButtons } +
        { error &&
        { error } @@ -538,10 +566,8 @@ export const SpaceHierarchy: React.FC = ({ { children } ; - } else if (!rooms) { - content = ; } else { - content =

        {_t("Your server does not support showing space hierarchies.")}

        ; + content = ; } // TODO loading state/error state diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx index 31358a3731..5db54815b7 100644 --- a/src/components/structures/SpaceRoomView.tsx +++ b/src/components/structures/SpaceRoomView.tsx @@ -51,6 +51,15 @@ import MemberAvatar from "../views/avatars/MemberAvatar"; import {useStateToggle} from "../../hooks/useStateToggle"; import SpaceStore from "../../stores/SpaceStore"; import FacePile from "../views/elements/FacePile"; +import {AddExistingToSpace} from "../views/dialogs/AddExistingToSpaceDialog"; +import {sleep} from "../../utils/promise"; +import {calculateRoomVia} from "../../utils/permalinks/Permalinks"; +import {ChevronFace, ContextMenuButton, useContextMenu} from "./ContextMenu"; +import IconizedContextMenu, { + IconizedContextMenuOption, + IconizedContextMenuOptionList, +} from "../views/context_menus/IconizedContextMenu"; +import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton"; interface IProps { space: Room; @@ -214,6 +223,67 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
        ; }; +const SpaceLandingAddButton = ({ space, onNewRoomAdded }) => { + const cli = useContext(MatrixClientContext); + const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu(); + + let contextMenu; + if (menuDisplayed) { + const rect = handle.current.getBoundingClientRect(); + contextMenu = + + { + e.preventDefault(); + e.stopPropagation(); + closeMenu(); + + if (await showCreateNewRoom(cli, space)) { + onNewRoomAdded(); + } + }} + /> + { + e.preventDefault(); + e.stopPropagation(); + closeMenu(); + + const [added] = await showAddExistingRooms(cli, space); + if (added) { + onNewRoomAdded(); + } + }} + /> + + ; + } + + return <> + + { _t("Add") } + + { contextMenu } + ; +}; + const SpaceLanding = ({ space }) => { const cli = useContext(MatrixClientContext); const myMembership = useMyRoomMembership(space); @@ -238,32 +308,20 @@ const SpaceLanding = ({ space }) => { const [refreshToken, forceUpdate] = useStateToggle(false); - let addRoomButtons; + let addRoomButton; if (canAddRooms) { - addRoomButtons = - { - const [added] = await showAddExistingRooms(cli, space); - if (added) { - forceUpdate(); - } - }}> - { _t("Add existing rooms & spaces") } - - { - showCreateNewRoom(cli, space); - }}> - { _t("Create a new room") } - - ; + addRoomButton = ; } let settingsButton; if (shouldShowSpaceSettings(cli, space)) { - settingsButton = { - showSpaceSettings(cli, space); - }}> - { _t("Settings") } - ; + settingsButton = { + showSpaceSettings(cli, space); + }} + title={_t("Settings")} + />; } const onMembersClick = () => { @@ -290,17 +348,19 @@ const SpaceLanding = ({ space }) => { { inviteButton } + { settingsButton }

      -
      - { addRoomButtons } - { settingsButton } -
      - +
      ; }; @@ -354,7 +414,7 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => { let buttonLabel = _t("Skip for now"); if (roomNames.some(name => name.trim())) { onClick = onNextClick; - buttonLabel = busy ? _t("Creating rooms...") : _t("Continue") + buttonLabel = busy ? _t("Creating rooms...") : _t("Continue"); } return
      @@ -376,6 +436,74 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => {
      ; }; +const SpaceAddExistingRooms = ({ space, onFinished }) => { + const [selectedToAdd, setSelectedToAdd] = useState(new Set()); + + const [busy, setBusy] = useState(false); + const [error, setError] = useState(""); + + let onClick = onFinished; + let buttonLabel = _t("Skip for now"); + if (selectedToAdd.size > 0) { + onClick = async () => { + setBusy(true); + + for (const room of selectedToAdd) { + const via = calculateRoomVia(room); + try { + await SpaceStore.instance.addRoomToSpace(space, room.roomId, via).catch(async e => { + if (e.errcode === "M_LIMIT_EXCEEDED") { + await sleep(e.data.retry_after_ms); + return SpaceStore.instance.addRoomToSpace(space, room.roomId, via); // retry + } + + throw e; + }); + } catch (e) { + console.error("Failed to add rooms to space", e); + setError(_t("Failed to add rooms to space")); + break; + } + } + setBusy(false); + }; + buttonLabel = busy ? _t("Adding...") : _t("Add"); + } + + return
      +

      { _t("What do you want to organise?") }

      +
      + { _t("Pick rooms or conversations to add. This is just a space for you, " + + "no one will be informed. You can add more later.") } +
      + + { error &&
      { error }
      } + + { + if (checked) { + selectedToAdd.add(room); + } else { + selectedToAdd.delete(room); + } + setSelectedToAdd(new Set(selectedToAdd)); + }} + /> + +
      + + { buttonLabel } + +
      +
      ; +}; + const SpaceSetupPublicShare = ({ space, onFinished }) => { return

      { _t("Share %(name)s", { name: space.name }) }

      @@ -659,7 +787,7 @@ export default class SpaceRoomView extends React.PureComponent { return { - this.setState({ phase: invite ? Phase.PrivateInvite : Phase.PrivateCreateRooms }); + this.setState({ phase: invite ? Phase.PrivateInvite : Phase.PrivateExistingRooms }); }} />; case Phase.PrivateInvite: @@ -675,6 +803,11 @@ export default class SpaceRoomView extends React.PureComponent { "You can add more later too, including already existing ones.")} onFinished={() => this.setState({ phase: Phase.Landing })} />; + case Phase.PrivateExistingRooms: + return this.setState({ phase: Phase.Landing })} + />; } } diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 12f5d6e890..8cc344f66b 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -68,6 +68,7 @@ class TimelinePanel extends React.Component { showReadReceipts: PropTypes.bool, // Enable managing RRs and RMs. These require the timelineSet to have a room. manageReadReceipts: PropTypes.bool, + sendReadReceiptOnLoad: PropTypes.bool, manageReadMarkers: PropTypes.bool, // true to give the component a 'display: none' style. @@ -126,6 +127,7 @@ class TimelinePanel extends React.Component { // event tile heights. (See _unpaginateEvents) timelineCap: Number.MAX_VALUE, className: 'mx_RoomView_messagePanel', + sendReadReceiptOnLoad: true, }; constructor(props) { @@ -785,8 +787,10 @@ class TimelinePanel extends React.Component { return; } const lastDisplayedEvent = this.state.events[lastDisplayedIndex]; - this._setReadMarker(lastDisplayedEvent.getId(), - lastDisplayedEvent.getTs()); + this._setReadMarker( + lastDisplayedEvent.getId(), + lastDisplayedEvent.getTs(), + ); // the read-marker should become invisible, so that if the user scrolls // down, they don't see it. @@ -872,7 +876,7 @@ class TimelinePanel extends React.Component { // The messagepanel knows where the RM is, so we must have loaded // the relevant event. this._messagePanel.current.scrollToEvent(this.state.readMarkerEventId, - 0, 1/3); + 0, 1/3); return; } @@ -1044,12 +1048,14 @@ class TimelinePanel extends React.Component { } if (eventId) { this._messagePanel.current.scrollToEvent(eventId, pixelOffset, - offsetBase); + offsetBase); } else { this._messagePanel.current.scrollToBottom(); } - this.sendReadReceipt(); + if (this.props.sendReadReceiptOnLoad) { + this.sendReadReceipt(); + } }); }; @@ -1418,8 +1424,8 @@ class TimelinePanel extends React.Component { ['PREPARED', 'CATCHUP'].includes(this.state.clientSyncState) ); const events = this.state.firstVisibleEventIndex - ? this.state.events.slice(this.state.firstVisibleEventIndex) - : this.state.events; + ? this.state.events.slice(this.state.firstVisibleEventIndex) + : this.state.events; return ( void, +} + +interface IState { + loginView: number; + keyBackupNeeded: boolean; + busy: boolean; + password: string; + errorText: string; + flows: LoginFlow[]; +} + +@replaceableComponent("structures.auth.SoftLogout") +export default class SoftLogout extends React.Component { + constructor(props) { + super(props); this.state = { loginView: LOGIN_VIEW.LOADING, keyBackupNeeded: true, // assume we do while we figure it out (see componentDidMount) - busy: false, password: "", errorText: "", + flows: [], }; } @@ -72,7 +83,7 @@ export default class SoftLogout extends React.Component { return; } - this._initLogin(); + this.initLogin(); const cli = MatrixClientPeg.get(); if (cli.isCryptoEnabled()) { @@ -94,7 +105,7 @@ export default class SoftLogout extends React.Component { }); }; - async _initLogin() { + private async initLogin() { const queryParams = this.props.realQueryParams; const hasAllParams = queryParams && queryParams['loginToken']; if (hasAllParams) { @@ -189,7 +200,7 @@ export default class SoftLogout extends React.Component { }); } - _renderSignInSection() { + private renderSignInSection() { if (this.state.loginView === LOGIN_VIEW.LOADING) { const Spinner = sdk.getComponent("elements.Spinner"); return ; @@ -247,7 +258,7 @@ export default class SoftLogout extends React.Component { } // else we already have a message and should use it (key backup warning) const loginType = this.state.loginView === LOGIN_VIEW.CAS ? "cas" : "sso"; - const flow = this.state.flows.find(flow => flow.type === "m.login." + loginType); + const flow = this.state.flows.find(flow => flow.type === "m.login." + loginType) as ISSOFlow; return (
      @@ -289,7 +300,7 @@ export default class SoftLogout extends React.Component {

      {_t("Sign in")}

      - {this._renderSignInSection()} + {this.renderSignInSection()}

      {_t("Clear personal data")}

      diff --git a/src/components/views/auth/InteractiveAuthEntryComponents.js b/src/components/views/auth/InteractiveAuthEntryComponents.js index 6cbecd22ee..e34349c474 100644 --- a/src/components/views/auth/InteractiveAuthEntryComponents.js +++ b/src/components/views/auth/InteractiveAuthEntryComponents.js @@ -169,7 +169,7 @@ export class PasswordAuthEntry extends React.Component { { submitButtonOrSpinner }
      - { errorSection } + { errorSection }
      ); } @@ -375,7 +375,7 @@ export class TermsAuthEntry extends React.Component { if (this.props.showContinue !== false) { // XXX: button classes submitButton = ; + onClick={this._trySubmit} disabled={!allChecked}>{_t("Accept")}; } return ( diff --git a/src/components/views/avatars/MemberAvatar.tsx b/src/components/views/avatars/MemberAvatar.tsx index c79cbc0d32..3205ca372c 100644 --- a/src/components/views/avatars/MemberAvatar.tsx +++ b/src/components/views/avatars/MemberAvatar.tsx @@ -68,8 +68,8 @@ export default class MemberAvatar extends React.Component { let imageUrl = null; if (props.member.getMxcAvatarUrl()) { imageUrl = mediaFromMxc(props.member.getMxcAvatarUrl()).getThumbnailOfSourceHttp( - Math.floor(props.width * window.devicePixelRatio), - Math.floor(props.height * window.devicePixelRatio), + props.width, + props.height, props.resizeMethod, ); } diff --git a/src/components/views/avatars/RoomAvatar.tsx b/src/components/views/avatars/RoomAvatar.tsx index ad0eb45a52..4693d907ba 100644 --- a/src/components/views/avatars/RoomAvatar.tsx +++ b/src/components/views/avatars/RoomAvatar.tsx @@ -93,8 +93,8 @@ export default class RoomAvatar extends React.Component { let oobAvatar = null; if (props.oobData.avatarUrl) { oobAvatar = mediaFromMxc(props.oobData.avatarUrl).getThumbnailOfSourceHttp( - Math.floor(props.width * window.devicePixelRatio), - Math.floor(props.height * window.devicePixelRatio), + props.width, + props.height, props.resizeMethod, ); } @@ -109,12 +109,7 @@ export default class RoomAvatar extends React.Component { private static getRoomAvatarUrl(props: IProps): string { if (!props.room) return null; - return Avatar.avatarUrlForRoom( - props.room, - Math.floor(props.width * window.devicePixelRatio), - Math.floor(props.height * window.devicePixelRatio), - props.resizeMethod, - ); + return Avatar.avatarUrlForRoom(props.room, props.width, props.height, props.resizeMethod); } private onRoomAvatarClick = () => { diff --git a/src/components/views/context_menus/MessageContextMenu.js b/src/components/views/context_menus/MessageContextMenu.js index f86cd26f32..35efd12c9c 100644 --- a/src/components/views/context_menus/MessageContextMenu.js +++ b/src/components/views/context_menus/MessageContextMenu.js @@ -1,8 +1,6 @@ /* -Copyright 2015, 2016 OpenMarket Ltd -Copyright 2018 New Vector Ltd Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2015, 2016, 2018, 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. @@ -34,7 +32,7 @@ import {MenuItem} from "../../structures/ContextMenu"; import {EventType} from "matrix-js-sdk/src/@types/event"; import {replaceableComponent} from "../../../utils/replaceableComponent"; -function canCancel(eventStatus) { +export function canCancel(eventStatus) { return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT; } @@ -98,21 +96,6 @@ export default class MessageContextMenu extends React.Component { return content.pinned && Array.isArray(content.pinned) && content.pinned.includes(this.props.mxEvent.getId()); } - onResendClick = () => { - Resend.resend(this.props.mxEvent); - this.closeMenu(); - }; - - onResendEditClick = () => { - Resend.resend(this.props.mxEvent.replacingEvent()); - this.closeMenu(); - }; - - onResendRedactionClick = () => { - Resend.resend(this.props.mxEvent.localRedactionEvent()); - this.closeMenu(); - }; - onResendReactionsClick = () => { for (const reaction of this._getUnsentReactions()) { Resend.resend(reaction); @@ -170,29 +153,6 @@ export default class MessageContextMenu extends React.Component { this.closeMenu(); }; - onCancelSendClick = () => { - const mxEvent = this.props.mxEvent; - const editEvent = mxEvent.replacingEvent(); - const redactEvent = mxEvent.localRedactionEvent(); - const pendingReactions = this._getPendingReactions(); - - if (editEvent && canCancel(editEvent.status)) { - Resend.removeFromQueue(editEvent); - } - if (redactEvent && canCancel(redactEvent.status)) { - Resend.removeFromQueue(redactEvent); - } - if (pendingReactions.length) { - for (const reaction of pendingReactions) { - Resend.removeFromQueue(reaction); - } - } - if (canCancel(mxEvent.status)) { - Resend.removeFromQueue(this.props.mxEvent); - } - this.closeMenu(); - }; - onForwardClick = () => { if (this.props.onCloseDialog) this.props.onCloseDialog(); dis.dispatch({ @@ -285,20 +245,9 @@ export default class MessageContextMenu extends React.Component { const me = cli.getUserId(); const mxEvent = this.props.mxEvent; const eventStatus = mxEvent.status; - const editStatus = mxEvent.replacingEvent() && mxEvent.replacingEvent().status; - const redactStatus = mxEvent.localRedactionEvent() && mxEvent.localRedactionEvent().status; const unsentReactionsCount = this._getUnsentReactions().length; - const pendingReactionsCount = this._getPendingReactions().length; - const allowCancel = canCancel(mxEvent.status) || - canCancel(editStatus) || - canCancel(redactStatus) || - pendingReactionsCount !== 0; - let resendButton; - let resendEditButton; let resendReactionsButton; - let resendRedactionButton; let redactButton; - let cancelButton; let forwardButton; let pinButton; let unhidePreviewButton; @@ -309,22 +258,6 @@ export default class MessageContextMenu extends React.Component { // status is SENT before remote-echo, null after const isSent = !eventStatus || eventStatus === EventStatus.SENT; if (!mxEvent.isRedacted()) { - if (eventStatus === EventStatus.NOT_SENT) { - resendButton = ( - - { _t('Resend') } - - ); - } - - if (editStatus === EventStatus.NOT_SENT) { - resendEditButton = ( - - { _t('Resend edit') } - - ); - } - if (unsentReactionsCount !== 0) { resendReactionsButton = ( @@ -334,14 +267,6 @@ export default class MessageContextMenu extends React.Component { } } - if (redactStatus === EventStatus.NOT_SENT) { - resendRedactionButton = ( - - { _t('Resend removal') } - - ); - } - if (isSent && this.state.canRedact) { redactButton = ( @@ -350,14 +275,6 @@ export default class MessageContextMenu extends React.Component { ); } - if (allowCancel) { - cancelButton = ( - - { _t('Cancel Sending') } - - ); - } - if (isContentActionable(mxEvent)) { forwardButton = ( @@ -433,7 +350,7 @@ export default class MessageContextMenu extends React.Component { > { _t('Source URL') } - ); + ); } if (this.props.collapseReplyThread) { @@ -455,12 +372,8 @@ export default class MessageContextMenu extends React.Component { return (
      - { resendButton } - { resendEditButton } { resendReactionsButton } - { resendRedactionButton } { redactButton } - { cancelButton } { forwardButton } { pinButton } { viewSourceButton } diff --git a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx index 0f58a624f3..a33248200c 100644 --- a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx +++ b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, {useState} from "react"; +import React, {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"; @@ -29,10 +29,13 @@ import RoomAvatar from "../avatars/RoomAvatar"; import {getDisplayAliasForRoom} from "../../../Rooms"; import AccessibleButton from "../elements/AccessibleButton"; import AutoHideScrollbar from "../../structures/AutoHideScrollbar"; -import {allSettled} from "../../../utils/promise"; +import {sleep} from "../../../utils/promise"; import DMRoomMap from "../../../utils/DMRoomMap"; import {calculateRoomVia} from "../../../utils/permalinks/Permalinks"; import StyledCheckbox from "../elements/StyledCheckbox"; +import MatrixClientContext from "../../../contexts/MatrixClientContext"; +import {sortRooms} from "../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm"; +import ProgressBar from "../elements/ProgressBar"; interface IProps extends IDialogProps { matrixClient: MatrixClient; @@ -41,45 +44,128 @@ interface IProps extends IDialogProps { } const Entry = ({ room, checked, onChange }) => { - return
      + return
      ; + { room.name } + onChange(e.target.checked) : null} + checked={checked} + disabled={!onChange} + /> + ; }; -const AddExistingToSpaceDialog: React.FC = ({ matrixClient: cli, space, onCreateRoomClick, onFinished }) => { +interface IAddExistingToSpaceProps { + space: Room; + selected: Set; + onChange(checked: boolean, room: Room): void; +} + +export const AddExistingToSpace: React.FC = ({ space, selected, onChange }) => { + const cli = useContext(MatrixClientContext); + const visibleRooms = useMemo(() => sortRooms(cli.getVisibleRooms()), [cli]); + const [query, setQuery] = useState(""); const lcQuery = query.toLowerCase(); - const [selectedSpace, setSelectedSpace] = useState(space); - const [selectedToAdd, setSelectedToAdd] = useState(new Set()); - const existingSubspaces = SpaceStore.instance.getChildSpaces(space.roomId); const existingSubspacesSet = new Set(existingSubspaces); const existingRoomsSet = new Set(SpaceStore.instance.getChildRooms(space.roomId)); - const joinRule = selectedSpace.getJoinRule(); - const [spaces, rooms, dms] = cli.getVisibleRooms().reduce((arr, room) => { + const joinRule = space.getJoinRule(); + const [spaces, rooms, dms] = visibleRooms.reduce((arr, room) => { if (room.getMyMembership() !== "join") return arr; if (!room.name.toLowerCase().includes(lcQuery)) return arr; if (room.isSpaceRoom()) { - if (room !== space && room !== selectedSpace && !existingSubspacesSet.has(room)) { + if (room !== space && !existingSubspacesSet.has(room)) { arr[0].push(room); } - } else if (!existingRoomsSet.has(room) && joinRule !== "public") { - // Only show DMs for non-public spaces as they make very little sense in spaces other than "Just Me" ones. - arr[DMRoomMap.shared().getUserIdForRoomId(room.roomId) ? 2 : 1].push(room); + } else if (!existingRoomsSet.has(room)) { + if (!DMRoomMap.shared().getUserIdForRoomId(room.roomId)) { + arr[1].push(room); + } else if (joinRule !== "public") { + // Only show DMs for non-public spaces as they make very little sense in spaces other than "Just Me" ones. + arr[2].push(room); + } } return arr; }, [[], [], []]); - const [busy, setBusy] = useState(false); - const [error, setError] = useState(""); + return
      + + + { rooms.length > 0 ? ( +
      +

      { _t("Rooms") }

      + { rooms.map(room => { + return { + onChange(checked, room); + } : null} + />; + }) } +
      + ) : undefined } + + { spaces.length > 0 ? ( +
      +

      { _t("Spaces") }

      + { spaces.map(space => { + return { + onChange(checked, space); + } : null} + />; + }) } +
      + ) : null } + + { dms.length > 0 ? ( +
      +

      { _t("Direct Messages") }

      + { dms.map(room => { + return { + onChange(checked, room); + } : null} + />; + }) } +
      + ) : null } + + { spaces.length + rooms.length + dms.length < 1 ? + { _t("No results") } + : undefined } +
      +
      ; +}; + +const AddExistingToSpaceDialog: React.FC = ({ matrixClient: cli, space, onCreateRoomClick, onFinished }) => { + const [selectedSpace, setSelectedSpace] = useState(space); + const existingSubspaces = SpaceStore.instance.getChildSpaces(space.roomId); + const [selectedToAdd, setSelectedToAdd] = useState(new Set()); + + const [progress, setProgress] = useState(null); + const [error, setError] = useState(null); let spaceOptionSection; - if (existingSubspacesSet.size > 0) { + if (existingSubspaces.length > 0) { const options = [space, ...existingSubspaces].map((space) => { const classes = classNames("mx_AddExistingToSpaceDialog_dropdownOption", { mx_AddExistingToSpaceDialog_dropdownOptionActive: space === selectedSpace, @@ -116,116 +202,106 @@ const AddExistingToSpaceDialog: React.FC = ({ matrixClient: cli, space,
      ; - return - { error &&
      { error }
      } + const addRooms = async () => { + setError(null); + setProgress(0); - - - { rooms.length > 0 ? ( -
      -

      { _t("Rooms") }

      - { rooms.map(room => { - return { - if (checked) { - selectedToAdd.add(room); - } else { - selectedToAdd.delete(room); - } - setSelectedToAdd(new Set(selectedToAdd)); - }} - />; - }) } -
      - ) : undefined } + let error; - { spaces.length > 0 ? ( -
      -

      { _t("Spaces") }

      - { spaces.map(space => { - return { - if (checked) { - selectedToAdd.add(space); - } else { - selectedToAdd.delete(space); - } - setSelectedToAdd(new Set(selectedToAdd)); - }} - />; - }) } -
      - ) : null } + for (const room of selectedToAdd) { + const via = calculateRoomVia(room); + try { + await SpaceStore.instance.addRoomToSpace(space, room.roomId, via).catch(async e => { + if (e.errcode === "M_LIMIT_EXCEEDED") { + await sleep(e.data.retry_after_ms); + return SpaceStore.instance.addRoomToSpace(space, room.roomId, via); // retry + } - { dms.length > 0 ? ( -
      -

      { _t("Direct Messages") }

      - { dms.map(space => { - return { - if (checked) { - selectedToAdd.add(space); - } else { - selectedToAdd.delete(space); - } - setSelectedToAdd(new Set(selectedToAdd)); - }} - />; - }) } -
      - ) : null } + throw e; + }); + setProgress(i => i + 1); + } catch (e) { + console.error("Failed to add rooms to space", e); + setError(error = e); + break; + } + } - { spaces.length + rooms.length + dms.length < 1 ? - { _t("No results") } - : undefined } -
      + if (!error) { + onFinished(true); + } + }; -
      + const busy = progress !== null; + + let footer; + if (error) { + footer = <> + + + +
      { _t("Not all selected were added") }
      +
      { _t("Try again") }
      +
      + + + { _t("Retry") } + + ; + } else if (busy) { + footer = + +
      + { _t("Adding rooms... (%(progress)s out of %(count)s)", { + count: selectedToAdd.size, + progress, + }) } +
      +
      ; + } else { + footer = <> -
      { _t("Don't want to add an existing room?") }
      +
      { _t("Want to add a new room instead?") }
      onCreateRoomClick(cli, space)} kind="link"> { _t("Create a new room") }
      - { - setBusy(true); - try { - await allSettled(Array.from(selectedToAdd).map((room) => - SpaceStore.instance.addRoomToSpace(space, room.roomId, calculateRoomVia(room)))); - onFinished(true); - } catch (e) { - console.error("Failed to add rooms to space", e); - setError(_t("Failed to add rooms to space")); - } - setBusy(false); - }} - > - { busy ? _t("Adding...") : _t("Add") } + + { _t("Add") } + ; + } + + return + + { + if (checked) { + selectedToAdd.add(room); + } else { + selectedToAdd.delete(room); + } + setSelectedToAdd(new Set(selectedToAdd)); + } : null} + /> + + +
      + { footer }
      ; }; diff --git a/src/components/views/dialogs/BugReportDialog.js b/src/components/views/dialogs/BugReportDialog.js index 8948c14c7c..cbe0130649 100644 --- a/src/components/views/dialogs/BugReportDialog.js +++ b/src/components/views/dialogs/BugReportDialog.js @@ -184,7 +184,7 @@ export default class BugReportDialog extends React.Component { return (
      diff --git a/src/components/views/dialogs/ChangelogDialog.js b/src/components/views/dialogs/ChangelogDialog.js index 50bc13cff5..efbeba3977 100644 --- a/src/components/views/dialogs/ChangelogDialog.js +++ b/src/components/views/dialogs/ChangelogDialog.js @@ -95,7 +95,7 @@ export default class ChangelogDialog extends React.Component { description={content} button={_t("Update")} onFinished={this.props.onFinished} - /> + /> ); } } diff --git a/src/components/views/dialogs/ConfirmWipeDeviceDialog.js b/src/components/views/dialogs/ConfirmWipeDeviceDialog.js index 4faaad0f7e..333e1522f1 100644 --- a/src/components/views/dialogs/ConfirmWipeDeviceDialog.js +++ b/src/components/views/dialogs/ConfirmWipeDeviceDialog.js @@ -39,9 +39,12 @@ export default class ConfirmWipeDeviceDialog extends React.Component { const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); return ( - +

      {_t( diff --git a/src/components/views/dialogs/DevtoolsDialog.js b/src/components/views/dialogs/DevtoolsDialog.js index 9f5513e0a3..8a035263cc 100644 --- a/src/components/views/dialogs/DevtoolsDialog.js +++ b/src/components/views/dialogs/DevtoolsDialog.js @@ -70,8 +70,16 @@ class GenericEditor extends React.PureComponent { } textInput(id, label) { - return ; + return ; } } @@ -155,7 +163,7 @@ export class SendCustomEvent extends GenericEditor {
      + autoComplete="off" value={this.state.evContent} onChange={this._onChange} element="textarea" />

      @@ -239,7 +247,7 @@ class SendAccountData extends GenericEditor {
      + autoComplete="off" value={this.state.evContent} onChange={this._onChange} element="textarea" />
      @@ -315,15 +323,15 @@ class FilteredList extends React.PureComponent { const TruncatedList = sdk.getComponent("elements.TruncatedList"); return
      + type="text" autoComplete="off" value={this.props.query} onChange={this.onQuery} + className="mx_TextInputDialog_input mx_DevTools_RoomStateExplorer_query" + // force re-render so that autoFocus is applied when this component is re-used + key={this.props.children[0] ? this.props.children[0].key : ''} /> + getChildCount={this.getChildCount} + truncateAt={this.state.truncateAt} + createOverflowElement={this.createOverflowElement} />
      ; } } @@ -647,7 +655,7 @@ function VerificationRequest({txnId, request}) { /* Note that request.timeout is a getter, so its value changes */ const id = setInterval(() => { - setRequestTimeout(request.timeout); + setRequestTimeout(request.timeout); }, 500); return () => { clearInterval(id); }; @@ -941,35 +949,35 @@ class SettingsExplorer extends React.Component { /> - - - - - + + + + + - {allSettings.map(i => ( - - + - - - - ))} + + + + + + ))}
      {_t("Setting ID")}{_t("Value")}{_t("Value in this room")}
      {_t("Setting ID")}{_t("Value")}{_t("Value in this room")}
      - this.onViewClick(e, i)}> - {i} - - this.onEditClick(e, i)} - className='mx_DevTools_SettingsExplorer_edit' - > + {allSettings.map(i => ( +
      + this.onViewClick(e, i)}> + {i} + + this.onEditClick(e, i)} + className='mx_DevTools_SettingsExplorer_edit' + > ✏ - - - {this.renderSettingValue(SettingsStore.getValue(i))} - - - {this.renderSettingValue(SettingsStore.getValue(i, room.roomId))} - -
      + {this.renderSettingValue(SettingsStore.getValue(i))} + + + {this.renderSettingValue(SettingsStore.getValue(i, room.roomId))} + +
      @@ -998,11 +1006,11 @@ class SettingsExplorer extends React.Component {
      - - - - - + + + + + {LEVEL_ORDER.map(lvl => ( diff --git a/src/components/views/dialogs/IncomingSasDialog.js b/src/components/views/dialogs/IncomingSasDialog.js index f18b7a9d0c..5df02d7a6f 100644 --- a/src/components/views/dialogs/IncomingSasDialog.js +++ b/src/components/views/dialogs/IncomingSasDialog.js @@ -130,7 +130,7 @@ export default class IncomingSasDialog extends React.Component { const oppProfile = this.state.opponentProfile; if (oppProfile) { const url = oppProfile.avatar_url - ? mediaFromMxc(oppProfile.avatar_url).getSquareThumbnailHttp(Math.floor(48 * window.devicePixelRatio)) + ? mediaFromMxc(oppProfile.avatar_url).getSquareThumbnailHttp(48) : null; profile =
      +

      {_t("Enable 'Manage Integrations' in Settings to do this.")}

      diff --git a/src/components/views/dialogs/IntegrationsImpossibleDialog.js b/src/components/views/dialogs/IntegrationsImpossibleDialog.js index 9bc9d02ba6..e14d40aaef 100644 --- a/src/components/views/dialogs/IntegrationsImpossibleDialog.js +++ b/src/components/views/dialogs/IntegrationsImpossibleDialog.js @@ -37,9 +37,12 @@ export default class IntegrationsImpossibleDialog extends React.Component { const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); return ( - +

      {_t( diff --git a/src/components/views/dialogs/InviteDialog.tsx b/src/components/views/dialogs/InviteDialog.tsx index 2ebc84ec7c..ec9c71ccbe 100644 --- a/src/components/views/dialogs/InviteDialog.tsx +++ b/src/components/views/dialogs/InviteDialog.tsx @@ -1312,7 +1312,7 @@ export default class InviteDialog extends React.PureComponent {success ? - {_t("Upload completed")} : - cancelled ? - {_t("Cancelled signature upload")} : - {_t("Unable to upload")}} + {_t("Upload completed")} : + cancelled ? + {_t("Cancelled signature upload")} : + {_t("Unable to upload")}} + {content} ); diff --git a/src/components/views/dialogs/RoomSettingsDialog.js b/src/components/views/dialogs/RoomSettingsDialog.js index 9c2f23ef22..b6c4d42243 100644 --- a/src/components/views/dialogs/RoomSettingsDialog.js +++ b/src/components/views/dialogs/RoomSettingsDialog.js @@ -116,8 +116,12 @@ export default class RoomSettingsDialog extends React.Component { const roomName = MatrixClientPeg.get().getRoom(this.props.roomId).name; return ( - +

      diff --git a/src/components/views/dialogs/ServerPickerDialog.tsx b/src/components/views/dialogs/ServerPickerDialog.tsx index 4abc0a88b1..62a2b95c68 100644 --- a/src/components/views/dialogs/ServerPickerDialog.tsx +++ b/src/components/views/dialogs/ServerPickerDialog.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. @@ -110,7 +110,7 @@ export default class ServerPickerDialog extends React.PureComponent diff --git a/src/components/views/dialogs/SessionRestoreErrorDialog.js b/src/components/views/dialogs/SessionRestoreErrorDialog.js index 50d7fbea09..43e73a2f83 100644 --- a/src/components/views/dialogs/SessionRestoreErrorDialog.js +++ b/src/components/views/dialogs/SessionRestoreErrorDialog.js @@ -98,7 +98,7 @@ export default class SessionRestoreErrorDialog extends React.Component { "may be incompatible with this version. Close this window and return " + "to the more recent version.", { brand }, - ) }

      + ) }

      { _t( "Clearing your browser's storage may fix the problem, but will sign you " + diff --git a/src/components/views/dialogs/StorageEvictedDialog.js b/src/components/views/dialogs/StorageEvictedDialog.js index 15c5347644..1e17ab1738 100644 --- a/src/components/views/dialogs/StorageEvictedDialog.js +++ b/src/components/views/dialogs/StorageEvictedDialog.js @@ -45,10 +45,12 @@ export default class StorageEvictedDialog extends React.Component { let logRequest; if (SdkConfig.get().bug_report_endpoint_url) { logRequest = _t( - "To help us prevent this in future, please send us logs.", {}, - { - a: text => {text}, - }); + "To help us prevent this in future, please send us logs.", + {}, + { + a: text => {text}, + }, + ); } return ( diff --git a/src/components/views/dialogs/UserSettingsDialog.js b/src/components/views/dialogs/UserSettingsDialog.js index eb9eaeb5dd..e7f6953589 100644 --- a/src/components/views/dialogs/UserSettingsDialog.js +++ b/src/components/views/dialogs/UserSettingsDialog.js @@ -155,8 +155,12 @@ export default class UserSettingsDialog extends React.Component { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); return ( - +

      diff --git a/src/components/views/dialogs/VerificationRequestDialog.js b/src/components/views/dialogs/VerificationRequestDialog.js index 205597a1c4..9281275e6a 100644 --- a/src/components/views/dialogs/VerificationRequestDialog.js +++ b/src/components/views/dialogs/VerificationRequestDialog.js @@ -52,11 +52,13 @@ export default class VerificationRequestDialog extends React.Component { const title = request && request.isSelfVerification ? _t("Verify other login") : _t("Verification Request"); - return + return +

      {_t("The widget will verify your user ID, but won't be able to perform actions for you:")} diff --git a/src/components/views/dialogs/security/ConfirmDestroyCrossSigningDialog.js b/src/components/views/dialogs/security/ConfirmDestroyCrossSigningDialog.js index 43fb25f152..e71983b074 100644 --- a/src/components/views/dialogs/security/ConfirmDestroyCrossSigningDialog.js +++ b/src/components/views/dialogs/security/ConfirmDestroyCrossSigningDialog.js @@ -40,10 +40,11 @@ export default class ConfirmDestroyCrossSigningDialog extends React.Component { return ( + className='mx_ConfirmDestroyCrossSigningDialog' + hasCancel={true} + onFinished={this.props.onFinished} + title={_t("Destroy cross-signing keys?")} + >

      {_t( diff --git a/src/components/views/dialogs/security/RestoreKeyBackupDialog.js b/src/components/views/dialogs/security/RestoreKeyBackupDialog.js index 1fafe03d95..4ac15ab5a3 100644 --- a/src/components/views/dialogs/security/RestoreKeyBackupDialog.js +++ b/src/components/views/dialogs/security/RestoreKeyBackupDialog.js @@ -373,21 +373,24 @@ export default class RestoreKeyBackupDialog extends React.PureComponent { {_t( "If you've forgotten your Security Phrase you can "+ "use your Security Key or " + - "set up new recovery options" - , {}, { - button1: s => - {s} - , - button2: s => - {s} - , - })} + "set up new recovery options", + {}, + { + button1: s => + {s} + , + button2: s => + {s} + , + })}

      ; } else { title = _t("Enter Security Key"); @@ -435,15 +438,17 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
      {_t( "If you've forgotten your Security Key you can "+ - "" - , {}, { - button: s => - {s} - , - })} + "", + {}, + { + button: s => + {s} + , + }, + )}
      ; } @@ -452,9 +457,9 @@ export default class RestoreKeyBackupDialog extends React.PureComponent { onFinished={this.props.onFinished} title={title} > -
      - {content} -
      +
      + {content} +
      ); } diff --git a/src/components/views/elements/ActionButton.js b/src/components/views/elements/ActionButton.js index 1714891cb5..9903c631b2 100644 --- a/src/components/views/elements/ActionButton.js +++ b/src/components/views/elements/ActionButton.js @@ -70,8 +70,8 @@ export default class ActionButton extends React.Component { } const icon = this.props.iconPath ? - () : - undefined; + () : + undefined; const classNames = ["mx_RoleButton"]; if (this.props.className) { diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index e206fda797..b898ad2ebc 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -109,7 +109,7 @@ export default class AppTile extends React.Component { const childContentProtocol = u.protocol; if (parentContentProtocol === 'https:' && childContentProtocol !== 'https:') { console.warn("Refusing to load mixed-content app:", - parentContentProtocol, childContentProtocol, window.location, this.props.app.url); + parentContentProtocol, childContentProtocol, window.location, this.props.app.url); return true; } return false; diff --git a/src/components/views/elements/EditableItemList.js b/src/components/views/elements/EditableItemList.js index ff62f169fa..d8ec5af278 100644 --- a/src/components/views/elements/EditableItemList.js +++ b/src/components/views/elements/EditableItemList.js @@ -65,12 +65,18 @@ export class EditableItem extends React.Component { {_t("Are you sure?")} - + {_t("Yes")} - + {_t("No")}
      @@ -121,11 +127,15 @@ export default class EditableItemList extends React.Component { _renderNewItemField() { return ( - + + autoComplete="off" value={this.props.newItem || ""} onChange={this._onNewItemChanged} + list={this.props.suggestionsListId} /> {_t("Add")} diff --git a/src/components/views/elements/EditableText.js b/src/components/views/elements/EditableText.js index 638fd02553..7c38ac1777 100644 --- a/src/components/views/elements/EditableText.js +++ b/src/components/views/elements/EditableText.js @@ -221,13 +221,15 @@ export default class EditableText extends React.Component { ; } else { // show the content editable div, but manually manage its contents as react and contentEditable don't play nice together - editableEl =
      ; + editableEl =
      ; } return editableEl; diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index cbced07bfe..05d487a9eb 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -34,16 +34,15 @@ import {RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks" import {MatrixEvent} from "matrix-js-sdk/src/models/event"; import {normalizeWheelEvent} from "../../../utils/Mouse"; -const MIN_ZOOM = 100; -const MAX_ZOOM = 300; +// Max scale to keep gaps around the image +const MAX_SCALE = 0.95; // This is used for the buttons -const ZOOM_STEP = 10; +const ZOOM_STEP = 0.10; // This is used for mouse wheel events -const ZOOM_COEFFICIENT = 0.5; +const ZOOM_COEFFICIENT = 0.0025; // If we have moved only this much we can zoom const ZOOM_DISTANCE = 10; - interface IProps { src: string, // the source of the image being displayed name?: string, // the main title ('name') for the image @@ -62,8 +61,10 @@ interface IProps { } interface IState { - rotation: number, zoom: number, + minZoom: number, + maxZoom: number, + rotation: number, translationX: number, translationY: number, moving: boolean, @@ -75,8 +76,10 @@ export default class ImageView extends React.Component { constructor(props) { super(props); this.state = { + zoom: 0, + minZoom: MAX_SCALE, + maxZoom: MAX_SCALE, rotation: 0, - zoom: MIN_ZOOM, translationX: 0, translationY: 0, moving: false, @@ -87,6 +90,8 @@ export default class ImageView extends React.Component { // XXX: Refs to functional components private contextMenuButton = createRef(); private focusLock = createRef(); + private imageWrapper = createRef(); + private image = createRef(); private initX = 0; private initY = 0; @@ -99,12 +104,89 @@ export default class ImageView extends React.Component { // We have to use addEventListener() because the listener // needs to be passive in order to work with Chromium this.focusLock.current.addEventListener('wheel', this.onWheel, { passive: false }); + // We want to recalculate zoom whenever the window's size changes + window.addEventListener("resize", this.calculateZoom); + // After the image loads for the first time we want to calculate the zoom + this.image.current.addEventListener("load", this.calculateZoom); + // Try to precalculate the zoom from width and height props + this.calculateZoom(); } componentWillUnmount() { this.focusLock.current.removeEventListener('wheel', this.onWheel); + window.removeEventListener("resize", this.calculateZoom); + this.image.current.removeEventListener("load", this.calculateZoom); } + private calculateZoom = () => { + const image = this.image.current; + const imageWrapper = this.imageWrapper.current; + + const width = this.props.width || image.naturalWidth; + const height = this.props.height || image.naturalHeight; + + const zoomX = imageWrapper.clientWidth / width; + const zoomY = imageWrapper.clientHeight / height; + + // If the image is smaller in both dimensions set its the zoom to 1 to + // display it in its original size + if (zoomX >= 1 && zoomY >= 1) { + this.setState({ + zoom: 1, + minZoom: 1, + maxZoom: 1, + }); + return; + } + // We set minZoom to the min of the zoomX and zoomY to avoid overflow in + // any direction. We also multiply by MAX_SCALE to get a gap around the + // image by default + const minZoom = Math.min(zoomX, zoomY) * MAX_SCALE; + + if (this.state.zoom <= this.state.minZoom) this.setState({zoom: minZoom}); + this.setState({ + minZoom: minZoom, + maxZoom: 1, + }); + } + + private zoom(delta: number) { + const newZoom = this.state.zoom + delta; + + if (newZoom <= this.state.minZoom) { + this.setState({ + zoom: this.state.minZoom, + translationX: 0, + translationY: 0, + }); + return; + } + if (newZoom >= this.state.maxZoom) { + this.setState({zoom: this.state.maxZoom}); + return; + } + + this.setState({ + zoom: newZoom, + }); + } + + private onWheel = (ev: WheelEvent) => { + ev.stopPropagation(); + ev.preventDefault(); + + const {deltaY} = normalizeWheelEvent(ev); + this.zoom(-(deltaY * ZOOM_COEFFICIENT)); + }; + + private onZoomInClick = () => { + this.zoom(ZOOM_STEP); + }; + + private onZoomOutClick = () => { + this.zoom(-ZOOM_STEP); + }; + private onKeyDown = (ev: KeyboardEvent) => { if (ev.key === Key.ESCAPE) { ev.stopPropagation(); @@ -113,31 +195,6 @@ export default class ImageView extends React.Component { } }; - private onWheel = (ev: WheelEvent) => { - ev.stopPropagation(); - ev.preventDefault(); - - const {deltaY} = normalizeWheelEvent(ev); - const newZoom = this.state.zoom - (deltaY * ZOOM_COEFFICIENT); - - if (newZoom <= MIN_ZOOM) { - this.setState({ - zoom: MIN_ZOOM, - translationX: 0, - translationY: 0, - }); - return; - } - if (newZoom >= MAX_ZOOM) { - this.setState({zoom: MAX_ZOOM}); - return; - } - - this.setState({ - zoom: newZoom, - }); - }; - private onRotateCounterClockwiseClick = () => { const cur = this.state.rotation; const rotationDegrees = cur - 90; @@ -150,31 +207,6 @@ export default class ImageView extends React.Component { this.setState({ rotation: rotationDegrees }); }; - private onZoomInClick = () => { - if (this.state.zoom >= MAX_ZOOM) { - this.setState({zoom: MAX_ZOOM}); - return; - } - - this.setState({ - zoom: this.state.zoom + ZOOM_STEP, - }); - }; - - private onZoomOutClick = () => { - if (this.state.zoom <= MIN_ZOOM) { - this.setState({ - zoom: MIN_ZOOM, - translationX: 0, - translationY: 0, - }); - return; - } - this.setState({ - zoom: this.state.zoom - ZOOM_STEP, - }); - }; - private onDownloadClick = () => { const a = document.createElement("a"); a.href = this.props.src; @@ -217,8 +249,8 @@ export default class ImageView extends React.Component { if (ev.button !== 0) return; // Zoom in if we are completely zoomed out - if (this.state.zoom === MIN_ZOOM) { - this.setState({zoom: MAX_ZOOM}); + if (this.state.zoom === this.state.minZoom) { + this.setState({zoom: this.state.maxZoom}); return; } @@ -251,7 +283,7 @@ export default class ImageView extends React.Component { Math.abs(this.state.translationY - this.previousY) < ZOOM_DISTANCE ) { this.setState({ - zoom: MIN_ZOOM, + zoom: this.state.minZoom, translationX: 0, translationY: 0, }); @@ -286,17 +318,20 @@ export default class ImageView extends React.Component { render() { const showEventMeta = !!this.props.mxEvent; + const zoomingDisabled = this.state.maxZoom === this.state.minZoom; let cursor; if (this.state.moving) { cursor= "grabbing"; - } else if (this.state.zoom === MIN_ZOOM) { + } else if (zoomingDisabled) { + cursor = "default"; + } else if (this.state.zoom === this.state.minZoom) { cursor = "zoom-in"; } else { cursor = "zoom-out"; } const rotationDegrees = this.state.rotation + "deg"; - const zoomPercentage = this.state.zoom/100; + const zoom = this.state.zoom; const translatePixelsX = this.state.translationX + "px"; const translatePixelsY = this.state.translationY + "px"; // The order of the values is important! @@ -308,7 +343,7 @@ export default class ImageView extends React.Component { transition: this.state.moving ? null : "transform 200ms ease 0s", transform: `translateX(${translatePixelsX}) translateY(${translatePixelsY}) - scale(${zoomPercentage}) + scale(${zoom}) rotate(${rotationDegrees})`, }; @@ -380,6 +415,25 @@ export default class ImageView extends React.Component { ); } + let zoomOutButton; + let zoomInButton; + if (!zoomingDisabled) { + zoomOutButton = ( + + + ); + zoomInButton = ( + + + ); + } + return ( { title={_t("Rotate Left")} onClick={ this.onRotateCounterClockwiseClick }> - - - - + {zoomOutButton} + {zoomInButton} { {this.renderContextMenu()}
      -
      +
      {this.props.label}; - let secondPart = ; + let secondPart = ; if (this.props.toggleInFront) { const temp = firstPart; diff --git a/src/components/views/elements/LanguageDropdown.js b/src/components/views/elements/LanguageDropdown.js index 2e961be700..b8734c5afb 100644 --- a/src/components/views/elements/LanguageDropdown.js +++ b/src/components/views/elements/LanguageDropdown.js @@ -60,10 +60,10 @@ export default class LanguageDropdown extends React.Component { // doesn't know this, therefore we do this. const language = SettingsStore.getValue("language", null, /*excludeDefault:*/true); if (language) { - this.props.onOptionChange(language); + this.props.onOptionChange(language); } else { - const language = languageHandler.normalizeLanguageKey(languageHandler.getLanguageFromBrowser()); - this.props.onOptionChange(language); + const language = languageHandler.normalizeLanguageKey(languageHandler.getLanguageFromBrowser()); + this.props.onOptionChange(language); } } } diff --git a/src/components/views/elements/Pill.js b/src/components/views/elements/Pill.js index a8e16813e6..ace41db39d 100644 --- a/src/components/views/elements/Pill.js +++ b/src/components/views/elements/Pill.js @@ -225,19 +225,19 @@ class Pill extends React.Component { } break; case Pill.TYPE_USER_MENTION: { - // If this user is not a member of this room, default to the empty member - const member = this.state.member; - if (member) { - userId = member.userId; - member.rawDisplayName = member.rawDisplayName || ''; - linkText = member.rawDisplayName; - if (this.props.shouldShowPillAvatar) { - avatar =
      -
      {_t("Level")}{_t("Settable at global")}{_t("Settable at room")}
      {_t("Level")}{_t("Settable at global")}{_t("Settable at room")}
      {_t("Homeserver feature support:")} {homeserverSupportsCrossSigning ? _t("exists") : _t("not found")}
      + {errorSection} {actionRow} diff --git a/src/components/views/settings/DevicesPanel.js b/src/components/views/settings/DevicesPanel.js index e7d300b0f8..b1ad605a37 100644 --- a/src/components/views/settings/DevicesPanel.js +++ b/src/components/views/settings/DevicesPanel.js @@ -214,7 +214,7 @@ export default class DevicesPanel extends React.Component { const deleteButton = this.state.deleting ? : - { _t("Delete %(count)s sessions", {count: this.state.selectedDevices.length}) } + { _t("Delete %(count)s sessions", {count: this.state.selectedDevices.length})} ; const classes = classNames(this.props.className, "mx_DevicesPanel"); diff --git a/src/components/views/settings/E2eAdvancedPanel.js b/src/components/views/settings/E2eAdvancedPanel.tsx similarity index 100% rename from src/components/views/settings/E2eAdvancedPanel.js rename to src/components/views/settings/E2eAdvancedPanel.tsx diff --git a/src/components/views/settings/EventIndexPanel.js b/src/components/views/settings/EventIndexPanel.tsx similarity index 65% rename from src/components/views/settings/EventIndexPanel.js rename to src/components/views/settings/EventIndexPanel.tsx index d1a02de16d..fa84063ee8 100644 --- a/src/components/views/settings/EventIndexPanel.js +++ b/src/components/views/settings/EventIndexPanel.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. @@ -28,10 +28,17 @@ import {SettingLevel} from "../../../settings/SettingLevel"; import {replaceableComponent} from "../../../utils/replaceableComponent"; import SeshatResetDialog from '../dialogs/SeshatResetDialog'; +interface IState { + enabling: boolean; + eventIndexSize: number; + roomCount: number; + eventIndexingEnabled: boolean; +} + @replaceableComponent("views.settings.EventIndexPanel") -export default class EventIndexPanel extends React.Component { - constructor() { - super(); +export default class EventIndexPanel extends React.Component<{}, IState> { + constructor(props) { + super(props); this.state = { enabling: false, @@ -68,7 +75,7 @@ export default class EventIndexPanel extends React.Component { } } - async componentDidMount(): void { + componentDidMount(): void { this.updateState(); } @@ -102,8 +109,10 @@ export default class EventIndexPanel extends React.Component { }); } - _onManage = async () => { + private onManage = async () => { Modal.createTrackedDialogAsync('Message search', 'Message search', + // @ts-ignore: TS doesn't seem to like the type of this now that it + // has also been converted to TS as well, but I can't figure out why... import('../../../async-components/views/dialogs/eventindex/ManageEventIndexDialog'), { onFinished: () => {}, @@ -111,7 +120,7 @@ export default class EventIndexPanel extends React.Component { ); } - _onEnable = async () => { + private onEnable = async () => { this.setState({ enabling: true, }); @@ -123,14 +132,13 @@ export default class EventIndexPanel extends React.Component { await this.updateState(); } - _confirmEventStoreReset = () => { - const self = this; + private confirmEventStoreReset = () => { const { close } = Modal.createDialog(SeshatResetDialog, { onFinished: async (success) => { if (success) { await SettingsStore.setValue('enableEventIndexing', null, SettingLevel.DEVICE, false); await EventIndexPeg.deleteEventIndex(); - await self._onEnable(); + await this.onEnable(); close(); } }, @@ -145,20 +153,19 @@ export default class EventIndexPanel extends React.Component { if (EventIndexPeg.get() !== null) { eventIndexingSettings = (
      -
      - {_t("Securely cache encrypted messages locally for them " + - "to appear in search results, using %(size)s to store messages from %(rooms)s rooms.", - { - size: formatBytes(this.state.eventIndexSize, 0), - // This drives the singular / plural string - // selection for "room" / "rooms" only. - count: this.state.roomCount, - rooms: formatCountLong(this.state.roomCount), - }, - )} -
      +
      {_t( + "Securely cache encrypted messages locally for them " + + "to appear in search results, using %(size)s to store messages from %(rooms)s rooms.", + { + size: formatBytes(this.state.eventIndexSize, 0), + // This drives the singular / plural string + // selection for "room" / "rooms" only. + count: this.state.roomCount, + rooms: formatCountLong(this.state.roomCount), + }, + )}
      - + {_t("Manage")}
      @@ -167,13 +174,13 @@ export default class EventIndexPanel extends React.Component { } else if (!this.state.eventIndexingEnabled && EventIndexPeg.supportIsInstalled()) { eventIndexingSettings = (
      -
      - {_t( "Securely cache encrypted messages locally for them to " + - "appear in search results.")} -
      +
      {_t( + "Securely cache encrypted messages locally for them to " + + "appear in search results.", + )}
      + onClick={this.onEnable}> {_t("Enable")} {this.state.enabling ? :
      } @@ -188,40 +195,36 @@ export default class EventIndexPanel extends React.Component { ); eventIndexingSettings = ( -
      +
      {_t( + "%(brand)s is missing some components required for securely " + + "caching encrypted messages locally. If you'd like to " + + "experiment with this feature, build a custom %(brand)s Desktop " + + "with search components added.", { - _t( "%(brand)s is missing some components required for securely " + - "caching encrypted messages locally. If you'd like to " + - "experiment with this feature, build a custom %(brand)s Desktop " + - "with search components added.", - { - brand, - }, - { - 'nativeLink': (sub) => {sub}, - }, - ) - } -
      + brand, + }, + { + nativeLink: sub => {sub}, + }, + )}
      ); } else if (!EventIndexPeg.platformHasSupport()) { eventIndexingSettings = ( -
      +
      {_t( + "%(brand)s can't securely cache encrypted messages locally " + + "while running in a web browser. Use %(brand)s Desktop " + + "for encrypted messages to appear in search results.", { - _t( "%(brand)s can't securely cache encrypted messages locally " + - "while running in a web browser. Use %(brand)s Desktop " + - "for encrypted messages to appear in search results.", - { - brand, - }, - { - 'desktopLink': (sub) => {sub}, - }, - ) - } -
      + brand, + }, + { + desktopLink: sub => {sub}, + }, + )}
      ); } else { eventIndexingSettings = ( @@ -233,19 +236,18 @@ export default class EventIndexPanel extends React.Component { }

      {EventIndexPeg.error && ( -
      - {_t("Advanced")} - - {EventIndexPeg.error.message} - -

      - - {_t("Reset")} - -

      -
      +
      + {_t("Advanced")} + + {EventIndexPeg.error.message} + +

      + + {_t("Reset")} + +

      +
      )} -
      ); } diff --git a/src/components/views/settings/Notifications.js b/src/components/views/settings/Notifications.js index 25fe434994..5756536085 100644 --- a/src/components/views/settings/Notifications.js +++ b/src/components/views/settings/Notifications.js @@ -100,7 +100,7 @@ export default class Notifications extends React.Component { MatrixClientPeg.get().setPushRuleEnabled( 'global', self.state.masterPushRule.kind, self.state.masterPushRule.rule_id, !checked, ).then(function() { - self._refreshFromServer(); + self._refreshFromServer(); }); }; @@ -580,12 +580,12 @@ export default class Notifications extends React.Component { "vectorRuleId": "_keywords", "description": ( - { _t('Messages containing keywords', - {}, - { 'span': (sub) => - {sub}, - }, - )} + { _t('Messages containing keywords', + {}, + { 'span': (sub) => + {sub}, + }, + )} ), "vectorState": self.state.vectorContentRules.vectorState, @@ -743,8 +743,8 @@ export default class Notifications extends React.Component { emailNotificationsRow(address, label) { return ; + onChange={this.onEnableEmailNotificationsChange.bind(this, address)} + label={label} key={`emailNotif_${label}`} />; } render() { @@ -757,8 +757,8 @@ export default class Notifications extends React.Component { let masterPushRuleDiv; if (this.state.masterPushRule) { masterPushRuleDiv = ; + onChange={this.onEnableNotificationsChange} + label={_t('Enable notifications for this account')} />; } let clearNotificationsButton; @@ -874,16 +874,16 @@ export default class Notifications extends React.Component { { spinner } + onChange={this.onEnableDesktopNotificationsChange} + label={_t('Enable desktop notifications for this session')} /> + onChange={this.onEnableDesktopNotificationBodyChange} + label={_t('Show message in desktop notification')} /> + onChange={this.onEnableAudioNotificationsChange} + label={_t('Enable audible notifications for this session')} /> { emailNotificationsRows } diff --git a/src/components/views/settings/ProfileSettings.js b/src/components/views/settings/ProfileSettings.js index 971b868751..9ecf369eba 100644 --- a/src/components/views/settings/ProfileSettings.js +++ b/src/components/views/settings/ProfileSettings.js @@ -170,8 +170,12 @@ export default class ProfileSettings extends React.Component { noValidate={true} className="mx_ProfileSettings_profileForm" > - +
      {_t("Profile")} diff --git a/src/components/views/settings/SetIdServer.js b/src/components/views/settings/SetIdServer.tsx similarity index 89% rename from src/components/views/settings/SetIdServer.js rename to src/components/views/settings/SetIdServer.tsx index fa2a36476d..05d1f83387 100644 --- a/src/components/views/settings/SetIdServer.js +++ b/src/components/views/settings/SetIdServer.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. @@ -16,7 +16,6 @@ limitations under the License. import url from 'url'; import React from 'react'; -import PropTypes from 'prop-types'; import {_t} from "../../../languageHandler"; import * as sdk from '../../../index'; import {MatrixClientPeg} from "../../../MatrixClientPeg"; @@ -28,6 +27,7 @@ import {abbreviateUrl, unabbreviateUrl} from "../../../utils/UrlUtils"; import { getDefaultIdentityServerUrl, doesIdentityServerHaveTerms } from '../../../utils/IdentityServerUtils'; import {timeout} from "../../../utils/promise"; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { ActionPayload } from '../../../dispatcher/payloads'; // We'll wait up to this long when checking for 3PID bindings on the IS. const REACHABILITY_TIMEOUT = 10000; // ms @@ -59,16 +59,28 @@ async function checkIdentityServerUrl(u) { } } -@replaceableComponent("views.settings.SetIdServer") -export default class SetIdServer extends React.Component { - static propTypes = { - // Whether or not the ID server is missing terms. This affects the text - // shown to the user. - missingTerms: PropTypes.bool, - }; +interface IProps { + // Whether or not the ID server is missing terms. This affects the text + // shown to the user. + missingTerms: boolean; +} - constructor() { - super(); +interface IState { + defaultIdServer?: string; + currentClientIdServer: string; + idServer?: string; + error?: string; + busy: boolean; + disconnectBusy: boolean; + checking: boolean; +} + +@replaceableComponent("views.settings.SetIdServer") +export default class SetIdServer extends React.Component { + private dispatcherRef: string; + + constructor(props) { + super(props); let defaultIdServer = ''; if (!MatrixClientPeg.get().getIdentityServerUrl() && getDefaultIdentityServerUrl()) { @@ -96,7 +108,7 @@ export default class SetIdServer extends React.Component { dis.unregister(this.dispatcherRef); } - onAction = (payload) => { + private onAction = (payload: ActionPayload) => { // We react to changes in the ID server in the event the user is staring at this form // when changing their identity server on another device. if (payload.action !== "id_server_changed") return; @@ -106,13 +118,13 @@ export default class SetIdServer extends React.Component { }); }; - _onIdentityServerChanged = (ev) => { + private onIdentityServerChanged = (ev) => { const u = ev.target.value; this.setState({idServer: u}); }; - _getTooltip = () => { + private getTooltip = () => { if (this.state.checking) { const InlineSpinner = sdk.getComponent('views.elements.InlineSpinner'); return
      @@ -126,11 +138,11 @@ export default class SetIdServer extends React.Component { } }; - _idServerChangeEnabled = () => { + private idServerChangeEnabled = () => { return !!this.state.idServer && !this.state.busy; }; - _saveIdServer = (fullUrl) => { + private saveIdServer = (fullUrl) => { // Account data change will update localstorage, client, etc through dispatcher MatrixClientPeg.get().setAccountData("m.identity_server", { base_url: fullUrl, @@ -143,7 +155,7 @@ export default class SetIdServer extends React.Component { }); }; - _checkIdServer = async (e) => { + private checkIdServer = async (e) => { e.preventDefault(); const { idServer, currentClientIdServer } = this.state; @@ -166,14 +178,14 @@ export default class SetIdServer extends React.Component { // Double check that the identity server even has terms of service. const hasTerms = await doesIdentityServerHaveTerms(fullUrl); if (!hasTerms) { - const [confirmed] = await this._showNoTermsWarning(fullUrl); + const [confirmed] = await this.showNoTermsWarning(fullUrl); save = confirmed; } // Show a general warning, possibly with details about any bound // 3PIDs that would be left behind. if (save && currentClientIdServer && fullUrl !== currentClientIdServer) { - const [confirmed] = await this._showServerChangeWarning({ + const [confirmed] = await this.showServerChangeWarning({ title: _t("Change identity server"), unboundMessage: _t( "Disconnect from the identity server and " + @@ -189,7 +201,7 @@ export default class SetIdServer extends React.Component { } if (save) { - this._saveIdServer(fullUrl); + this.saveIdServer(fullUrl); } } catch (e) { console.error(e); @@ -204,7 +216,7 @@ export default class SetIdServer extends React.Component { }); }; - _showNoTermsWarning(fullUrl) { + 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"), @@ -223,10 +235,10 @@ export default class SetIdServer extends React.Component { return finished; } - _onDisconnectClicked = async () => { + private onDisconnectClicked = async () => { this.setState({disconnectBusy: true}); try { - const [confirmed] = await this._showServerChangeWarning({ + const [confirmed] = await this.showServerChangeWarning({ title: _t("Disconnect identity server"), unboundMessage: _t( "Disconnect from the identity server ?", {}, @@ -235,14 +247,14 @@ export default class SetIdServer extends React.Component { button: _t("Disconnect"), }); if (confirmed) { - this._disconnectIdServer(); + this.disconnectIdServer(); } } finally { this.setState({disconnectBusy: false}); } }; - async _showServerChangeWarning({ title, unboundMessage, button }) { + private async showServerChangeWarning({ title, unboundMessage, button }) { const { currentClientIdServer } = this.state; let threepids = []; @@ -318,7 +330,7 @@ export default class SetIdServer extends React.Component { return finished; } - _disconnectIdServer = () => { + private disconnectIdServer = () => { // Account data change will update localstorage, client, etc through dispatcher MatrixClientPeg.get().setAccountData("m.identity_server", { base_url: null, // clear @@ -371,7 +383,7 @@ export default class SetIdServer extends React.Component { let discoSection; if (idServerUrl) { - let discoButtonContent = _t("Disconnect"); + let discoButtonContent: React.ReactNode = _t("Disconnect"); let discoBodyText = _t( "Disconnecting from your identity server will mean you " + "won't be discoverable by other users and you won't be " + @@ -391,14 +403,14 @@ export default class SetIdServer extends React.Component { } discoSection =
      {discoBodyText} - + {discoButtonContent}
      ; } return ( - + {sectionTitle} @@ -411,15 +423,15 @@ export default class SetIdServer extends React.Component { autoComplete="off" placeholder={this.state.defaultIdServer} value={this.state.idServer} - onChange={this._onIdentityServerChanged} - tooltipContent={this._getTooltip()} + onChange={this.onIdentityServerChanged} + tooltipContent={this.getTooltip()} tooltipClassName="mx_SetIdServer_tooltip" disabled={this.state.busy} forceValidity={this.state.error ? false : null} /> {_t("Change")} {discoSection} diff --git a/src/components/views/settings/account/EmailAddresses.js b/src/components/views/settings/account/EmailAddresses.js index 1ebd374173..a36369cf88 100644 --- a/src/components/views/settings/account/EmailAddresses.js +++ b/src/components/views/settings/account/EmailAddresses.js @@ -90,12 +90,18 @@ export class ExistingEmailAddress extends React.Component { {_t("Remove %(email)s?", {email: this.props.email.address} )} - + {_t("Remove")} - + {_t("Cancel")}
      @@ -228,21 +234,28 @@ export default class EmailAddresses extends React.Component { ); if (this.state.verifying) { addButton = ( -
      -
      {_t("We've sent you an email to verify your address. Please follow the instructions there and then click the button below.")}
      - - {_t("Continue")} - -
      +
      +
      {_t("We've sent you an email to verify your address. Please follow the instructions there and then click the button below.")}
      + + {_t("Continue")} + +
      ); } return (
      {existingEmailElements} -
      + {_t("Remove %(phone)s?", {phone: this.props.msisdn.address})} - + {_t("Remove")} - + {_t("Cancel")}
      @@ -246,8 +252,11 @@ export default class PhoneNumbers extends React.Component { value={this.state.newPhoneNumberCode} onChange={this._onChangeNewPhoneNumberCode} /> - + {_t("Continue")} diff --git a/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.js b/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.js index cd4a043622..139cfd5fbd 100644 --- a/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.js +++ b/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.js @@ -80,9 +80,11 @@ export default class GeneralRoomSettingsTab extends React.Component { flairSection = <> {_t("Flair")}
      - +
      ; } @@ -97,8 +99,8 @@ export default class GeneralRoomSettingsTab extends React.Component {
      {_t("Room Addresses")}
      + canSetCanonicalAlias={canSetCanonical} canSetAliases={canSetAliases} + canonicalAliasEvent={canonicalAliasEv} aliasEvents={aliasEvents} />
      {_t("Other")}
      { flairSection } diff --git a/src/components/views/settings/tabs/room/NotificationSettingsTab.js b/src/components/views/settings/tabs/room/NotificationSettingsTab.js index baefb5ae20..fa56fa2cb6 100644 --- a/src/components/views/settings/tabs/room/NotificationSettingsTab.js +++ b/src/components/views/settings/tabs/room/NotificationSettingsTab.js @@ -155,7 +155,7 @@ export default class NotificationsSettingsTab extends React.Component {
      {_t("Notification sound")}: {this.state.currentSound}
      - {_t("Reset")} + {_t("Reset")}
      @@ -167,11 +167,11 @@ export default class NotificationsSettingsTab extends React.Component { {currentUploadedFile} - {_t("Browse")} + {_t("Browse")} - {_t("Save")} + {_t("Save")}
      diff --git a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.js b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx similarity index 89% rename from src/components/views/settings/tabs/room/RolesRoomSettingsTab.js rename to src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx index 59a175906d..4fa521f598 100644 --- a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.js +++ b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx @@ -1,5 +1,5 @@ /* -Copyright 2019, 2021 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. @@ -15,7 +15,6 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import {_t, _td} from "../../../../../languageHandler"; import {MatrixClientPeg} from "../../../../../MatrixClientPeg"; import * as sdk from "../../../../.."; @@ -23,6 +22,9 @@ import AccessibleButton from "../../../elements/AccessibleButton"; import Modal from "../../../../../Modal"; import {replaceableComponent} from "../../../../../utils/replaceableComponent"; import {EventType} from "matrix-js-sdk/src/@types/event"; +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"; const plEventsToLabels = { // These will be translated for us later. @@ -63,15 +65,15 @@ function parseIntWithDefault(val, def) { return isNaN(res) ? def : res; } -export class BannedUser extends React.Component { - static propTypes = { - canUnban: PropTypes.bool, - member: PropTypes.object.isRequired, // js-sdk RoomMember - by: PropTypes.string.isRequired, - reason: PropTypes.string, - }; +interface IBannedUserProps { + canUnban?: boolean; + member: RoomMember; + by: string; + reason?: string; +} - _onUnbanClick = (e) => { +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); @@ -87,8 +89,10 @@ export class BannedUser extends React.Component { if (this.props.canUnban) { unbanButton = ( - + { _t('Unban') } ); @@ -107,29 +111,29 @@ export class BannedUser extends React.Component { } } -@replaceableComponent("views.settings.tabs.room.RolesRoomSettingsTab") -export default class RolesRoomSettingsTab extends React.Component { - static propTypes = { - roomId: PropTypes.string.isRequired, - }; +interface IProps { + roomId: string; +} - componentDidMount(): void { - MatrixClientPeg.get().on("RoomState.members", this._onRoomMembership); +@replaceableComponent("views.settings.tabs.room.RolesRoomSettingsTab") +export default class RolesRoomSettingsTab extends React.Component { + componentDidMount() { + MatrixClientPeg.get().on("RoomState.members", this.onRoomMembership); } - componentWillUnmount(): void { + componentWillUnmount() { const client = MatrixClientPeg.get(); if (client) { - client.removeListener("RoomState.members", this._onRoomMembership); + client.removeListener("RoomState.members", this.onRoomMembership); } } - _onRoomMembership = (event, state, member) => { + private onRoomMembership = (event: MatrixEvent, state: RoomState, member: RoomMember) => { if (state.roomId !== this.props.roomId) return; this.forceUpdate(); }; - _populateDefaultPlEvents(eventsSection, stateLevel, eventsLevel) { + private populateDefaultPlEvents(eventsSection: Record, stateLevel: number, eventsLevel: number) { for (const desiredEvent of Object.keys(plEventsToShow)) { if (!(desiredEvent in eventsSection)) { eventsSection[desiredEvent] = (plEventsToShow[desiredEvent].isState ? stateLevel : eventsLevel); @@ -137,7 +141,7 @@ export default class RolesRoomSettingsTab extends React.Component { } } - _onPowerLevelsChanged = (value, powerLevelKey) => { + private onPowerLevelsChanged = (inputValue: string, powerLevelKey: string) => { const client = MatrixClientPeg.get(); const room = client.getRoom(this.props.roomId); const plEvent = room.currentState.getStateEvents('m.room.power_levels', ''); @@ -148,7 +152,7 @@ export default class RolesRoomSettingsTab extends React.Component { const eventsLevelPrefix = "event_levels_"; - value = parseInt(value); + const value = parseInt(inputValue); if (powerLevelKey.startsWith(eventsLevelPrefix)) { // deep copy "events" object, Object.assign itself won't deep copy @@ -182,7 +186,7 @@ export default class RolesRoomSettingsTab extends React.Component { }); }; - _onUserPowerLevelChanged = (value, powerLevelKey) => { + private onUserPowerLevelChanged = (value: string, powerLevelKey: string) => { const client = MatrixClientPeg.get(); const room = client.getRoom(this.props.roomId); const plEvent = room.currentState.getStateEvents('m.room.power_levels', ''); @@ -266,7 +270,7 @@ export default class RolesRoomSettingsTab extends React.Component { currentUserLevel = defaultUserLevel; } - this._populateDefaultPlEvents( + this.populateDefaultPlEvents( eventsLevels, parseIntWithDefault(plContent.state_default, powerLevelDescriptors.state_default.defaultValue), parseIntWithDefault(plContent.events_default, powerLevelDescriptors.events_default.defaultValue), @@ -288,7 +292,7 @@ export default class RolesRoomSettingsTab extends React.Component { label={user} key={user} powerLevelKey={user} // Will be sent as the second parameter to `onChange` - onChange={this._onUserPowerLevelChanged} + onChange={this.onUserPowerLevelChanged} />, ); } else if (userLevels[user] < defaultUserLevel) { // muted @@ -299,7 +303,7 @@ export default class RolesRoomSettingsTab extends React.Component { label={user} key={user} powerLevelKey={user} // Will be sent as the second parameter to `onChange` - onChange={this._onUserPowerLevelChanged} + onChange={this.onUserPowerLevelChanged} />, ); } @@ -345,8 +349,9 @@ export default class RolesRoomSettingsTab extends React.Component { if (sender) bannedBy = sender.name; return ( + member={member} reason={banEvent.reason} + by={bannedBy} + /> ); })} @@ -373,7 +378,7 @@ export default class RolesRoomSettingsTab extends React.Component { usersDefault={defaultUserLevel} disabled={!canChangeLevels || currentUserLevel < value} powerLevelKey={key} // Will be sent as the second parameter to `onChange` - onChange={this._onPowerLevelsChanged} + onChange={this.onPowerLevelsChanged} />
      ; }); @@ -398,7 +403,7 @@ export default class RolesRoomSettingsTab extends React.Component { usersDefault={defaultUserLevel} disabled={!canChangeLevels || currentUserLevel < eventsLevels[eventType]} powerLevelKey={"event_levels_" + eventType} - onChange={this._onPowerLevelsChanged} + onChange={this.onPowerLevelsChanged} />
      ); diff --git a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.js b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx similarity index 79% rename from src/components/views/settings/tabs/room/SecurityRoomSettingsTab.js rename to src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx index ce883c6d23..02bbcfb751 100644 --- a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.js +++ b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx @@ -1,5 +1,5 @@ /* -Copyright 2019 New Vector Ltd +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. @@ -15,7 +15,7 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import {_t} from "../../../../../languageHandler"; import {MatrixClientPeg} from "../../../../../MatrixClientPeg"; import * as sdk from "../../../../.."; @@ -26,64 +26,92 @@ import StyledRadioGroup from '../../../elements/StyledRadioGroup'; import {SettingLevel} from "../../../../../settings/SettingLevel"; import SettingsStore from "../../../../../settings/SettingsStore"; import {UIFeature} from "../../../../../settings/UIFeature"; -import {replaceableComponent} from "../../../../../utils/replaceableComponent"; +import { replaceableComponent } from "../../../../../utils/replaceableComponent"; + +// Knock and private are reserved keywords which are not yet implemented. +enum JoinRule { + Public = "public", + Knock = "knock", + Invite = "invite", + Private = "private", +} + +enum GuestAccess { + CanJoin = "can_join", + Forbidden = "forbidden", +} + +enum HistoryVisibility { + Invited = "invited", + Joined = "joined", + Shared = "shared", + WorldReadable = "world_readable", +} + +interface IProps { + roomId: string; +} + +interface IState { + joinRule: JoinRule; + guestAccess: GuestAccess; + history: HistoryVisibility; + hasAliases: boolean; + encrypted: boolean; +} @replaceableComponent("views.settings.tabs.room.SecurityRoomSettingsTab") -export default class SecurityRoomSettingsTab extends React.Component { - static propTypes = { - roomId: PropTypes.string.isRequired, - }; - - constructor() { - super(); +export default class SecurityRoomSettingsTab extends React.Component { + constructor(props) { + super(props); this.state = { - joinRule: "invite", - guestAccess: "can_join", - history: "shared", + joinRule: JoinRule.Invite, + guestAccess: GuestAccess.CanJoin, + history: HistoryVisibility.Shared, hasAliases: false, encrypted: false, }; } // TODO: [REACT-WARNING] Move this to constructor - async UNSAFE_componentWillMount(): void { // eslint-disable-line camelcase - MatrixClientPeg.get().on("RoomState.events", this._onStateEvent); + async UNSAFE_componentWillMount() { // eslint-disable-line camelcase + MatrixClientPeg.get().on("RoomState.events", this.onStateEvent); const room = MatrixClientPeg.get().getRoom(this.props.roomId); const state = room.currentState; - const joinRule = this._pullContentPropertyFromEvent( + const joinRule: JoinRule = this.pullContentPropertyFromEvent( state.getStateEvents("m.room.join_rules", ""), 'join_rule', - 'invite', + JoinRule.Invite, ); - const guestAccess = this._pullContentPropertyFromEvent( + const guestAccess: GuestAccess = this.pullContentPropertyFromEvent( state.getStateEvents("m.room.guest_access", ""), 'guest_access', - 'forbidden', + GuestAccess.Forbidden, ); - const history = this._pullContentPropertyFromEvent( + const history: HistoryVisibility = this.pullContentPropertyFromEvent( state.getStateEvents("m.room.history_visibility", ""), 'history_visibility', - 'shared', + HistoryVisibility.Shared, ); const encrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.roomId); this.setState({joinRule, guestAccess, history, encrypted}); - const hasAliases = await this._hasAliases(); + const hasAliases = await this.hasAliases(); this.setState({hasAliases}); } - _pullContentPropertyFromEvent(event, key, defaultValue) { + private pullContentPropertyFromEvent(event: MatrixEvent, key: string, defaultValue: T): T { if (!event || !event.getContent()) return defaultValue; return event.getContent()[key] || defaultValue; } - componentWillUnmount(): void { - MatrixClientPeg.get().removeListener("RoomState.events", this._onStateEvent); + componentWillUnmount() { + MatrixClientPeg.get().removeListener("RoomState.events", this.onStateEvent); } - _onStateEvent = (e) => { + private onStateEvent = (e: MatrixEvent) => { const refreshWhenTypes = [ 'm.room.join_rules', 'm.room.guest_access', @@ -93,7 +121,7 @@ export default class SecurityRoomSettingsTab extends React.Component { if (refreshWhenTypes.includes(e.getType())) this.forceUpdate(); }; - _onEncryptionChange = (e) => { + private onEncryptionChange = (e: React.ChangeEvent) => { Modal.createTrackedDialog('Enable encryption', '', QuestionDialog, { title: _t('Enable encryption?'), description: _t( @@ -102,10 +130,9 @@ export default class SecurityRoomSettingsTab extends React.Component { "may prevent many bots and bridges from working correctly. Learn more about encryption.", {}, { - 'a': (sub) => { - return {sub}; - }, + a: sub => {sub}, }, ), onFinished: (confirm) => { @@ -127,12 +154,12 @@ export default class SecurityRoomSettingsTab extends React.Component { }); }; - _fixGuestAccess = (e) => { + private fixGuestAccess = (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); - const joinRule = "invite"; - const guestAccess = "can_join"; + const joinRule = JoinRule.Invite; + const guestAccess = GuestAccess.CanJoin; const beforeJoinRule = this.state.joinRule; const beforeGuestAccess = this.state.guestAccess; @@ -149,7 +176,7 @@ export default class SecurityRoomSettingsTab extends React.Component { }); }; - _onRoomAccessRadioToggle = (roomAccess) => { + private onRoomAccessRadioToggle = (roomAccess: string) => { // join_rule // INVITE | PUBLIC // ----------------------+---------------- @@ -163,20 +190,20 @@ export default class SecurityRoomSettingsTab extends React.Component { // invite them, you clearly want them to join, whether they're a // guest or not. In practice, guest_access should probably have // been implemented as part of the join_rules enum. - let joinRule = "invite"; - let guestAccess = "can_join"; + let joinRule = JoinRule.Invite; + let guestAccess = GuestAccess.CanJoin; switch (roomAccess) { case "invite_only": // no change - use defaults above break; case "public_no_guests": - joinRule = "public"; - guestAccess = "forbidden"; + joinRule = JoinRule.Public; + guestAccess = GuestAccess.Forbidden; break; case "public_with_guests": - joinRule = "public"; - guestAccess = "can_join"; + joinRule = JoinRule.Public; + guestAccess = GuestAccess.CanJoin; break; } @@ -195,7 +222,7 @@ export default class SecurityRoomSettingsTab extends React.Component { }); }; - _onHistoryRadioToggle = (history) => { + private onHistoryRadioToggle = (history: HistoryVisibility) => { const beforeHistory = this.state.history; this.setState({history: history}); MatrixClientPeg.get().sendStateEvent(this.props.roomId, "m.room.history_visibility", { @@ -206,11 +233,11 @@ export default class SecurityRoomSettingsTab extends React.Component { }); }; - _updateBlacklistDevicesFlag = (checked) => { + private updateBlacklistDevicesFlag = (checked: boolean) => { MatrixClientPeg.get().getRoom(this.props.roomId).setBlacklistUnverifiedDevices(checked); }; - async _hasAliases() { + private async hasAliases(): Promise { const cli = MatrixClientPeg.get(); if (await cli.doesServerSupportUnstableFeature("org.matrix.msc2432")) { const response = await cli.unstableGetLocalAliases(this.props.roomId); @@ -224,7 +251,7 @@ export default class SecurityRoomSettingsTab extends React.Component { } } - _renderRoomAccess() { + private renderRoomAccess() { const client = MatrixClientPeg.get(); const room = client.getRoom(this.props.roomId); const joinRule = this.state.joinRule; @@ -240,7 +267,7 @@ export default class SecurityRoomSettingsTab extends React.Component { {_t("Guests cannot join this room even if explicitly invited.")}  - {_t("Click here to fix")} + {_t("Click here to fix")}
      ); @@ -265,7 +292,7 @@ export default class SecurityRoomSettingsTab extends React.Component { ; } @@ -356,7 +383,7 @@ export default class SecurityRoomSettingsTab extends React.Component { let historySection = (<> {_t("Who can read history?")}
      - {this._renderHistory()} + {this.renderHistory()}
      ); if (!SettingsStore.getValue(UIFeature.RoomHistorySettings)) { @@ -373,15 +400,16 @@ export default class SecurityRoomSettingsTab extends React.Component {
      {_t("Once enabled, encryption cannot be disabled.")}
      - +
      {encryptionSettings}
      {_t("Who can access this room?")}
      - {this._renderRoomAccess()} + {this.renderRoomAccess()}
      {historySection} diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js index b1ad9f3d23..5118414903 100644 --- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js @@ -192,7 +192,11 @@ export default class GeneralUserSettingsTab extends React.Component { SettingsStore.setValue("language", null, SettingLevel.DEVICE, newLanguage); this.setState({language: newLanguage}); - PlatformPeg.get().reload(); + const platform = PlatformPeg.get(); + if (platform) { + platform.setLanguage(newLanguage); + platform.reload(); + } }; _onSpellCheckLanguagesChange = (languages) => { @@ -319,8 +323,11 @@ export default class GeneralUserSettingsTab extends React.Component { return (
      {_t("Language and region")} - +
      ); } @@ -329,8 +336,10 @@ export default class GeneralUserSettingsTab extends React.Component { return (
      {_t("Spell check dictionaries")} - +
      ); } diff --git a/src/components/views/settings/tabs/user/HelpUserSettingsTab.js b/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx similarity index 64% rename from src/components/views/settings/tabs/user/HelpUserSettingsTab.js rename to src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx index e16ee686f5..3fa0be478c 100644 --- a/src/components/views/settings/tabs/user/HelpUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx @@ -1,6 +1,5 @@ /* -Copyright 2019 New Vector Ltd -Copyright 2020 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,27 +15,37 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import {_t, getCurrentLanguage} from "../../../../../languageHandler"; import {MatrixClientPeg} from "../../../../../MatrixClientPeg"; import AccessibleButton from "../../../elements/AccessibleButton"; +import AccessibleTooltipButton from '../../../elements/AccessibleTooltipButton'; import SdkConfig from "../../../../../SdkConfig"; import createRoom from "../../../../../createRoom"; import Modal from "../../../../../Modal"; -import * as sdk from "../../../../../"; +import * as sdk from "../../../../.."; import PlatformPeg from "../../../../../PlatformPeg"; import * as KeyboardShortcuts from "../../../../../accessibility/KeyboardShortcuts"; import UpdateCheckButton from "../../UpdateCheckButton"; -import {replaceableComponent} from "../../../../../utils/replaceableComponent"; +import { replaceableComponent } from "../../../../../utils/replaceableComponent"; +import { copyPlaintext } from "../../../../../utils/strings"; +import * as ContextMenu from "../../../../structures/ContextMenu"; +import { toRightOf } from "../../../../structures/ContextMenu"; + +interface IProps { + closeSettingsFn: () => {}; +} + +interface IState { + appVersion: string; + canUpdate: boolean; +} @replaceableComponent("views.settings.tabs.user.HelpUserSettingsTab") -export default class HelpUserSettingsTab extends React.Component { - static propTypes = { - closeSettingsFn: PropTypes.func.isRequired, - }; +export default class HelpUserSettingsTab extends React.Component { + protected closeCopiedTooltip: () => void; - constructor() { - super(); + constructor(props) { + super(props); this.state = { appVersion: null, @@ -53,7 +62,13 @@ export default class HelpUserSettingsTab extends React.Component { }); } - _onClearCacheAndReload = (e) => { + componentWillUnmount() { + // if the Copied tooltip is open then get rid of it, there are ways to close the modal which wouldn't close + // the tooltip otherwise, such as pressing Escape + if (this.closeCopiedTooltip) this.closeCopiedTooltip(); + } + + private onClearCacheAndReload = (e) => { if (!PlatformPeg.get()) return; // Dev note: please keep this log line, it's useful when troubleshooting a MatrixClient suddenly @@ -65,7 +80,7 @@ export default class HelpUserSettingsTab extends React.Component { }); }; - _onBugReport = (e) => { + private onBugReport = (e) => { const BugReportDialog = sdk.getComponent("dialogs.BugReportDialog"); if (!BugReportDialog) { return; @@ -73,7 +88,7 @@ export default class HelpUserSettingsTab extends React.Component { Modal.createTrackedDialog('Bug Report Dialog', '', BugReportDialog, {}); }; - _onStartBotChat = (e) => { + private onStartBotChat = (e) => { this.props.closeSettingsFn(); createRoom({ dmUserId: SdkConfig.get().welcomeUserId, @@ -81,7 +96,7 @@ export default class HelpUserSettingsTab extends React.Component { }); }; - _showSpoiler = (event) => { + private showSpoiler = (event) => { const target = event.target; target.innerHTML = target.getAttribute('data-spoiler'); @@ -93,7 +108,7 @@ export default class HelpUserSettingsTab extends React.Component { selection.addRange(range); }; - _renderLegal() { + private renderLegal() { const tocLinks = SdkConfig.get().terms_and_conditions_links; if (!tocLinks) return null; @@ -114,7 +129,7 @@ export default class HelpUserSettingsTab extends React.Component { ); } - _renderCredits() { + private renderCredits() { // Note: This is not translated because it is legal text. // Also,   is ugly but necessary. return ( @@ -122,34 +137,48 @@ export default class HelpUserSettingsTab extends React.Component { {_t("Credits")}
      ); } + onAccessTokenCopyClick = async (e) => { + e.preventDefault(); + const target = e.target; // copy target before we go async and React throws it away + + 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'), + }); + this.closeCopiedTooltip = target.onmouseleave = close; + } + render() { const brand = SdkConfig.get().brand; @@ -188,7 +217,7 @@ export default class HelpUserSettingsTab extends React.Component { }, )}
      - + {_t("Chat with %(brand)s Bot", { brand })}
      @@ -212,28 +241,27 @@ export default class HelpUserSettingsTab extends React.Component {
      {_t('Bug reporting')}
      - { - _t( "If you've submitted a bug via GitHub, debug logs can help " + - "us track down the problem. Debug logs contain application " + - "usage data including your username, the IDs or aliases of " + - "the rooms or groups you have visited and the usernames of " + - "other users. They do not contain messages.", - ) - } + {_t( + "If you've submitted a bug via GitHub, debug logs can help " + + "us track down the problem. Debug logs contain application " + + "usage data including your username, the IDs or aliases of " + + "the rooms or groups you have visited and the usernames of " + + "other users. They do not contain messages.", + )}
      - + {_t("Submit debug logs")}
      - { - _t( "To report a Matrix-related security issue, please read the Matrix.org " + - "Security Disclosure Policy.", {}, - { - 'a': (sub) => - {sub}, - }) - } + {_t( + "To report a Matrix-related security issue, please read the Matrix.org " + + "Security Disclosure Policy.", {}, + { + a: sub => {sub}, + }, + )}
      ); @@ -260,20 +288,29 @@ export default class HelpUserSettingsTab extends React.Component { {updateButton}
      - {this._renderLegal()} - {this._renderCredits()} + {this.renderLegal()} + {this.renderCredits()}
      {_t("Advanced")}
      {_t("Homeserver is")} {MatrixClientPeg.get().getHomeserverUrl()}
      {_t("Identity Server is")} {MatrixClientPeg.get().getIdentityServerUrl()}
      - {_t("Access Token:") + ' '} - - <{ _t("click to reveal") }> - +
      +
      + {_t("Access Token")}
      + {_t("Your access token gives full access to your account." + + " Do not share it with anyone." )} +
      + {MatrixClientPeg.get().getAccessToken()} + +
      +

      - + {_t("Clear cache and reload")}
      diff --git a/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.js b/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.tsx similarity index 90% rename from src/components/views/settings/tabs/user/MjolnirUserSettingsTab.js rename to src/components/views/settings/tabs/user/MjolnirUserSettingsTab.tsx index 91f6728a7a..6997defea9 100644 --- a/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.tsx @@ -1,5 +1,5 @@ /* -Copyright 2019, 2020 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. @@ -25,10 +25,16 @@ import {MatrixClientPeg} from "../../../../../MatrixClientPeg"; import * as sdk from "../../../../../index"; import {replaceableComponent} from "../../../../../utils/replaceableComponent"; +interface IState { + busy: boolean; + newPersonalRule: string; + newList: string; +} + @replaceableComponent("views.settings.tabs.user.MjolnirUserSettingsTab") -export default class MjolnirUserSettingsTab extends React.Component { - constructor() { - super(); +export default class MjolnirUserSettingsTab extends React.Component<{}, IState> { + constructor(props) { + super(props); this.state = { busy: false, @@ -37,15 +43,15 @@ export default class MjolnirUserSettingsTab extends React.Component { }; } - _onPersonalRuleChanged = (e) => { + private onPersonalRuleChanged = (e) => { this.setState({newPersonalRule: e.target.value}); }; - _onNewListChanged = (e) => { + private onNewListChanged = (e) => { this.setState({newList: e.target.value}); }; - _onAddPersonalRule = async (e) => { + private onAddPersonalRule = async (e) => { e.preventDefault(); e.stopPropagation(); @@ -72,7 +78,7 @@ export default class MjolnirUserSettingsTab extends React.Component { } }; - _onSubscribeList = async (e) => { + private onSubscribeList = async (e) => { e.preventDefault(); e.stopPropagation(); @@ -94,7 +100,7 @@ export default class MjolnirUserSettingsTab extends React.Component { } }; - async _removePersonalRule(rule: ListRule) { + private async removePersonalRule(rule: ListRule) { this.setState({busy: true}); try { const list = Mjolnir.sharedInstance().getPersonalList(); @@ -112,7 +118,7 @@ export default class MjolnirUserSettingsTab extends React.Component { } } - async _unsubscribeFromList(list: BanList) { + private async unsubscribeFromList(list: BanList) { this.setState({busy: true}); try { await Mjolnir.sharedInstance().unsubscribeFromList(list.roomId); @@ -130,7 +136,7 @@ export default class MjolnirUserSettingsTab extends React.Component { } } - _viewListRules(list: BanList) { + private viewListRules(list: BanList) { const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); const room = MatrixClientPeg.get().getRoom(list.roomId); @@ -161,7 +167,7 @@ export default class MjolnirUserSettingsTab extends React.Component { }); } - _renderPersonalBanListRules() { + private renderPersonalBanListRules() { const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const list = Mjolnir.sharedInstance().getPersonalList(); @@ -174,7 +180,7 @@ export default class MjolnirUserSettingsTab extends React.Component {
    4. this._removePersonalRule(rule)} + onClick={() => this.removePersonalRule(rule)} disabled={this.state.busy} > {_t("Remove")} @@ -192,7 +198,7 @@ export default class MjolnirUserSettingsTab extends React.Component { ); } - _renderSubscribedBanLists() { + private renderSubscribedBanLists() { const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const personalList = Mjolnir.sharedInstance().getPersonalList(); @@ -209,14 +215,14 @@ export default class MjolnirUserSettingsTab extends React.Component {
    5. this._unsubscribeFromList(list)} + onClick={() => this.unsubscribeFromList(list)} disabled={this.state.busy} > {_t("Unsubscribe")}   this._viewListRules(list)} + onClick={() => this.viewListRules(list)} disabled={this.state.busy} > {_t("View rules")} @@ -271,21 +277,21 @@ export default class MjolnirUserSettingsTab extends React.Component { )}
    6. - {this._renderPersonalBanListRules()} + {this.renderPersonalBanListRules()}
      -
      + {_t("Ignore")} @@ -303,20 +309,20 @@ export default class MjolnirUserSettingsTab extends React.Component { )}
      - {this._renderSubscribedBanLists()} + {this.renderSubscribedBanLists()}
      - + {_t("Subscribe")} diff --git a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx similarity index 80% rename from src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js rename to src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx index 0cd3dd6698..f02c5c9ce0 100644 --- a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx @@ -1,5 +1,5 @@ /* -Copyright 2019 New Vector Ltd +Copyright 2019-2021 The Matrix.org Foundation C.I.C. Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> Licensed under the Apache License, Version 2.0 (the "License"); @@ -23,10 +23,24 @@ import Field from "../../../elements/Field"; import * as sdk from "../../../../.."; import PlatformPeg from "../../../../../PlatformPeg"; import {SettingLevel} from "../../../../../settings/SettingLevel"; -import {replaceableComponent} from "../../../../../utils/replaceableComponent"; +import { replaceableComponent } from "../../../../../utils/replaceableComponent"; + +interface IState { + autoLaunch: boolean; + autoLaunchSupported: boolean; + warnBeforeExit: boolean; + warnBeforeExitSupported: boolean; + alwaysShowMenuBarSupported: boolean; + alwaysShowMenuBar: boolean; + minimizeToTraySupported: boolean; + minimizeToTray: boolean; + autocompleteDelay: string; + readMarkerInViewThresholdMs: string; + readMarkerOutOfViewThresholdMs: string; +} @replaceableComponent("views.settings.tabs.user.PreferencesUserSettingsTab") -export default class PreferencesUserSettingsTab extends React.Component { +export default class PreferencesUserSettingsTab extends React.Component<{}, IState> { static ROOM_LIST_SETTINGS = [ 'breadcrumbs', ]; @@ -68,8 +82,8 @@ export default class PreferencesUserSettingsTab extends React.Component { // Autocomplete delay (niche text box) ]; - constructor() { - super(); + constructor(props) { + super(props); this.state = { autoLaunch: false, @@ -89,7 +103,7 @@ export default class PreferencesUserSettingsTab extends React.Component { }; } - async componentDidMount(): void { + async componentDidMount() { const platform = PlatformPeg.get(); const autoLaunchSupported = await platform.supportsAutoLaunch(); @@ -128,38 +142,38 @@ export default class PreferencesUserSettingsTab extends React.Component { }); } - _onAutoLaunchChange = (checked) => { + private onAutoLaunchChange = (checked: boolean) => { PlatformPeg.get().setAutoLaunchEnabled(checked).then(() => this.setState({autoLaunch: checked})); }; - _onWarnBeforeExitChange = (checked) => { + private onWarnBeforeExitChange = (checked: boolean) => { PlatformPeg.get().setWarnBeforeExit(checked).then(() => this.setState({warnBeforeExit: checked})); } - _onAlwaysShowMenuBarChange = (checked) => { + private onAlwaysShowMenuBarChange = (checked: boolean) => { PlatformPeg.get().setAutoHideMenuBarEnabled(!checked).then(() => this.setState({alwaysShowMenuBar: checked})); }; - _onMinimizeToTrayChange = (checked) => { + private onMinimizeToTrayChange = (checked: boolean) => { PlatformPeg.get().setMinimizeToTrayEnabled(checked).then(() => this.setState({minimizeToTray: checked})); }; - _onAutocompleteDelayChange = (e) => { + private onAutocompleteDelayChange = (e: React.ChangeEvent) => { this.setState({autocompleteDelay: e.target.value}); SettingsStore.setValue("autocompleteDelay", null, SettingLevel.DEVICE, e.target.value); }; - _onReadMarkerInViewThresholdMs = (e) => { + private onReadMarkerInViewThresholdMs = (e: React.ChangeEvent) => { this.setState({readMarkerInViewThresholdMs: e.target.value}); SettingsStore.setValue("readMarkerInViewThresholdMs", null, SettingLevel.DEVICE, e.target.value); }; - _onReadMarkerOutOfViewThresholdMs = (e) => { + private onReadMarkerOutOfViewThresholdMs = (e: React.ChangeEvent) => { this.setState({readMarkerOutOfViewThresholdMs: e.target.value}); SettingsStore.setValue("readMarkerOutOfViewThresholdMs", null, SettingLevel.DEVICE, e.target.value); }; - _renderGroup(settingIds) { + private renderGroup(settingIds: string[]): React.ReactNodeArray { const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag"); return settingIds.filter(SettingsStore.isEnabled).map(i => { return ; @@ -171,7 +185,7 @@ export default class PreferencesUserSettingsTab extends React.Component { if (this.state.autoLaunchSupported) { autoLaunchOption = ; } @@ -179,7 +193,7 @@ export default class PreferencesUserSettingsTab extends React.Component { if (this.state.warnBeforeExitSupported) { warnBeforeExitOption = ; } @@ -187,7 +201,7 @@ export default class PreferencesUserSettingsTab extends React.Component { if (this.state.alwaysShowMenuBarSupported) { autoHideMenuOption = ; } @@ -195,7 +209,7 @@ export default class PreferencesUserSettingsTab extends React.Component { if (this.state.minimizeToTraySupported) { minimizeToTrayOption = ; } @@ -205,22 +219,22 @@ export default class PreferencesUserSettingsTab extends React.Component {
      {_t("Room list")} - {this._renderGroup(PreferencesUserSettingsTab.ROOM_LIST_SETTINGS)} + {this.renderGroup(PreferencesUserSettingsTab.ROOM_LIST_SETTINGS)}
      {_t("Composer")} - {this._renderGroup(PreferencesUserSettingsTab.COMPOSER_SETTINGS)} + {this.renderGroup(PreferencesUserSettingsTab.COMPOSER_SETTINGS)}
      {_t("Timeline")} - {this._renderGroup(PreferencesUserSettingsTab.TIMELINE_SETTINGS)} + {this.renderGroup(PreferencesUserSettingsTab.TIMELINE_SETTINGS)}
      {_t("General")} - {this._renderGroup(PreferencesUserSettingsTab.GENERAL_SETTINGS)} + {this.renderGroup(PreferencesUserSettingsTab.GENERAL_SETTINGS)} {minimizeToTrayOption} {autoHideMenuOption} {autoLaunchOption} @@ -229,17 +243,17 @@ export default class PreferencesUserSettingsTab extends React.Component { label={_t('Autocomplete delay (ms)')} type='number' value={this.state.autocompleteDelay} - onChange={this._onAutocompleteDelayChange} /> + onChange={this.onAutocompleteDelayChange} /> + onChange={this.onReadMarkerInViewThresholdMs} /> + onChange={this.onReadMarkerOutOfViewThresholdMs} />
      ); diff --git a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js index 8a70811399..53ed511b0a 100644 --- a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js @@ -255,15 +255,18 @@ export default class SecurityUserSettingsTab extends React.Component { _renderIgnoredUsers() { const {waitingUnignored, ignoredUserIds} = this.state; - if (!ignoredUserIds || ignoredUserIds.length === 0) return null; - - const userIds = ignoredUserIds - .map((u) => ); + const userIds = !ignoredUserIds?.length + ? _t('You have no ignored users.') + : ignoredUserIds.map((u) => { + return ( + + ); + }); return (
      diff --git a/src/components/views/settings/tabs/user/VoiceUserSettingsTab.js b/src/components/views/settings/tabs/user/VoiceUserSettingsTab.js index d8adab55f6..362059f8ed 100644 --- a/src/components/views/settings/tabs/user/VoiceUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/VoiceUserSettingsTab.js @@ -176,8 +176,8 @@ export default class VoiceUserSettingsTab extends React.Component { const defaultDevice = getDefaultDevice(audioOutputs); speakerDropdown = ( + value={this.state.activeAudioOutput || defaultDevice} + onChange={this._setAudioOutput}> {this._renderDeviceOptions(audioOutputs, 'audioOutput')} ); @@ -188,8 +188,8 @@ export default class VoiceUserSettingsTab extends React.Component { const defaultDevice = getDefaultDevice(audioInputs); microphoneDropdown = ( + value={this.state.activeAudioInput || defaultDevice} + onChange={this._setAudioInput}> {this._renderDeviceOptions(audioInputs, 'audioInput')} ); @@ -200,8 +200,8 @@ export default class VoiceUserSettingsTab extends React.Component { const defaultDevice = getDefaultDevice(videoInputs); webcamDropdown = ( + value={this.state.activeVideoInput || defaultDevice} + onChange={this._setVideoInput}> {this._renderDeviceOptions(videoInputs, 'videoInput')} ); diff --git a/src/components/views/spaces/SpaceTreeLevel.tsx b/src/components/views/spaces/SpaceTreeLevel.tsx index 6825d84013..e48e1d5dc2 100644 --- a/src/components/views/spaces/SpaceTreeLevel.tsx +++ b/src/components/views/spaces/SpaceTreeLevel.tsx @@ -297,15 +297,19 @@ export class SpaceItem extends React.PureComponent { const isActive = activeSpaces.includes(space); const itemClasses = classNames({ "mx_SpaceItem": true, + "mx_SpaceItem_narrow": isNarrow, "collapsed": collapsed, "hasSubSpaces": childSpaces && childSpaces.length, }); + + const isInvite = space.getMyMembership() === "invite"; const classes = classNames("mx_SpaceButton", { mx_SpaceButton_active: isActive, mx_SpaceButton_hasMenuOpen: !!this.state.contextMenuPosition, mx_SpaceButton_narrow: isNarrow, + mx_SpaceButton_invite: isInvite, }); - const notificationState = space.getMyMembership() === "invite" + const notificationState = isInvite ? StaticNotificationState.forSymbol("!", NotificationColor.Red) : SpaceStore.instance.getNotificationState(space.roomId); diff --git a/src/components/views/verification/VerificationCancelled.js b/src/components/views/verification/VerificationCancelled.js index 0bbaea1804..c57094d9b5 100644 --- a/src/components/views/verification/VerificationCancelled.js +++ b/src/components/views/verification/VerificationCancelled.js @@ -29,14 +29,14 @@ export default class VerificationCancelled extends React.Component { render() { const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); return
      -

      {_t( - "The other party cancelled the verification.", - )}

      - +

      {_t( + "The other party cancelled the verification.", + )}

      +
      ; } } diff --git a/src/components/views/voice_messages/Clock.tsx b/src/components/views/voice_messages/Clock.tsx index 6c256957e9..23e6762c52 100644 --- a/src/components/views/voice_messages/Clock.tsx +++ b/src/components/views/voice_messages/Clock.tsx @@ -29,14 +29,20 @@ interface IState { * displayed, making it possible to see "82:29". */ @replaceableComponent("views.voice_messages.Clock") -export default class Clock extends React.PureComponent { +export default class Clock extends React.Component { public constructor(props) { super(props); } + shouldComponentUpdate(nextProps: Readonly, nextState: Readonly, nextContext: any): boolean { + const currentFloor = Math.floor(this.props.seconds); + const nextFloor = Math.floor(nextProps.seconds); + return currentFloor !== nextFloor; + } + public render() { const minutes = Math.floor(this.props.seconds / 60).toFixed(0).padStart(2, '0'); - const seconds = Math.round(this.props.seconds % 60).toFixed(0).padStart(2, '0'); // hide millis + const seconds = Math.floor(this.props.seconds % 60).toFixed(0).padStart(2, '0'); // hide millis return {minutes}:{seconds}; } } diff --git a/src/components/views/voice_messages/LiveRecordingClock.tsx b/src/components/views/voice_messages/LiveRecordingClock.tsx index 5e9006c6ab..b82539eb16 100644 --- a/src/components/views/voice_messages/LiveRecordingClock.tsx +++ b/src/components/views/voice_messages/LiveRecordingClock.tsx @@ -31,7 +31,7 @@ interface IState { * A clock for a live recording. */ @replaceableComponent("views.voice_messages.LiveRecordingClock") -export default class LiveRecordingClock extends React.Component { +export default class LiveRecordingClock extends React.PureComponent { public constructor(props) { super(props); @@ -39,12 +39,6 @@ export default class LiveRecordingClock extends React.Component this.props.recorder.liveData.onUpdate(this.onRecordingUpdate); } - shouldComponentUpdate(nextProps: Readonly, nextState: Readonly, nextContext: any): boolean { - const currentFloor = Math.floor(this.state.seconds); - const nextFloor = Math.floor(nextState.seconds); - return currentFloor !== nextFloor; - } - private onRecordingUpdate = (update: IRecordingUpdate) => { this.setState({seconds: update.timeSeconds}); }; diff --git a/src/components/views/voice_messages/LiveRecordingWaveform.tsx b/src/components/views/voice_messages/LiveRecordingWaveform.tsx index c1f5e97fff..aab89f6ab1 100644 --- a/src/components/views/voice_messages/LiveRecordingWaveform.tsx +++ b/src/components/views/voice_messages/LiveRecordingWaveform.tsx @@ -15,7 +15,7 @@ limitations under the License. */ import React from "react"; -import {IRecordingUpdate, VoiceRecording} from "../../../voice/VoiceRecording"; +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"; @@ -29,8 +29,6 @@ interface IState { heights: number[]; } -const DOWNSAMPLE_TARGET = 35; // number of bars we want - /** * A waveform which shows the waveform of a live recording */ @@ -39,14 +37,14 @@ export default class LiveRecordingWaveform extends React.PureComponent { // 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), DOWNSAMPLE_TARGET); + 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 diff --git a/src/components/views/voice_messages/PlayPauseButton.tsx b/src/components/views/voice_messages/PlayPauseButton.tsx new file mode 100644 index 0000000000..1f87eb012d --- /dev/null +++ b/src/components/views/voice_messages/PlayPauseButton.tsx @@ -0,0 +1,61 @@ +/* +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, {ReactNode} from "react"; +import {replaceableComponent} from "../../../utils/replaceableComponent"; +import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; +import {_t} from "../../../languageHandler"; +import {Playback, PlaybackState} from "../../../voice/Playback"; +import classNames from "classnames"; + +interface IProps { + // Playback instance to manipulate. Cannot change during the component lifecycle. + playback: Playback; + + // The playback phase to render. Able to change during the component lifecycle. + playbackPhase: PlaybackState; +} + +/** + * 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") +export default class PlayPauseButton extends React.PureComponent { + public constructor(props) { + super(props); + } + + private onClick = async () => { + await this.props.playback.toggle(); + }; + + public render(): ReactNode { + const isPlaying = this.props.playback.isPlaying; + const isDisabled = this.props.playbackPhase === PlaybackState.Decoding; + const classes = classNames('mx_PlayPauseButton', { + 'mx_PlayPauseButton_play': !isPlaying, + 'mx_PlayPauseButton_pause': isPlaying, + 'mx_PlayPauseButton_disabled': isDisabled, + }); + return ; + } +} diff --git a/src/components/views/voice_messages/PlaybackClock.tsx b/src/components/views/voice_messages/PlaybackClock.tsx new file mode 100644 index 0000000000..2e8ec9a3e7 --- /dev/null +++ b/src/components/views/voice_messages/PlaybackClock.tsx @@ -0,0 +1,71 @@ +/* +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, PlaybackState} from "../../../voice/Playback"; +import {UPDATE_EVENT} from "../../../stores/AsyncStore"; + +interface IProps { + playback: Playback; +} + +interface IState { + seconds: number; + durationSeconds: number; + playbackPhase: PlaybackState; +} + +/** + * A clock for a playback of a recording. + */ +@replaceableComponent("views.voice_messages.PlaybackClock") +export default class PlaybackClock extends React.PureComponent { + public constructor(props) { + super(props); + + this.state = { + seconds: this.props.playback.clockInfo.timeSeconds, + // 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, + playbackPhase: PlaybackState.Stopped, // assume not started, so full clock + }; + this.props.playback.on(UPDATE_EVENT, this.onPlaybackUpdate); + this.props.playback.clockInfo.liveData.onUpdate(this.onTimeUpdate); + } + + private onPlaybackUpdate = (ev: PlaybackState) => { + // Convert Decoding -> Stopped because we don't care about the distinction here + if (ev === PlaybackState.Decoding) ev = PlaybackState.Stopped; + this.setState({playbackPhase: ev}); + }; + + private onTimeUpdate = (time: number[]) => { + this.setState({seconds: time[0], durationSeconds: time[1]}); + }; + + public render() { + let seconds = this.state.seconds; + if (this.state.playbackPhase === PlaybackState.Stopped) { + seconds = this.state.durationSeconds; + } + return ; + } +} diff --git a/src/components/views/voice_messages/PlaybackWaveform.tsx b/src/components/views/voice_messages/PlaybackWaveform.tsx new file mode 100644 index 0000000000..123af5dfa5 --- /dev/null +++ b/src/components/views/voice_messages/PlaybackWaveform.tsx @@ -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. +*/ + +import React from "react"; +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"; + +interface IProps { + playback: Playback; +} + +interface IState { + heights: number[]; + progress: number; +} + +/** + * A waveform which shows the waveform of a previously recorded recording + */ +@replaceableComponent("views.voice_messages.PlaybackWaveform") +export default class PlaybackWaveform extends React.PureComponent { + public constructor(props) { + super(props); + + this.state = { + heights: this.toHeights(this.props.playback.waveform), + progress: 0, // default no progress + }; + + this.props.playback.waveformData.onUpdate(this.onWaveformUpdate); + this.props.playback.clockInfo.liveData.onUpdate(this.onTimeUpdate); + } + + private toHeights(waveform: number[]) { + const seed = arraySeed(0, PLAYBACK_WAVEFORM_SAMPLES); + return arrayTrimFill(waveform, PLAYBACK_WAVEFORM_SAMPLES, seed); + } + + private onWaveformUpdate = (waveform: number[]) => { + this.setState({heights: this.toHeights(waveform)}); + }; + + private onTimeUpdate = (time: number[]) => { + // Track percentages to very coarse precision, otherwise 0.002 ends up highlighting a bar. + const progress = Number(percentageOf(time[0], 0, time[1]).toFixed(1)); + this.setState({progress}); + }; + + public render() { + return ; + } +} diff --git a/src/components/views/voice_messages/RecordingPlayback.tsx b/src/components/views/voice_messages/RecordingPlayback.tsx new file mode 100644 index 0000000000..776997cec2 --- /dev/null +++ b/src/components/views/voice_messages/RecordingPlayback.tsx @@ -0,0 +1,62 @@ +/* +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, {ReactNode} from "react"; +import {UPDATE_EVENT} from "../../../stores/AsyncStore"; +import PlaybackWaveform from "./PlaybackWaveform"; +import PlayPauseButton from "./PlayPauseButton"; +import PlaybackClock from "./PlaybackClock"; + +interface IProps { + // Playback instance to render. Cannot change during component lifecycle: create + // an all-new component instead. + playback: Playback; +} + +interface IState { + playbackPhase: PlaybackState; +} + +export default class RecordingPlayback extends React.PureComponent { + 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}); + }; + + public render(): ReactNode { + return
      + + + +
      + } +} diff --git a/src/components/views/voice_messages/Waveform.tsx b/src/components/views/voice_messages/Waveform.tsx index 5fa68dcadc..840a5a12b3 100644 --- a/src/components/views/voice_messages/Waveform.tsx +++ b/src/components/views/voice_messages/Waveform.tsx @@ -16,9 +16,11 @@ limitations under the License. import React from "react"; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import classNames from "classnames"; interface IProps { relHeights: number[]; // relative heights (0-1) + progress: number; // percent complete, 0-1, default 100% } interface IState { @@ -28,9 +30,16 @@ interface IState { * A simple waveform component. This renders bars (centered vertically) for each * height provided in the component properties. Updating the properties will update * the rendered waveform. + * + * 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") export default class Waveform extends React.PureComponent { + public static defaultProps = { + progress: 1, + }; + public constructor(props) { super(props); } @@ -38,7 +47,13 @@ export default class Waveform extends React.PureComponent { public render() { return
      {this.props.relHeights.map((h, i) => { - return ; + const progress = this.props.progress; + const isCompleteBar = (i / this.props.relHeights.length) <= progress && progress > 0; + const classes = classNames({ + 'mx_Waveform_bar': true, + 'mx_Waveform_bar_100pct': isCompleteBar, + }); + return ; })}
      ; } diff --git a/src/components/views/voip/AudioFeed.tsx b/src/components/views/voip/AudioFeed.tsx new file mode 100644 index 0000000000..c78f0c0fc8 --- /dev/null +++ b/src/components/views/voip/AudioFeed.tsx @@ -0,0 +1,97 @@ +/* +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 React, {createRef} from 'react'; +import { CallFeed, CallFeedEvent } from 'matrix-js-sdk/src/webrtc/callFeed'; +import { logger } from 'matrix-js-sdk/src/logger'; +import CallMediaHandler from "../../../CallMediaHandler"; + +interface IProps { + feed: CallFeed, +} + +export default class AudioFeed extends React.Component { + private element = createRef(); + + componentDidMount() { + this.props.feed.addListener(CallFeedEvent.NewStream, this.onNewStream); + this.playMedia(); + } + + componentWillUnmount() { + this.props.feed.removeListener(CallFeedEvent.NewStream, this.onNewStream); + this.stopMedia(); + } + + private playMedia() { + const element = this.element.current; + const audioOutput = CallMediaHandler.getAudioOutput(); + + if (audioOutput) { + try { + // This seems quite unreliable in Chrome, although I haven't yet managed to make a jsfiddle where + // it fails. + // It seems reliable if you set the sink ID after setting the srcObject and then set the sink ID + // back to the default after the call is over - Dave + element.setSinkId(audioOutput); + } catch (e) { + console.error("Couldn't set requested audio output device: using default", e); + logger.warn("Couldn't set requested audio output device: using default", e); + } + } + + element.muted = false; + element.srcObject = this.props.feed.stream; + element.autoplay = true; + + try { + // A note on calling methods on media elements: + // We used to have queues per media element to serialise all calls on those elements. + // The reason given for this was that load() and play() were racing. However, we now + // never call load() explicitly so this seems unnecessary. However, serialising every + // operation was causing bugs where video would not resume because some play command + // had got stuck and all media operations were queued up behind it. If necessary, we + // should serialise the ones that need to be serialised but then be able to interrupt + // them with another load() which will cancel the pending one, but since we don't call + // load() explicitly, it shouldn't be a problem. - Dave + element.play() + } catch (e) { + logger.info("Failed to play media element with feed", this.props.feed, e); + } + } + + private stopMedia() { + const element = this.element.current; + + element.pause(); + element.src = null; + + // As per comment in componentDidMount, setting the sink ID back to the + // default once the call is over makes setSinkId work reliably. - Dave + // Since we are not using the same element anymore, the above doesn't + // seem to be necessary - Šimon + } + + private onNewStream = () => { + this.playMedia(); + }; + + render() { + return ( +
      ; + const avatarSize = this.props.pipMode ? 76 : 160; + // The 'content' for the call, ie. the videos for a video call and profile picture // for voice calls (fills the bg) let contentView: React.ReactNode; @@ -482,11 +492,13 @@ export default class CallView extends React.Component { const isOnHold = this.state.isLocalOnHold || this.state.isRemoteOnHold; let holdTransferContent; if (transfereeCall) { - const transferTargetRoom = MatrixClientPeg.get().getRoom(CallHandler.roomIdForCall(this.props.call)); + const transferTargetRoom = MatrixClientPeg.get().getRoom( + CallHandler.sharedInstance().roomIdForCall(this.props.call), + ); const transferTargetName = transferTargetRoom ? transferTargetRoom.name : _t("unknown person"); const transfereeRoom = MatrixClientPeg.get().getRoom( - CallHandler.roomIdForCall(transfereeCall), + CallHandler.sharedInstance().roomIdForCall(transfereeCall), ); const transfereeName = transfereeRoom ? transfereeRoom.name : _t("unknown person"); @@ -522,41 +534,85 @@ export default class CallView extends React.Component {
      ; } - if (this.props.call.type === CallType.Video) { - let localVideoFeed = null; - let onHoldBackground = null; - const backgroundStyle: CSSProperties = {}; - const containerClasses = classNames({ - mx_CallView_video: true, - mx_CallView_video_hold: isOnHold, - }); - if (isOnHold) { + // This is a bit messy. I can't see a reason to have two onHold/transfer screens + if (isOnHold || transfereeCall) { + if (this.props.call.type === CallType.Video) { + const containerClasses = classNames({ + mx_CallView_content: true, + mx_CallView_video: true, + mx_CallView_video_hold: isOnHold, + }); + let onHoldBackground = null; + const backgroundStyle: CSSProperties = {}; const backgroundAvatarUrl = avatarUrlForMember( - // is it worth getting the size of the div to pass here? + // is it worth getting the size of the div to pass here? this.props.call.getOpponentMember(), 1024, 1024, 'crop', ); backgroundStyle.backgroundImage = 'url(' + backgroundAvatarUrl + ')'; onHoldBackground =
      ; - } - if (!this.state.vidMuted) { - localVideoFeed = ; - } - contentView =
      - {onHoldBackground} - - {localVideoFeed} - {holdTransferContent} - {callControls} -
      ; - } else { - const avatarSize = this.props.pipMode ? 76 : 160; + contentView = ( +
      + {onHoldBackground} + {holdTransferContent} + {callControls} +
      + ); + } else { + const classes = classNames({ + mx_CallView_content: true, + mx_CallView_voice: true, + mx_CallView_voice_hold: isOnHold, + }); + + contentView =( +
      +
      +
      + +
      +
      + {holdTransferContent} + {callControls} +
      + ); + } + } else if (this.props.call.noIncomingFeeds()) { + // Here we're reusing the css classes from voice on hold, because + // I am lazy. If this gets merged, the CallView might be subject + // to change anyway - I might take an axe to this file in order to + // try to get other things working const classes = classNames({ + mx_CallView_content: true, mx_CallView_voice: true, - mx_CallView_voice_hold: isOnHold, }); + const feeds = this.props.call.getLocalFeeds().map((feed, i) => { + // Here we check to hide local audio feeds to achieve the same UI/UX + // as before. But once again this might be subject to change + if (feed.isVideoMuted()) return; + return ( + + ); + }); + + // Saying "Connecting" here isn't really true, but the best thing + // I can come up with, but this might be subject to change as well contentView =
      + {feeds}
      { />
      - {holdTransferContent} +
      {_t("Connecting")}
      + {callControls} +
      ; + } else { + const containerClasses = classNames({ + mx_CallView_content: true, + mx_CallView_video: true, + }); + + // TODO: Later the CallView should probably be reworked to support + // any number of feeds but now we can always expect there to be two + // feeds. This is because the js-sdk ignores any new incoming streams + const feeds = this.state.feeds.map((feed, i) => { + // Here we check to hide local audio feeds to achieve the same UI/UX + // as before. But once again this might be subject to change + if (feed.isVideoMuted() && feed.isLocal()) return; + return ( + + ); + }); + + contentView =
      + {feeds} {callControls}
      ; } diff --git a/src/components/views/voip/CallViewForRoom.tsx b/src/components/views/voip/CallViewForRoom.tsx index 878b6af20f..0c785f758d 100644 --- a/src/components/views/voip/CallViewForRoom.tsx +++ b/src/components/views/voip/CallViewForRoom.tsx @@ -16,7 +16,7 @@ limitations under the License. import { CallState, MatrixCall } from 'matrix-js-sdk/src/webrtc/call'; import React from 'react'; -import CallHandler from '../../../CallHandler'; +import CallHandler, { CallHandlerEvent } from '../../../CallHandler'; import CallView from './CallView'; import dis from '../../../dispatcher/dispatcher'; import {Resizable} from "re-resizable"; @@ -54,24 +54,30 @@ export default class CallViewForRoom extends React.Component { public componentDidMount() { this.dispatcherRef = dis.register(this.onAction); + CallHandler.sharedInstance().addListener(CallHandlerEvent.CallChangeRoom, this.updateCall); } public componentWillUnmount() { dis.unregister(this.dispatcherRef); + CallHandler.sharedInstance().removeListener(CallHandlerEvent.CallChangeRoom, this.updateCall); } private onAction = (payload) => { switch (payload.action) { case 'call_state': { - const newCall = this.getCall(); - if (newCall !== this.state.call) { - this.setState({call: newCall}); - } + this.updateCall(); break; } } }; + private updateCall = () => { + const newCall = this.getCall(); + if (newCall !== this.state.call) { + this.setState({call: newCall}); + } + }; + private getCall(): MatrixCall { const call = CallHandler.sharedInstance().getCallForRoom(this.props.roomId); diff --git a/src/components/views/voip/IncomingCallBox.tsx b/src/components/views/voip/IncomingCallBox.tsx index 0ca2a196c2..2abdc0641d 100644 --- a/src/components/views/voip/IncomingCallBox.tsx +++ b/src/components/views/voip/IncomingCallBox.tsx @@ -72,7 +72,7 @@ export default class IncomingCallBox extends React.Component { e.stopPropagation(); dis.dispatch({ action: 'answer', - room_id: CallHandler.roomIdForCall(this.state.incomingCall), + room_id: CallHandler.sharedInstance().roomIdForCall(this.state.incomingCall), }); }; @@ -80,7 +80,7 @@ export default class IncomingCallBox extends React.Component { e.stopPropagation(); dis.dispatch({ action: 'reject', - room_id: CallHandler.roomIdForCall(this.state.incomingCall), + room_id: CallHandler.sharedInstance().roomIdForCall(this.state.incomingCall), }); }; @@ -91,7 +91,7 @@ export default class IncomingCallBox extends React.Component { let room = null; if (this.state.incomingCall) { - room = MatrixClientPeg.get().getRoom(CallHandler.roomIdForCall(this.state.incomingCall)); + room = MatrixClientPeg.get().getRoom(CallHandler.sharedInstance().roomIdForCall(this.state.incomingCall)); } const caller = room ? room.name : _t("Unknown caller"); diff --git a/src/components/views/voip/VideoFeed.tsx b/src/components/views/voip/VideoFeed.tsx index 2981fb6c04..d22fa055ce 100644 --- a/src/components/views/voip/VideoFeed.tsx +++ b/src/components/views/voip/VideoFeed.tsx @@ -18,52 +18,102 @@ import classnames from 'classnames'; import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call'; import React, {createRef} from 'react'; import SettingsStore from "../../../settings/SettingsStore"; +import { CallFeed, CallFeedEvent } from 'matrix-js-sdk/src/webrtc/callFeed'; +import { logger } from 'matrix-js-sdk/src/logger'; +import MemberAvatar from "../avatars/MemberAvatar" import {replaceableComponent} from "../../../utils/replaceableComponent"; -export enum VideoFeedType { - Local, - Remote, -} - interface IProps { call: MatrixCall, - type: VideoFeedType, + 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. + // 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; // a callback which is called when the video element is resized // due to a change in video metadata onResize?: (e: Event) => void, } -@replaceableComponent("views.voip.VideoFeed") -export default class VideoFeed extends React.Component { - private vid = createRef(); +interface IState { + audioMuted: boolean; + videoMuted: boolean; +} - componentDidMount() { - this.vid.current.addEventListener('resize', this.onResize); - this.setVideoElement(); +@replaceableComponent("views.voip.VideoFeed") +export default class VideoFeed extends React.Component { + private element = createRef(); + + constructor(props: IProps) { + super(props); + + this.state = { + audioMuted: this.props.feed.isAudioMuted(), + videoMuted: this.props.feed.isVideoMuted(), + }; } - componentDidUpdate(prevProps) { - if (this.props.call !== prevProps.call) { - this.setVideoElement(); - } + componentDidMount() { + this.props.feed.addListener(CallFeedEvent.NewStream, this.onNewStream); + this.playMedia(); } componentWillUnmount() { - this.vid.current.removeEventListener('resize', this.onResize); + this.props.feed.removeListener(CallFeedEvent.NewStream, this.onNewStream); + this.element.current?.removeEventListener('resize', this.onResize); + this.stopMedia(); } - private setVideoElement() { - if (this.props.type === VideoFeedType.Local) { - this.props.call.setLocalVideoElement(this.vid.current); - } else { - this.props.call.setRemoteVideoElement(this.vid.current); + private playMedia() { + const element = this.element.current; + if (!element) return; + // We play audio in AudioFeed, not here + element.muted = true; + element.srcObject = this.props.feed.stream; + element.autoplay = true; + try { + // A note on calling methods on media elements: + // We used to have queues per media element to serialise all calls on those elements. + // The reason given for this was that load() and play() were racing. However, we now + // never call load() explicitly so this seems unnecessary. However, serialising every + // operation was causing bugs where video would not resume because some play command + // had got stuck and all media operations were queued up behind it. If necessary, we + // should serialise the ones that need to be serialised but then be able to interrupt + // them with another load() which will cancel the pending one, but since we don't call + // load() explicitly, it shouldn't be a problem. - Dave + element.play() + } catch (e) { + logger.info("Failed to play media element with feed", this.props.feed, e); } } - onResize = (e) => { - if (this.props.onResize) { + private stopMedia() { + const element = this.element.current; + if (!element) return; + + element.pause(); + element.src = null; + + // As per comment in componentDidMount, setting the sink ID back to the + // default once the call is over makes setSinkId work reliably. - Dave + // Since we are not using the same element anymore, the above doesn't + // seem to be necessary - Šimon + } + + private onNewStream = () => { + this.setState({ + audioMuted: this.props.feed.isAudioMuted(), + videoMuted: this.props.feed.isVideoMuted(), + }); + this.playMedia(); + }; + + private onResize = (e) => { + if (this.props.onResize && !this.props.feed.isLocal()) { this.props.onResize(e); } }; @@ -71,14 +121,33 @@ export default class VideoFeed extends React.Component { render() { const videoClasses = { mx_VideoFeed: true, - mx_VideoFeed_local: this.props.type === VideoFeedType.Local, - mx_VideoFeed_remote: this.props.type === VideoFeedType.Remote, + mx_VideoFeed_local: this.props.feed.isLocal(), + mx_VideoFeed_remote: !this.props.feed.isLocal(), + mx_VideoFeed_voice: this.state.videoMuted, + mx_VideoFeed_video: !this.state.videoMuted, mx_VideoFeed_mirror: ( - this.props.type === VideoFeedType.Local && + this.props.feed.isLocal() && SettingsStore.getValue('VideoView.flipVideoHorizontally') ), }; - return
      diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index ed49fb6b44..6f2a4e8aee 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1458,7 +1458,6 @@ "Encrypting your message...": "Encrypting your message...", "Your message was sent": "Your message was sent", "Failed to send": "Failed to send", - "Please select the destination room for this message": "Please select the destination room for this message", "Scroll to most recent messages": "Scroll to most recent messages", "Close preview": "Close preview", "and %(count)s others...|other": "and %(count)s others...", diff --git a/src/stores/RoomViewStore.tsx b/src/stores/RoomViewStore.tsx index fe2e0a66b2..37162c8ae7 100644 --- a/src/stores/RoomViewStore.tsx +++ b/src/stores/RoomViewStore.tsx @@ -53,8 +53,6 @@ const INITIAL_STATE = { // Any error that has occurred during loading roomLoadError: null, - forwardingEvent: null, - quotingEvent: null, replyingToEvent: null, @@ -149,11 +147,6 @@ class RoomViewStore extends Store { case 'on_logged_out': this.reset(); break; - case 'forward_event': - this.setState({ - forwardingEvent: payload.event, - }); - break; case 'reply_to_event': // If currently viewed room does not match the room in which we wish to reply then change rooms // this can happen when performing a search across all rooms @@ -186,7 +179,6 @@ class RoomViewStore extends Store { roomAlias: payload.room_alias, initialEventId: payload.event_id, isInitialEventHighlighted: payload.highlighted, - forwardingEvent: null, roomLoading: false, roomLoadError: null, // should peek by default @@ -206,14 +198,6 @@ class RoomViewStore extends Store { newState.replyingToEvent = payload.replyingToEvent; } - if (this.state.forwardingEvent) { - dis.dispatch({ - action: 'send_event', - room_id: newState.roomId, - event: this.state.forwardingEvent, - }); - } - this.setState(newState); if (payload.auto_join) { @@ -419,11 +403,6 @@ class RoomViewStore extends Store { return this.state.joinError; } - // The mxEvent if one is about to be forwarded - public getForwardingEvent() { - return this.state.forwardingEvent; - } - // The mxEvent if one is currently being replied to/quoted public getQuotingEvent() { return this.state.replyingToEvent; From 44b143c8c3063be7ca2bf24e6cfdb81be9351c75 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Sat, 8 May 2021 21:17:05 -0400 Subject: [PATCH 065/999] Match requested avatar size to displayed size Reduces the blurriness of avatars in the EventTilePreview. Signed-off-by: Robin Townsend --- src/components/views/elements/EventTilePreview.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/elements/EventTilePreview.tsx b/src/components/views/elements/EventTilePreview.tsx index b15fbbed2b..95f9a97058 100644 --- a/src/components/views/elements/EventTilePreview.tsx +++ b/src/components/views/elements/EventTilePreview.tsx @@ -61,7 +61,7 @@ interface IState { message: string; } -const AVATAR_SIZE = 32; +const AVATAR_SIZE = 30; @replaceableComponent("views.elements.EventTilePreview") export default class EventTilePreview extends React.Component { From e46bc931781095447a1929938a5cb5bdbdb7de4d Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Sat, 8 May 2021 21:22:31 -0400 Subject: [PATCH 066/999] Fall back to MXID when no display name is present MemberAvatar requires a display name, or else it refuses to render. Signed-off-by: Robin Townsend --- src/components/views/elements/EventTilePreview.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/elements/EventTilePreview.tsx b/src/components/views/elements/EventTilePreview.tsx index 95f9a97058..6d2ea687de 100644 --- a/src/components/views/elements/EventTilePreview.tsx +++ b/src/components/views/elements/EventTilePreview.tsx @@ -101,7 +101,7 @@ export default class EventTilePreview extends React.Component { // Fake it more event.sender = { - name: this.props.displayName, + name: this.props.displayName || this.props.userId, userId: this.props.userId, getAvatarUrl: (..._) => { return Avatar.avatarUrlForUser( From 219c983d191d9172e3c68aa11b1765f91ac5c6cd Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Sun, 9 May 2021 10:58:44 -0400 Subject: [PATCH 067/999] Use import instead of sdk.getComponent Signed-off-by: Robin Townsend --- src/components/views/context_menus/MessageContextMenu.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/context_menus/MessageContextMenu.js b/src/components/views/context_menus/MessageContextMenu.js index e069bba975..fc2c366b07 100644 --- a/src/components/views/context_menus/MessageContextMenu.js +++ b/src/components/views/context_menus/MessageContextMenu.js @@ -31,6 +31,7 @@ import { isContentActionable } from '../../../utils/EventUtils'; import {MenuItem} from "../../structures/ContextMenu"; import {EventType} from "matrix-js-sdk/src/@types/event"; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import ForwardDialog from "../dialogs/ForwardDialog"; export function canCancel(eventStatus) { return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT; @@ -154,7 +155,6 @@ export default class MessageContextMenu extends React.Component { }; onForwardClick = () => { - const ForwardDialog = sdk.getComponent("dialogs.ForwardDialog"); Modal.createTrackedDialog('Forward Message', '', ForwardDialog, { cli: MatrixClientPeg.get(), event: this.props.mxEvent, From c96888c9cba48dfb1d318b2cfb89cd699c6a8529 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Mon, 10 May 2021 00:38:01 -0400 Subject: [PATCH 068/999] Make ForwardDialog more readable Signed-off-by: Robin Townsend --- .../views/dialogs/ForwardDialog.tsx | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/components/views/dialogs/ForwardDialog.tsx b/src/components/views/dialogs/ForwardDialog.tsx index ce38a783b3..b1f558dfef 100644 --- a/src/components/views/dialogs/ForwardDialog.tsx +++ b/src/components/views/dialogs/ForwardDialog.tsx @@ -39,6 +39,7 @@ const AVATAR_SIZE = 30; interface IProps extends IDialogProps { cli: MatrixClient; + // The event to forward event: MatrixEvent; } @@ -116,7 +117,7 @@ const ForwardDialog: React.FC = ({ cli, event, onFinished }) => { }); mockEvent.sender = { name: profileInfo.displayname || userId, - userId: userId, + userId, getAvatarUrl: (..._) => { return avatarUrlForUser( { avatarUrl: profileInfo.avatar_url }, @@ -179,28 +180,28 @@ const ForwardDialog: React.FC = ({ cli, event, onFinished }) => { { rooms.length > 0 ? (

      { _t("Rooms") }

      - { rooms.map(room => { - return + cli.sendEvent(room.roomId, event.getType(), event.getContent())} - />; - }) } + />, + ) }
      ) : undefined } { dms.length > 0 ? (

      { _t("Direct Messages") }

      - { dms.map(room => { - return + cli.sendEvent(room.roomId, event.getType(), event.getContent())} - />; - }) } + />, + ) }
      - ) : null } + ) : undefined } { rooms.length + dms.length < 1 ? { _t("No results") } From 100efb1a90c92ac6a8771787cd2564f218507550 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Mon, 10 May 2021 00:40:54 -0400 Subject: [PATCH 069/999] Fix ForwardDialog crashing when rendering reply Signed-off-by: Robin Townsend --- src/components/views/context_menus/MessageContextMenu.js | 1 + src/components/views/dialogs/ForwardDialog.tsx | 9 +++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/components/views/context_menus/MessageContextMenu.js b/src/components/views/context_menus/MessageContextMenu.js index fc2c366b07..002542bf88 100644 --- a/src/components/views/context_menus/MessageContextMenu.js +++ b/src/components/views/context_menus/MessageContextMenu.js @@ -158,6 +158,7 @@ export default class MessageContextMenu extends React.Component { Modal.createTrackedDialog('Forward Message', '', ForwardDialog, { cli: MatrixClientPeg.get(), event: this.props.mxEvent, + permalinkCreator: this.props.permalinkCreator, }, 'mx_Dialog_forwardmessage'); this.closeMenu(); }; diff --git a/src/components/views/dialogs/ForwardDialog.tsx b/src/components/views/dialogs/ForwardDialog.tsx index b1f558dfef..5cb5d68745 100644 --- a/src/components/views/dialogs/ForwardDialog.tsx +++ b/src/components/views/dialogs/ForwardDialog.tsx @@ -33,6 +33,7 @@ import RoomAvatar from "../avatars/RoomAvatar"; import AccessibleButton from "../elements/AccessibleButton"; import AutoHideScrollbar from "../../structures/AutoHideScrollbar"; import DMRoomMap from "../../../utils/DMRoomMap"; +import {RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks"; import {sortRooms} from "../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm"; const AVATAR_SIZE = 30; @@ -41,6 +42,9 @@ interface IProps extends IDialogProps { cli: MatrixClient; // The event to forward event: MatrixEvent; + // We need a permalink creator for the source room to pass through to EventTile + // in case the event is a reply (even though the user can't get at the link) + permalinkCreator: RoomPermalinkCreator; } interface IEntryProps { @@ -97,7 +101,7 @@ const Entry: React.FC = ({ room, send }) => {
      ; }; -const ForwardDialog: React.FC = ({ cli, event, onFinished }) => { +const ForwardDialog: React.FC = ({ cli, event, permalinkCreator, onFinished }) => { const userId = cli.getUserId(); const [profileInfo, setProfileInfo] = useState({}); useEffect(() => { @@ -113,7 +117,7 @@ const ForwardDialog: React.FC = ({ cli, event, onFinished }) => { age: 97, }, event_id: "$9999999999999999999999999999999999999999999", - room_id: "!999999999999999999:example.org", + room_id: event.getRoomId(), }); mockEvent.sender = { name: profileInfo.displayname || userId, @@ -165,6 +169,7 @@ const ForwardDialog: React.FC = ({ cli, event, onFinished }) => { mxEvent={mockEvent} layout={previewLayout} enableFlair={SettingsStore.getValue(UIFeature.Flair)} + permalinkCreator={permalinkCreator} />
      From eb07f1fb86de32944b8a0375beec89abdeef63d0 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Mon, 10 May 2021 00:54:00 -0400 Subject: [PATCH 070/999] Test that ForwardDialog can render replies Previously ForwardDialog was not giving its EventTile message preview the information it needed to render a ReplyThread. This was a bit tricky to fix since we were pulling a fake event out of thin air, so this ensures it doesn't regress. Signed-off-by: Robin Townsend --- .../views/dialogs/ForwardDialog-test.js | 64 ++++++++++++++----- test/test-utils.js | 1 + 2 files changed, 49 insertions(+), 16 deletions(-) diff --git a/test/components/views/dialogs/ForwardDialog-test.js b/test/components/views/dialogs/ForwardDialog-test.js index cabcbb0325..af325ab8f0 100644 --- a/test/components/views/dialogs/ForwardDialog-test.js +++ b/test/components/views/dialogs/ForwardDialog-test.js @@ -24,44 +24,51 @@ import {act} from "react-dom/test-utils"; import * as TestUtils from "../../../test-utils"; import {MatrixClientPeg} from "../../../../src/MatrixClientPeg"; import DMRoomMap from "../../../../src/utils/DMRoomMap"; +import {RoomPermalinkCreator} from "../../../../src/utils/permalinks/Permalinks"; import ForwardDialog from "../../../../src/components/views/dialogs/ForwardDialog"; configure({ adapter: new Adapter() }); describe("ForwardDialog", () => { - let client; - let wrapper; - - const message = TestUtils.mkMessage({ - room: "!111111111111111111:example.org", + const sourceRoom = "!111111111111111111:example.org"; + const defaultMessage = TestUtils.mkMessage({ + room: sourceRoom, user: "@alice:example.org", msg: "Hello world!", event: true, }); + const defaultRooms = ["a", "A", "b"].map(name => TestUtils.mkStubRoom(name, name)); - beforeEach(async () => { - TestUtils.stubClient(); - DMRoomMap.makeShared(); - client = MatrixClientPeg.get(); - client.getVisibleRooms = jest.fn().mockReturnValue( - ["a", "A", "b"].map(name => TestUtils.mkStubRoom(name, name)), - ); - client.getUserId = jest.fn().mockReturnValue("@bob:example.org"); + const mountForwardDialog = async (message = defaultMessage, rooms = defaultRooms) => { + const client = MatrixClientPeg.get(); + client.getVisibleRooms = jest.fn().mockReturnValue(rooms); + let wrapper; await act(async () => { wrapper = mount( , ); // Wait one tick for our profile data to load so the state update happens within act await new Promise(resolve => setImmediate(resolve)); }); + + return wrapper; + }; + + beforeEach(() => { + TestUtils.stubClient(); + DMRoomMap.makeShared(); + MatrixClientPeg.get().getUserId = jest.fn().mockReturnValue("@bob:example.org"); }); - it("shows a preview with us as the sender", () => { + it("shows a preview with us as the sender", async () => { + const wrapper = await mountForwardDialog(); + const previewBody = wrapper.find(".mx_EventTile_body"); expect(previewBody.text()).toBe("Hello world!"); @@ -70,7 +77,9 @@ describe("ForwardDialog", () => { expect(previewAvatar.prop("title")).toBe("@bob:example.org"); }); - it("filters the rooms", () => { + it("filters the rooms", async () => { + const wrapper = await mountForwardDialog(); + const roomsBefore = wrapper.find(".mx_ForwardList_entry"); expect(roomsBefore).toHaveLength(3); @@ -83,10 +92,12 @@ describe("ForwardDialog", () => { }); it("tracks message sending progress across multiple rooms", async () => { + const wrapper = await mountForwardDialog(); + // Make sendEvent require manual resolution so we can see the sending state let finishSend; let cancelSend; - client.sendEvent = jest.fn(() => new Promise((resolve, reject) => { + MatrixClientPeg.get().sendEvent = jest.fn(() => new Promise((resolve, reject) => { finishSend = resolve; cancelSend = reject; })); @@ -121,4 +132,25 @@ describe("ForwardDialog", () => { }); expect(secondRoom.find(".mx_AccessibleButton").text()).toBe("Sent"); }); + + it("can render replies", async () => { + const replyMessage = TestUtils.mkEvent({ + type: "m.room.message", + room: "!111111111111111111:example.org", + user: "@alice:example.org", + content: { + msgtype: "m.text", + body: "> <@bob:example.org> Hi Alice!\n\nHi Bob!", + "m.relates_to": { + "m.in_reply_to": { + event_id: "$2222222222222222222222222222222222222222222", + }, + }, + }, + event: true, + }); + + const wrapper = await mountForwardDialog(replyMessage); + expect(wrapper.find("ReplyThread")).toBeTruthy(); + }); }); diff --git a/test/test-utils.js b/test/test-utils.js index 985e9f804b..8c9c62844a 100644 --- a/test/test-utils.js +++ b/test/test-utils.js @@ -236,6 +236,7 @@ export function mkStubRoom(roomId = null, name) { getPendingEvents: () => [], getLiveTimeline: () => stubTimeline, getUnfilteredTimelineSet: () => null, + findEventById: () => null, getAccountData: () => null, hasMembershipState: () => null, getVersion: () => '1', From 35cf0e1c7efbd4f627e8fcc00cd8f8b96daf1b8e Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Mon, 10 May 2021 01:02:12 -0400 Subject: [PATCH 071/999] Find components by name rather than class in ForwardDialog test It makes things shorter and more readable! Signed-off-by: Robin Townsend --- .../views/dialogs/ForwardDialog-test.js | 32 ++++++++----------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/test/components/views/dialogs/ForwardDialog-test.js b/test/components/views/dialogs/ForwardDialog-test.js index af325ab8f0..f181157d09 100644 --- a/test/components/views/dialogs/ForwardDialog-test.js +++ b/test/components/views/dialogs/ForwardDialog-test.js @@ -80,15 +80,13 @@ describe("ForwardDialog", () => { it("filters the rooms", async () => { const wrapper = await mountForwardDialog(); - const roomsBefore = wrapper.find(".mx_ForwardList_entry"); - expect(roomsBefore).toHaveLength(3); + expect(wrapper.find("Entry")).toHaveLength(3); - const searchInput = wrapper.find(".mx_SearchBox input"); + const searchInput = wrapper.find("SearchBox input"); searchInput.instance().value = "a"; searchInput.simulate("change"); - const roomsAfter = wrapper.find(".mx_ForwardList_entry"); - expect(roomsAfter).toHaveLength(2); + expect(wrapper.find("Entry")).toHaveLength(2); }); it("tracks message sending progress across multiple rooms", async () => { @@ -102,35 +100,31 @@ describe("ForwardDialog", () => { cancelSend = reject; })); - const firstRoom = wrapper.find(".mx_ForwardList_entry").first(); - expect(firstRoom.find(".mx_AccessibleButton").text()).toBe("Send"); + const firstButton = wrapper.find("Entry AccessibleButton").first(); + expect(firstButton.text()).toBe("Send"); - act(() => { - firstRoom.find(".mx_AccessibleButton").simulate("click"); - }); - expect(firstRoom.find(".mx_AccessibleButton").text()).toBe("Sending…"); + act(() => { firstButton.simulate("click"); }); + expect(firstButton.text()).toBe("Sending…"); await act(async () => { cancelSend(); // Wait one tick for the button to realize the send failed await new Promise(resolve => setImmediate(resolve)); }); - expect(firstRoom.find(".mx_AccessibleButton").text()).toBe("Failed to send"); + expect(firstButton.text()).toBe("Failed to send"); - const secondRoom = wrapper.find(".mx_ForwardList_entry").at(1); - expect(secondRoom.find(".mx_AccessibleButton").text()).toBe("Send"); + const secondButton = wrapper.find("Entry AccessibleButton").at(1); + expect(secondButton.text()).toBe("Send"); - act(() => { - secondRoom.find(".mx_AccessibleButton").simulate("click"); - }); - expect(secondRoom.find(".mx_AccessibleButton").text()).toBe("Sending…"); + act(() => { secondButton.simulate("click"); }); + expect(secondButton.text()).toBe("Sending…"); await act(async () => { finishSend(); // Wait one tick for the button to realize the send succeeded await new Promise(resolve => setImmediate(resolve)); }); - expect(secondRoom.find(".mx_AccessibleButton").text()).toBe("Sent"); + expect(secondButton.text()).toBe("Sent"); }); it("can render replies", async () => { From 09ba74a8514fd907e758f1af24c795fa78ce8643 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Mon, 10 May 2021 01:04:25 -0400 Subject: [PATCH 072/999] Disable forward buttons for rooms without send permissions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …and add a tooltip to explain why they can't accept forwarded messages. It was chosen to disable the buttons rather than hide the entries from the list, since hiding them without explanation could cause confusion. Signed-off-by: Robin Townsend --- .../views/dialogs/ForwardDialog.tsx | 76 ++++++++++++------- 1 file changed, 47 insertions(+), 29 deletions(-) diff --git a/src/components/views/dialogs/ForwardDialog.tsx b/src/components/views/dialogs/ForwardDialog.tsx index 5cb5d68745..fd10a88d93 100644 --- a/src/components/views/dialogs/ForwardDialog.tsx +++ b/src/components/views/dialogs/ForwardDialog.tsx @@ -31,6 +31,7 @@ import EventTile from "../rooms/EventTile"; import SearchBox from "../../structures/SearchBox"; import RoomAvatar from "../avatars/RoomAvatar"; import AccessibleButton from "../elements/AccessibleButton"; +import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import AutoHideScrollbar from "../../structures/AutoHideScrollbar"; import DMRoomMap from "../../../utils/DMRoomMap"; import {RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks"; @@ -63,41 +64,58 @@ enum SendState { const Entry: React.FC = ({ room, send }) => { const [sendState, setSendState] = useState(SendState.CanSend); - let label; - let className; - if (sendState === SendState.CanSend) { - label = _t("Send"); - className = "mx_ForwardList_canSend"; - } else if (sendState === SendState.Sending) { - label = _t("Sending…"); - className = "mx_ForwardList_sending"; - } else if (sendState === SendState.Sent) { - label = _t("Sent"); - className = "mx_ForwardList_sent"; + let button; + if (room.maySendMessage()) { + let label; + let className; + if (sendState === SendState.CanSend) { + label = _t("Send"); + className = "mx_ForwardList_canSend"; + } else if (sendState === SendState.Sending) { + label = _t("Sending…"); + className = "mx_ForwardList_sending"; + } else if (sendState === SendState.Sent) { + label = _t("Sent"); + className = "mx_ForwardList_sent"; + } else { + label = _t("Failed to send"); + className = "mx_ForwardList_sendFailed"; + } + + button = + { + setSendState(SendState.Sending); + try { + await send(); + setSendState(SendState.Sent); + } catch (e) { + setSendState(SendState.Failed); + } + }} + disabled={sendState !== SendState.CanSend} + > + { label } + ; } else { - label = _t("Failed to send"); - className = "mx_ForwardList_sendFailed"; + button = + {}} + disabled={true} + title={_t("You do not have permission to post to this room")} + > + { _t("Send") } + ; } return
      { room.name } - { - setSendState(SendState.Sending); - try { - await send(); - setSendState(SendState.Sent); - } catch (e) { - setSendState(SendState.Failed); - } - }} - disabled={sendState !== SendState.CanSend} - > - { label } - + { button }
      ; }; From eb779cd3d8db279e9cf91134b1aa5bbf4326662c Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Mon, 10 May 2021 01:07:42 -0400 Subject: [PATCH 073/999] Test that forward buttons are disabled for rooms without permission Signed-off-by: Robin Townsend --- test/components/views/dialogs/ForwardDialog-test.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test/components/views/dialogs/ForwardDialog-test.js b/test/components/views/dialogs/ForwardDialog-test.js index f181157d09..706d47acdc 100644 --- a/test/components/views/dialogs/ForwardDialog-test.js +++ b/test/components/views/dialogs/ForwardDialog-test.js @@ -147,4 +147,17 @@ describe("ForwardDialog", () => { const wrapper = await mountForwardDialog(replyMessage); expect(wrapper.find("ReplyThread")).toBeTruthy(); }); + + it("disables buttons for rooms without send permissions", async () => { + const readOnlyRoom = TestUtils.mkStubRoom("a", "a"); + readOnlyRoom.maySendMessage = jest.fn().mockReturnValue(false); + const rooms = [readOnlyRoom, TestUtils.mkStubRoom("b", "b")]; + + const wrapper = await mountForwardDialog(undefined, rooms); + + const firstButton = wrapper.find("Entry AccessibleButton").first(); + expect(firstButton.prop("disabled")).toBe(true); + const secondButton = wrapper.find("Entry AccessibleButton").last(); + expect(secondButton.prop("disabled")).toBe(false); + }); }); From 5c10e1e5744e7c8b753304e4994c172f4ba493c3 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Mon, 10 May 2021 01:16:37 -0400 Subject: [PATCH 074/999] Fix lints Signed-off-by: Robin Townsend --- test/components/views/dialogs/ForwardDialog-test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/components/views/dialogs/ForwardDialog-test.js b/test/components/views/dialogs/ForwardDialog-test.js index 706d47acdc..2f062a84cc 100644 --- a/test/components/views/dialogs/ForwardDialog-test.js +++ b/test/components/views/dialogs/ForwardDialog-test.js @@ -133,8 +133,8 @@ describe("ForwardDialog", () => { room: "!111111111111111111:example.org", user: "@alice:example.org", content: { - msgtype: "m.text", - body: "> <@bob:example.org> Hi Alice!\n\nHi Bob!", + "msgtype": "m.text", + "body": "> <@bob:example.org> Hi Alice!\n\nHi Bob!", "m.relates_to": { "m.in_reply_to": { event_id: "$2222222222222222222222222222222222222222222", From bfba2b0b6f09b7d470a6274d07053d0d854f7cd0 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Mon, 10 May 2021 11:16:51 -0400 Subject: [PATCH 075/999] Push ForwardDialog scrollbar into the gutter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …to give more space between it and the buttons. Signed-off-by: Robin Townsend --- res/css/views/dialogs/_ForwardDialog.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/res/css/views/dialogs/_ForwardDialog.scss b/res/css/views/dialogs/_ForwardDialog.scss index a3aa08d04f..a447b93cae 100644 --- a/res/css/views/dialogs/_ForwardDialog.scss +++ b/res/css/views/dialogs/_ForwardDialog.scss @@ -48,6 +48,8 @@ limitations under the License. .mx_ForwardList_content { flex-grow: 1; + // Push the scrollbar into the gutter + margin-right: -6px; } .mx_ForwardList_noResults { @@ -56,6 +58,8 @@ limitations under the License. } .mx_ForwardList_section { + margin-right: 6px; + &:not(:first-child) { margin-top: 24px; } From 503301aa89e3a8378d9e48c95b37ec19ab4d5e89 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Mon, 10 May 2021 13:00:06 -0400 Subject: [PATCH 076/999] Make rooms in ForwardDialog clickable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …so that you can jump to a room easily once you've forwarded a message there. Signed-off-by: Robin Townsend --- res/css/views/dialogs/_ForwardDialog.scss | 29 ++++++---- .../views/dialogs/ForwardDialog.tsx | 53 ++++++++++++------- .../views/dialogs/ForwardDialog-test.js | 10 ++-- 3 files changed, 58 insertions(+), 34 deletions(-) diff --git a/res/css/views/dialogs/_ForwardDialog.scss b/res/css/views/dialogs/_ForwardDialog.scss index a447b93cae..30a4cfcacf 100644 --- a/res/css/views/dialogs/_ForwardDialog.scss +++ b/res/css/views/dialogs/_ForwardDialog.scss @@ -74,23 +74,30 @@ limitations under the License. .mx_ForwardList_entry { display: flex; + justify-content: space-between; margin-top: 12px; - .mx_BaseAvatar { + .mx_ForwardList_roomButton { + display: flex; margin-right: 12px; - } - - .mx_ForwardList_entry_name { - font-size: $font-15px; - line-height: 30px; flex-grow: 1; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - margin-right: 12px; + min-width: 0; + + .mx_BaseAvatar { + margin-right: 12px; + } + + .mx_ForwardList_entry_name { + font-size: $font-15px; + line-height: 30px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + margin-right: 12px; + } } - .mx_AccessibleButton { + .mx_ForwardList_sendButton { &.mx_ForwardList_sending, &.mx_ForwardList_sent { &::before { content: ''; diff --git a/src/components/views/dialogs/ForwardDialog.tsx b/src/components/views/dialogs/ForwardDialog.tsx index fd10a88d93..ed6973cd4a 100644 --- a/src/components/views/dialogs/ForwardDialog.tsx +++ b/src/components/views/dialogs/ForwardDialog.tsx @@ -21,6 +21,7 @@ import {Room} from "matrix-js-sdk/src/models/room"; import {MatrixClient} from "matrix-js-sdk/src/client"; import {_t} from "../../../languageHandler"; +import dis from "../../../dispatcher/dispatcher"; import SettingsStore from "../../../settings/SettingsStore"; import {UIFeature} from "../../../settings/UIFeature"; import {Layout} from "../../../settings/Layout"; @@ -50,8 +51,9 @@ interface IProps extends IDialogProps { interface IEntryProps { room: Room; - // Callback to forward the message to this room - send(): Promise; + event: MatrixEvent; + cli: MatrixClient; + onFinished(success: boolean): void; } enum SendState { @@ -61,9 +63,26 @@ enum SendState { Failed, } -const Entry: React.FC = ({ room, send }) => { +const Entry: React.FC = ({ room, event, cli, onFinished }) => { const [sendState, setSendState] = useState(SendState.CanSend); + const jumpToRoom = () => { + dis.dispatch({ + action: "view_room", + room_id: room.roomId, + }); + onFinished(true); + }; + const send = async () => { + setSendState(SendState.Sending); + try { + await cli.sendEvent(room.roomId, event.getType(), event.getContent()); + setSendState(SendState.Sent); + } catch (e) { + setSendState(SendState.Failed); + } + }; + let button; if (room.maySendMessage()) { let label; @@ -85,16 +104,8 @@ const Entry: React.FC = ({ room, send }) => { button = { - setSendState(SendState.Sending); - try { - await send(); - setSendState(SendState.Sent); - } catch (e) { - setSendState(SendState.Failed); - } - }} + className={`mx_ForwardList_sendButton ${className}`} + onClick={send} disabled={sendState !== SendState.CanSend} > { label } @@ -103,7 +114,7 @@ const Entry: React.FC = ({ room, send }) => { button = {}} disabled={true} title={_t("You do not have permission to post to this room")} @@ -113,8 +124,10 @@ const Entry: React.FC = ({ room, send }) => { } return
      - - { room.name } + + + { room.name } + { button }
      ; }; @@ -207,7 +220,9 @@ const ForwardDialog: React.FC = ({ cli, event, permalinkCreator, onFinis cli.sendEvent(room.roomId, event.getType(), event.getContent())} + event={event} + cli={cli} + onFinished={onFinished} />, ) }
      @@ -220,7 +235,9 @@ const ForwardDialog: React.FC = ({ cli, event, permalinkCreator, onFinis cli.sendEvent(room.roomId, event.getType(), event.getContent())} + event={event} + cli={cli} + onFinished={onFinished} />, ) }
      diff --git a/test/components/views/dialogs/ForwardDialog-test.js b/test/components/views/dialogs/ForwardDialog-test.js index 2f062a84cc..331ee9d131 100644 --- a/test/components/views/dialogs/ForwardDialog-test.js +++ b/test/components/views/dialogs/ForwardDialog-test.js @@ -100,7 +100,7 @@ describe("ForwardDialog", () => { cancelSend = reject; })); - const firstButton = wrapper.find("Entry AccessibleButton").first(); + const firstButton = wrapper.find("AccessibleButton.mx_ForwardList_sendButton").first(); expect(firstButton.text()).toBe("Send"); act(() => { firstButton.simulate("click"); }); @@ -113,8 +113,8 @@ describe("ForwardDialog", () => { }); expect(firstButton.text()).toBe("Failed to send"); - const secondButton = wrapper.find("Entry AccessibleButton").at(1); - expect(secondButton.text()).toBe("Send"); + const secondButton = wrapper.find("AccessibleButton.mx_ForwardList_sendButton").at(1); + expect(secondButton.render().text()).toBe("Send"); act(() => { secondButton.simulate("click"); }); expect(secondButton.text()).toBe("Sending…"); @@ -155,9 +155,9 @@ describe("ForwardDialog", () => { const wrapper = await mountForwardDialog(undefined, rooms); - const firstButton = wrapper.find("Entry AccessibleButton").first(); + const firstButton = wrapper.find("AccessibleButton.mx_ForwardList_sendButton").first(); expect(firstButton.prop("disabled")).toBe(true); - const secondButton = wrapper.find("Entry AccessibleButton").last(); + const secondButton = wrapper.find("AccessibleButton.mx_ForwardList_sendButton").last(); expect(secondButton.prop("disabled")).toBe(false); }); }); From 7efbd2d930cc93dffdabe10f84bc320f58cf23c5 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Mon, 10 May 2021 13:57:47 -0400 Subject: [PATCH 077/999] Hide unencrypted badge from ForwardDialog preview Signed-off-by: Robin Townsend --- res/css/views/dialogs/_ForwardDialog.scss | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/res/css/views/dialogs/_ForwardDialog.scss b/res/css/views/dialogs/_ForwardDialog.scss index 30a4cfcacf..9bd0bd2879 100644 --- a/res/css/views/dialogs/_ForwardDialog.scss +++ b/res/css/views/dialogs/_ForwardDialog.scss @@ -35,6 +35,12 @@ limitations under the License. .mx_EventTile_msgOption { display: none; } + + // When forwarding messages from encrypted rooms, EventTile will complain + // that our preview is unencrypted, which doesn't actually matter + .mx_EventTile_e2eIcon_unencrypted { + display: none; + } } .mx_ForwardList { From d6a25d493abd23d24ba2225f500762cb36b12c17 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Fri, 14 May 2021 10:11:59 +0100 Subject: [PATCH 078/999] Create performance monitoring abstraction --- src/components/structures/MatrixChat.tsx | 46 +++------ src/performance/entry-names.ts | 57 +++++++++++ src/performance/index.ts | 120 +++++++++++++++++++++++ 3 files changed, 192 insertions(+), 31 deletions(-) create mode 100644 src/performance/entry-names.ts create mode 100644 src/performance/index.ts diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 288acc108a..64d205c89c 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -86,6 +86,8 @@ import {RoomUpdateCause} from "../../stores/room-list/models"; import defaultDispatcher from "../../dispatcher/dispatcher"; import SecurityCustomisations from "../../customisations/Security"; +import PerformanceMonitor, { PerformanceEntryNames } from "../../performance"; + /** constants for MatrixChat.state.view */ export enum Views { // a special initial state which is only used at startup, while we are @@ -484,42 +486,20 @@ export default class MatrixChat extends React.PureComponent { } startPageChangeTimer() { - // Tor doesn't support performance - if (!performance || !performance.mark) return null; - - // This shouldn't happen because UNSAFE_componentWillUpdate and componentDidUpdate - // are used. - if (this.pageChanging) { - console.warn('MatrixChat.startPageChangeTimer: timer already started'); - return; - } - this.pageChanging = true; - performance.mark('element_MatrixChat_page_change_start'); + PerformanceMonitor.start(PerformanceEntryNames.SWITCH_ROOM); } stopPageChangeTimer() { - // Tor doesn't support performance - if (!performance || !performance.mark) return null; + PerformanceMonitor.stop(PerformanceEntryNames.SWITCH_ROOM); - if (!this.pageChanging) { - console.warn('MatrixChat.stopPageChangeTimer: timer not started'); - return; - } - this.pageChanging = false; - performance.mark('element_MatrixChat_page_change_stop'); - performance.measure( - 'element_MatrixChat_page_change_delta', - 'element_MatrixChat_page_change_start', - 'element_MatrixChat_page_change_stop', - ); - performance.clearMarks('element_MatrixChat_page_change_start'); - performance.clearMarks('element_MatrixChat_page_change_stop'); - const measurement = performance.getEntriesByName('element_MatrixChat_page_change_delta').pop(); + const entries = PerformanceMonitor.getEntries({ + name: PerformanceEntryNames.SWITCH_ROOM, + }); + const measurement = entries.pop(); - // In practice, sometimes the entries list is empty, so we get no measurement - if (!measurement) return null; - - return measurement.duration; + return measurement + ? measurement.duration + : null; } shouldTrackPageChange(prevState: IState, state: IState) { @@ -1632,11 +1612,13 @@ export default class MatrixChat extends React.PureComponent { action: 'start_registration', params: params, }); + Performance.start(PerformanceEntryNames.REGISTER); } else if (screen === 'login') { dis.dispatch({ action: 'start_login', params: params, }); + Performance.start(PerformanceEntryNames.LOGIN); } else if (screen === 'forgot_password') { dis.dispatch({ action: 'start_password_recovery', @@ -1876,6 +1858,7 @@ export default class MatrixChat extends React.PureComponent { // returns a promise which resolves to the new MatrixClient onRegistered(credentials: IMatrixClientCreds) { + Performance.stop(PerformanceEntryNames.REGISTER); return Lifecycle.setLoggedIn(credentials); } @@ -1965,6 +1948,7 @@ export default class MatrixChat extends React.PureComponent { // Create and start the client await Lifecycle.setLoggedIn(credentials); await this.postLoginSetup(); + Performance.stop(PerformanceEntryNames.LOGIN); }; // complete security / e2e setup has finished diff --git a/src/performance/entry-names.ts b/src/performance/entry-names.ts new file mode 100644 index 0000000000..effd9506f6 --- /dev/null +++ b/src/performance/entry-names.ts @@ -0,0 +1,57 @@ +/* +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. +*/ + +export enum PerformanceEntryNames { + + /** + * Application wide + */ + + APP_STARTUP = "mx_AppStartup", + PAGE_CHANGE = "mx_PageChange", + + /** + * Events + */ + + RESEND_EVENT = "mx_ResendEvent", + SEND_E2EE_EVENT = "mx_SendE2EEEvent", + SEND_ATTACHMENT = "mx_SendAttachment", + + /** + * Rooms + */ + + SWITCH_ROOM = "mx_SwithRoom", + JUMP_TO_ROOM = "mx_JumpToRoom", + JOIN_ROOM = "mx_JoinRoom", + CREATE_DM = "mx_CreateDM", + PEEK_ROOM = "mx_PeekRoom", + + /** + * User + */ + + VERIFY_E2EE_USER = "mx_VerifyE2EEUser", + LOGIN = "mx_Login", + REGISTER = "mx_Register", + + /** + * VoIP + */ + + SETUP_VOIP_CALL = "mx_SetupVoIPCall", +} diff --git a/src/performance/index.ts b/src/performance/index.ts new file mode 100644 index 0000000000..4379ba77e3 --- /dev/null +++ b/src/performance/index.ts @@ -0,0 +1,120 @@ +/* +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 { string } from "prop-types"; +import { PerformanceEntryNames } from "./entry-names"; + +const START_PREFIX = "start:"; +const STOP_PREFIX = "stop:"; + +export { + PerformanceEntryNames, +} + +interface GetEntriesOptions { + name?: string, + type?: string, +} + +export default class PerformanceMonitor { + /** + * Starts a performance recording + * @param name Name of the recording + * @param id Specify an identifier appended to the measurement name + * @returns {void} + */ + static start(name: string, id?: string): void { + if (!supportsPerformanceApi()) { + return; + } + const key = buildKey(name, id); + + if (!performance.getEntriesByName(key).length) { + console.warn(`Recording already started for: ${name}`); + return; + } + + performance.mark(START_PREFIX + key); + } + + /** + * Stops a performance recording and stores delta duration + * with the start marker + * @param name Name of the recording + * @param id Specify an identifier appended to the measurement name + * @returns {void} + */ + static stop(name: string, id?: string): void { + if (!supportsPerformanceApi()) { + return; + } + const key = buildKey(name, id); + if (!performance.getEntriesByName(START_PREFIX + key).length) { + console.warn(`No recording started for: ${name}`); + return; + } + + performance.mark(STOP_PREFIX + key); + performance.measure( + key, + START_PREFIX + key, + STOP_PREFIX + key, + ); + + this.clear(name, id); + } + + static clear(name: string, id?: string): void { + if (!supportsPerformanceApi()) { + return; + } + const key = buildKey(name, id); + performance.clearMarks(START_PREFIX + key); + performance.clearMarks(STOP_PREFIX + key); + } + + static getEntries({ name, type }: GetEntriesOptions = {}): PerformanceEntry[] { + if (!supportsPerformanceApi()) { + return; + } + + if (!name && !type) { + return performance.getEntries(); + } else if (!name) { + return performance.getEntriesByType(type); + } else { + return performance.getEntriesByName(name, type); + } + } +} + +/** + * Tor browser does not support the Performance API + * @returns {boolean} true if the Performance API is supported + */ +function supportsPerformanceApi(): boolean { + return performance !== undefined && performance.mark !== undefined; +} + +/** + * Internal utility to ensure consistent name for the recording + * @param name Name of the recording + * @param id Specify an identifier appended to the measurement name + * @returns {string} a compound of the name and identifier if present + */ +function buildKey(name: string, id?: string): string { + return `${name}${id ? `:${id}` : ''}`; +} From 6804a26e74267925637296a94ea9e2c927f00aa0 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Fri, 14 May 2021 11:04:58 +0100 Subject: [PATCH 079/999] Add performance data collection mechanism --- src/components/structures/MatrixChat.tsx | 14 ++--- src/performance/index.ts | 65 +++++++++++++++++++----- 2 files changed, 59 insertions(+), 20 deletions(-) diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 64d205c89c..81381c56d3 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -486,14 +486,14 @@ export default class MatrixChat extends React.PureComponent { } startPageChangeTimer() { - PerformanceMonitor.start(PerformanceEntryNames.SWITCH_ROOM); + PerformanceMonitor.start(PerformanceEntryNames.PAGE_CHANGE); } stopPageChangeTimer() { - PerformanceMonitor.stop(PerformanceEntryNames.SWITCH_ROOM); + PerformanceMonitor.stop(PerformanceEntryNames.PAGE_CHANGE); const entries = PerformanceMonitor.getEntries({ - name: PerformanceEntryNames.SWITCH_ROOM, + name: PerformanceEntryNames.PAGE_CHANGE, }); const measurement = entries.pop(); @@ -1612,13 +1612,13 @@ export default class MatrixChat extends React.PureComponent { action: 'start_registration', params: params, }); - Performance.start(PerformanceEntryNames.REGISTER); + PerformanceMonitor.start(PerformanceEntryNames.REGISTER); } else if (screen === 'login') { dis.dispatch({ action: 'start_login', params: params, }); - Performance.start(PerformanceEntryNames.LOGIN); + PerformanceMonitor.start(PerformanceEntryNames.LOGIN); } else if (screen === 'forgot_password') { dis.dispatch({ action: 'start_password_recovery', @@ -1858,7 +1858,7 @@ export default class MatrixChat extends React.PureComponent { // returns a promise which resolves to the new MatrixClient onRegistered(credentials: IMatrixClientCreds) { - Performance.stop(PerformanceEntryNames.REGISTER); + PerformanceMonitor.stop(PerformanceEntryNames.REGISTER); return Lifecycle.setLoggedIn(credentials); } @@ -1948,7 +1948,7 @@ export default class MatrixChat extends React.PureComponent { // Create and start the client await Lifecycle.setLoggedIn(credentials); await this.postLoginSetup(); - Performance.stop(PerformanceEntryNames.LOGIN); + PerformanceMonitor.stop(PerformanceEntryNames.LOGIN); }; // complete security / e2e setup has finished diff --git a/src/performance/index.ts b/src/performance/index.ts index 4379ba77e3..3d903537a6 100644 --- a/src/performance/index.ts +++ b/src/performance/index.ts @@ -14,7 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { string } from "prop-types"; import { PerformanceEntryNames } from "./entry-names"; const START_PREFIX = "start:"; @@ -29,6 +28,16 @@ interface GetEntriesOptions { type?: string, } +type PerformanceCallbackFunction = (entry: PerformanceEntry) => void; + +interface PerformanceDataListener { + entryTypes?: string[], + callback: PerformanceCallbackFunction +} + +let listeners: PerformanceDataListener[] = []; +const entries: PerformanceEntry[] = []; + export default class PerformanceMonitor { /** * Starts a performance recording @@ -42,7 +51,7 @@ export default class PerformanceMonitor { } const key = buildKey(name, id); - if (!performance.getEntriesByName(key).length) { + if (performance.getEntriesByName(key).length > 0) { console.warn(`Recording already started for: ${name}`); return; } @@ -57,12 +66,12 @@ export default class PerformanceMonitor { * @param id Specify an identifier appended to the measurement name * @returns {void} */ - static stop(name: string, id?: string): void { + static stop(name: string, id?: string): PerformanceEntry { if (!supportsPerformanceApi()) { return; } const key = buildKey(name, id); - if (!performance.getEntriesByName(START_PREFIX + key).length) { + if (performance.getEntriesByName(START_PREFIX + key).length === 0) { console.warn(`No recording started for: ${name}`); return; } @@ -75,6 +84,17 @@ export default class PerformanceMonitor { ); this.clear(name, id); + + const measurement = performance.getEntriesByName(key).pop(); + + // Keeping a reference to all PerformanceEntry created + // by this abstraction for historical events collection + // when adding a data callback + entries.push(measurement); + + listeners.forEach(listener => emitPerformanceData(listener, measurement)); + + return measurement; } static clear(name: string, id?: string): void { @@ -87,18 +107,37 @@ export default class PerformanceMonitor { } static getEntries({ name, type }: GetEntriesOptions = {}): PerformanceEntry[] { - if (!supportsPerformanceApi()) { - return; - } + return entries.filter(entry => { + const satisfiesName = !name || entry.name === name; + const satisfiedType = !type || entry.entryType === type; + return satisfiesName && satisfiedType; + }); + } - if (!name && !type) { - return performance.getEntries(); - } else if (!name) { - return performance.getEntriesByType(type); - } else { - return performance.getEntriesByName(name, type); + static addPerformanceDataCallback(listener: PerformanceDataListener, buffer = false) { + listeners.push(listener); + + if (buffer) { + entries.forEach(entry => emitPerformanceData(listener, entry)); } } + + static removePerformanceDataCallback(callback?: PerformanceCallbackFunction) { + if (!callback) { + listeners = []; + } else { + listeners.splice( + listeners.findIndex(listener => listener.callback === callback), + 1, + ); + } + } +} + +function emitPerformanceData(listener, entry): void { + if (!listener.entryTypes || listener.entryTypes.includes(entry.entryType)) { + listener.callback(entry) + } } /** From 89832eff9ef9dbe9cf3896d541797e294dd1e840 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Fri, 14 May 2021 12:23:23 +0100 Subject: [PATCH 080/999] Add data collection mechanism in end to end test suite --- src/@types/global.d.ts | 3 +++ src/components/structures/MatrixChat.tsx | 2 +- src/performance/index.ts | 25 +++++++++++++++--------- test/end-to-end-tests/.gitignore | 1 + test/end-to-end-tests/src/session.js | 2 +- test/end-to-end-tests/start.js | 22 +++++++++++++++++++-- 6 files changed, 42 insertions(+), 13 deletions(-) diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index f04a2ff237..dec8559320 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -42,6 +42,7 @@ import {SpaceStoreClass} from "../stores/SpaceStore"; import TypingStore from "../stores/TypingStore"; import { EventIndexPeg } from "../indexing/EventIndexPeg"; import {VoiceRecordingStore} from "../stores/VoiceRecordingStore"; +import PerformanceMonitor, { PerformanceEntryNames } from "../performance"; declare global { interface Window { @@ -79,6 +80,8 @@ declare global { mxVoiceRecordingStore: VoiceRecordingStore; mxTypingStore: TypingStore; mxEventIndexPeg: EventIndexPeg; + mxPerformanceMonitor: PerformanceMonitor; + mxPerformanceEntryNames: PerformanceEntryNames; } interface Document { diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 81381c56d3..5a275b9a99 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -1858,7 +1858,6 @@ export default class MatrixChat extends React.PureComponent { // returns a promise which resolves to the new MatrixClient onRegistered(credentials: IMatrixClientCreds) { - PerformanceMonitor.stop(PerformanceEntryNames.REGISTER); return Lifecycle.setLoggedIn(credentials); } @@ -1949,6 +1948,7 @@ export default class MatrixChat extends React.PureComponent { await Lifecycle.setLoggedIn(credentials); await this.postLoginSetup(); PerformanceMonitor.stop(PerformanceEntryNames.LOGIN); + PerformanceMonitor.stop(PerformanceEntryNames.REGISTER); }; // complete security / e2e setup has finished diff --git a/src/performance/index.ts b/src/performance/index.ts index 3d903537a6..bbe8a207fe 100644 --- a/src/performance/index.ts +++ b/src/performance/index.ts @@ -28,10 +28,10 @@ interface GetEntriesOptions { type?: string, } -type PerformanceCallbackFunction = (entry: PerformanceEntry) => void; +type PerformanceCallbackFunction = (entry: PerformanceEntry[]) => void; interface PerformanceDataListener { - entryTypes?: string[], + entryNames?: string[], callback: PerformanceCallbackFunction } @@ -92,7 +92,11 @@ export default class PerformanceMonitor { // when adding a data callback entries.push(measurement); - listeners.forEach(listener => emitPerformanceData(listener, measurement)); + listeners.forEach(listener => { + if (shouldEmit(listener, measurement)) { + listener.callback([measurement]) + } + }); return measurement; } @@ -116,9 +120,11 @@ export default class PerformanceMonitor { static addPerformanceDataCallback(listener: PerformanceDataListener, buffer = false) { listeners.push(listener); - if (buffer) { - entries.forEach(entry => emitPerformanceData(listener, entry)); + const toEmit = entries.filter(entry => shouldEmit(listener, entry)); + if (toEmit.length > 0) { + listener.callback(toEmit); + } } } @@ -134,10 +140,8 @@ export default class PerformanceMonitor { } } -function emitPerformanceData(listener, entry): void { - if (!listener.entryTypes || listener.entryTypes.includes(entry.entryType)) { - listener.callback(entry) - } +function shouldEmit(listener: PerformanceDataListener, entry: PerformanceEntry): boolean { + return !listener.entryNames || listener.entryNames.includes(entry.name); } /** @@ -157,3 +161,6 @@ function supportsPerformanceApi(): boolean { function buildKey(name: string, id?: string): string { return `${name}${id ? `:${id}` : ''}`; } + +window.mxPerformanceMonitor = PerformanceMonitor; +window.mxPerformanceEntryNames = PerformanceEntryNames; diff --git a/test/end-to-end-tests/.gitignore b/test/end-to-end-tests/.gitignore index 61f9012393..9180d32e90 100644 --- a/test/end-to-end-tests/.gitignore +++ b/test/end-to-end-tests/.gitignore @@ -1,3 +1,4 @@ node_modules *.png element/env +performance-entries.json diff --git a/test/end-to-end-tests/src/session.js b/test/end-to-end-tests/src/session.js index 4c611ef877..6c68929a0b 100644 --- a/test/end-to-end-tests/src/session.js +++ b/test/end-to-end-tests/src/session.js @@ -208,7 +208,7 @@ module.exports = class ElementSession { this.log.done(); } - close() { + async close() { return this.browser.close(); } diff --git a/test/end-to-end-tests/start.js b/test/end-to-end-tests/start.js index 234d60da9f..ac06dcd989 100644 --- a/test/end-to-end-tests/start.js +++ b/test/end-to-end-tests/start.js @@ -22,7 +22,7 @@ const fs = require("fs"); const program = require('commander'); program .option('--no-logs', "don't output logs, document html on error", false) - .option('--app-url [url]', "url to test", "http://localhost:5000") + .option('--app-url [url]', "url to test", "http://localhost:8080") .option('--windowed', "dont run tests headless", false) .option('--slow-mo', "type at a human speed", false) .option('--dev-tools', "open chrome devtools in browser window", false) @@ -79,8 +79,26 @@ async function runTests() { await new Promise((resolve) => setTimeout(resolve, 5 * 60 * 1000)); } - await Promise.all(sessions.map((session) => session.close())); + const performanceEntries = {}; + await Promise.all(sessions.map(async (session) => { + // Collecting all performance monitoring data before closing the session + const measurements = await session.page.evaluate(() => { + let measurements = []; + window.mxPerformanceMonitor.addPerformanceDataCallback({ + entryNames: [ + window.mxPerformanceEntryNames.REGISTER, + ], + callback: (events) => { + measurements = JSON.stringify(events); + }, + }, true); + return measurements; + }); + performanceEntries[session.username] = JSON.parse(measurements); + return session.close(); + })); + fs.writeFileSync(`performance-entries.json`, JSON.stringify(performanceEntries)); if (failure) { process.exit(-1); } else { From cbf645785772daa5f49ea05cb9ee1f07cedebafc Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Fri, 14 May 2021 12:49:32 +0100 Subject: [PATCH 081/999] Revert app url to use the default that CI relies on --- test/end-to-end-tests/start.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/end-to-end-tests/start.js b/test/end-to-end-tests/start.js index ac06dcd989..f29b485c84 100644 --- a/test/end-to-end-tests/start.js +++ b/test/end-to-end-tests/start.js @@ -22,7 +22,7 @@ const fs = require("fs"); const program = require('commander'); program .option('--no-logs', "don't output logs, document html on error", false) - .option('--app-url [url]', "url to test", "http://localhost:8080") + .option('--app-url [url]', "url to test", "http://localhost:5000") .option('--windowed', "dont run tests headless", false) .option('--slow-mo', "type at a human speed", false) .option('--dev-tools', "open chrome devtools in browser window", false) From e798b36f1db8ea73c81fcc1626e1af31014c0800 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Sun, 16 May 2021 08:39:22 -0400 Subject: [PATCH 082/999] Decorate forward dialog room avatars Signed-off-by: Robin Townsend --- res/css/views/dialogs/_ForwardDialog.scss | 2 +- src/components/views/dialogs/ForwardDialog.tsx | 4 ++-- test/test-utils.js | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/res/css/views/dialogs/_ForwardDialog.scss b/res/css/views/dialogs/_ForwardDialog.scss index 9bd0bd2879..2a72ea4ffb 100644 --- a/res/css/views/dialogs/_ForwardDialog.scss +++ b/res/css/views/dialogs/_ForwardDialog.scss @@ -89,7 +89,7 @@ limitations under the License. flex-grow: 1; min-width: 0; - .mx_BaseAvatar { + .mx_DecoratedRoomAvatar { margin-right: 12px; } diff --git a/src/components/views/dialogs/ForwardDialog.tsx b/src/components/views/dialogs/ForwardDialog.tsx index ed6973cd4a..1ef4841851 100644 --- a/src/components/views/dialogs/ForwardDialog.tsx +++ b/src/components/views/dialogs/ForwardDialog.tsx @@ -30,7 +30,7 @@ import BaseDialog from "./BaseDialog"; import {avatarUrlForUser} from "../../../Avatar"; import EventTile from "../rooms/EventTile"; import SearchBox from "../../structures/SearchBox"; -import RoomAvatar from "../avatars/RoomAvatar"; +import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar"; import AccessibleButton from "../elements/AccessibleButton"; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import AutoHideScrollbar from "../../structures/AutoHideScrollbar"; @@ -125,7 +125,7 @@ const Entry: React.FC = ({ room, event, cli, onFinished }) => { return
      - + { room.name } { button } diff --git a/test/test-utils.js b/test/test-utils.js index 8c9c62844a..a2ccf84728 100644 --- a/test/test-utils.js +++ b/test/test-utils.js @@ -253,6 +253,7 @@ export function mkStubRoom(roomId = null, name) { tags: {}, setBlacklistUnverifiedDevices: jest.fn(), on: jest.fn(), + off: jest.fn(), removeListener: jest.fn(), getDMInviter: jest.fn(), name, From 781c0dca68d293c67bfc2f125d1e08fbefaf5b06 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 17 May 2021 09:30:53 +0100 Subject: [PATCH 083/999] Refactor performance monitor to use instance pattern --- src/@types/global.d.ts | 2 +- src/components/structures/MatrixChat.tsx | 16 +-- src/performance/index.ts | 130 +++++++++++++---------- 3 files changed, 81 insertions(+), 67 deletions(-) diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index dec8559320..fb3b92e45a 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -81,7 +81,7 @@ declare global { mxTypingStore: TypingStore; mxEventIndexPeg: EventIndexPeg; mxPerformanceMonitor: PerformanceMonitor; - mxPerformanceEntryNames: PerformanceEntryNames; + mxPerformanceEntryNames: any; } interface Document { diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 5a275b9a99..4c7fca2fec 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -486,13 +486,15 @@ export default class MatrixChat extends React.PureComponent { } startPageChangeTimer() { - PerformanceMonitor.start(PerformanceEntryNames.PAGE_CHANGE); + PerformanceMonitor.instance.start(PerformanceEntryNames.PAGE_CHANGE); } stopPageChangeTimer() { - PerformanceMonitor.stop(PerformanceEntryNames.PAGE_CHANGE); + const perfMonitor = PerformanceMonitor.instance; - const entries = PerformanceMonitor.getEntries({ + perfMonitor.stop(PerformanceEntryNames.PAGE_CHANGE); + + const entries = perfMonitor.getEntries({ name: PerformanceEntryNames.PAGE_CHANGE, }); const measurement = entries.pop(); @@ -1612,13 +1614,13 @@ export default class MatrixChat extends React.PureComponent { action: 'start_registration', params: params, }); - PerformanceMonitor.start(PerformanceEntryNames.REGISTER); + PerformanceMonitor.instance.start(PerformanceEntryNames.REGISTER); } else if (screen === 'login') { dis.dispatch({ action: 'start_login', params: params, }); - PerformanceMonitor.start(PerformanceEntryNames.LOGIN); + PerformanceMonitor.instance.start(PerformanceEntryNames.LOGIN); } else if (screen === 'forgot_password') { dis.dispatch({ action: 'start_password_recovery', @@ -1947,8 +1949,8 @@ export default class MatrixChat extends React.PureComponent { // Create and start the client await Lifecycle.setLoggedIn(credentials); await this.postLoginSetup(); - PerformanceMonitor.stop(PerformanceEntryNames.LOGIN); - PerformanceMonitor.stop(PerformanceEntryNames.REGISTER); + PerformanceMonitor.instance.stop(PerformanceEntryNames.LOGIN); + PerformanceMonitor.instance.stop(PerformanceEntryNames.REGISTER); }; // complete security / e2e setup has finished diff --git a/src/performance/index.ts b/src/performance/index.ts index bbe8a207fe..13ad0a55bb 100644 --- a/src/performance/index.ts +++ b/src/performance/index.ts @@ -16,13 +16,6 @@ limitations under the License. import { PerformanceEntryNames } from "./entry-names"; -const START_PREFIX = "start:"; -const STOP_PREFIX = "stop:"; - -export { - PerformanceEntryNames, -} - interface GetEntriesOptions { name?: string, type?: string, @@ -35,28 +28,40 @@ interface PerformanceDataListener { callback: PerformanceCallbackFunction } -let listeners: PerformanceDataListener[] = []; -const entries: PerformanceEntry[] = []; - export default class PerformanceMonitor { + static _instance: PerformanceMonitor; + + private START_PREFIX = "start:" + private STOP_PREFIX = "stop:" + + private listeners: PerformanceDataListener[] = [] + private entries: PerformanceEntry[] = [] + + public static get instance(): PerformanceMonitor { + if (!PerformanceMonitor._instance) { + PerformanceMonitor._instance = new PerformanceMonitor(); + } + return PerformanceMonitor._instance; + } + /** * Starts a performance recording * @param name Name of the recording * @param id Specify an identifier appended to the measurement name * @returns {void} */ - static start(name: string, id?: string): void { - if (!supportsPerformanceApi()) { + start(name: string, id?: string): void { + if (!this.supportsPerformanceApi()) { return; } - const key = buildKey(name, id); + const key = this.buildKey(name, id); if (performance.getEntriesByName(key).length > 0) { console.warn(`Recording already started for: ${name}`); return; } - performance.mark(START_PREFIX + key); + performance.mark(this.START_PREFIX + key); } /** @@ -66,21 +71,21 @@ export default class PerformanceMonitor { * @param id Specify an identifier appended to the measurement name * @returns {void} */ - static stop(name: string, id?: string): PerformanceEntry { - if (!supportsPerformanceApi()) { + stop(name: string, id?: string): PerformanceEntry { + if (!this.supportsPerformanceApi()) { return; } - const key = buildKey(name, id); - if (performance.getEntriesByName(START_PREFIX + key).length === 0) { + const key = this.buildKey(name, id); + if (performance.getEntriesByName(this.START_PREFIX + key).length === 0) { console.warn(`No recording started for: ${name}`); return; } - performance.mark(STOP_PREFIX + key); + performance.mark(this.STOP_PREFIX + key); performance.measure( key, - START_PREFIX + key, - STOP_PREFIX + key, + this.START_PREFIX + key, + this.STOP_PREFIX + key, ); this.clear(name, id); @@ -90,10 +95,10 @@ export default class PerformanceMonitor { // Keeping a reference to all PerformanceEntry created // by this abstraction for historical events collection // when adding a data callback - entries.push(measurement); + this.entries.push(measurement); - listeners.forEach(listener => { - if (shouldEmit(listener, measurement)) { + this.listeners.forEach(listener => { + if (this.shouldEmit(listener, measurement)) { listener.callback([measurement]) } }); @@ -101,66 +106,73 @@ export default class PerformanceMonitor { return measurement; } - static clear(name: string, id?: string): void { - if (!supportsPerformanceApi()) { + clear(name: string, id?: string): void { + if (!this.supportsPerformanceApi()) { return; } - const key = buildKey(name, id); - performance.clearMarks(START_PREFIX + key); - performance.clearMarks(STOP_PREFIX + key); + const key = this.buildKey(name, id); + performance.clearMarks(this.START_PREFIX + key); + performance.clearMarks(this.STOP_PREFIX + key); } - static getEntries({ name, type }: GetEntriesOptions = {}): PerformanceEntry[] { - return entries.filter(entry => { + getEntries({ name, type }: GetEntriesOptions = {}): PerformanceEntry[] { + return this.entries.filter(entry => { const satisfiesName = !name || entry.name === name; const satisfiedType = !type || entry.entryType === type; return satisfiesName && satisfiedType; }); } - static addPerformanceDataCallback(listener: PerformanceDataListener, buffer = false) { - listeners.push(listener); + addPerformanceDataCallback(listener: PerformanceDataListener, buffer = false) { + this.listeners.push(listener); if (buffer) { - const toEmit = entries.filter(entry => shouldEmit(listener, entry)); + const toEmit = this.entries.filter(entry => this.shouldEmit(listener, entry)); if (toEmit.length > 0) { listener.callback(toEmit); } } } - static removePerformanceDataCallback(callback?: PerformanceCallbackFunction) { + removePerformanceDataCallback(callback?: PerformanceCallbackFunction) { if (!callback) { - listeners = []; + this.listeners = []; } else { - listeners.splice( - listeners.findIndex(listener => listener.callback === callback), + this.listeners.splice( + this.listeners.findIndex(listener => listener.callback === callback), 1, ); } } + + /** + * Tor browser does not support the Performance API + * @returns {boolean} true if the Performance API is supported + */ + private supportsPerformanceApi(): boolean { + return performance !== undefined && performance.mark !== undefined; + } + + private shouldEmit(listener: PerformanceDataListener, entry: PerformanceEntry): boolean { + return !listener.entryNames || listener.entryNames.includes(entry.name); + } + + /** + * Internal utility to ensure consistent name for the recording + * @param name Name of the recording + * @param id Specify an identifier appended to the measurement name + * @returns {string} a compound of the name and identifier if present + */ + private buildKey(name: string, id?: string): string { + return `${name}${id ? `:${id}` : ''}`; + } } -function shouldEmit(listener: PerformanceDataListener, entry: PerformanceEntry): boolean { - return !listener.entryNames || listener.entryNames.includes(entry.name); + +// Convienience exports +export { + PerformanceEntryNames, } -/** - * Tor browser does not support the Performance API - * @returns {boolean} true if the Performance API is supported - */ -function supportsPerformanceApi(): boolean { - return performance !== undefined && performance.mark !== undefined; -} - -/** - * Internal utility to ensure consistent name for the recording - * @param name Name of the recording - * @param id Specify an identifier appended to the measurement name - * @returns {string} a compound of the name and identifier if present - */ -function buildKey(name: string, id?: string): string { - return `${name}${id ? `:${id}` : ''}`; -} - -window.mxPerformanceMonitor = PerformanceMonitor; +// Exposing those to the window object to bridge them from tests +window.mxPerformanceMonitor = PerformanceMonitor.instance; window.mxPerformanceEntryNames = PerformanceEntryNames; From f3bebdbc8733f07556f79609e8afdc58778738f4 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 17 May 2021 09:44:36 +0100 Subject: [PATCH 084/999] remove unused import --- src/@types/global.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index fb3b92e45a..63966d96fa 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -42,7 +42,7 @@ import {SpaceStoreClass} from "../stores/SpaceStore"; import TypingStore from "../stores/TypingStore"; import { EventIndexPeg } from "../indexing/EventIndexPeg"; import {VoiceRecordingStore} from "../stores/VoiceRecordingStore"; -import PerformanceMonitor, { PerformanceEntryNames } from "../performance"; +import PerformanceMonitor from "../performance"; declare global { interface Window { From 5b6c5aac16f21be4cb604b93f981120f1fe1b828 Mon Sep 17 00:00:00 2001 From: Germain Date: Mon, 17 May 2021 12:02:24 +0100 Subject: [PATCH 085/999] Fix comment typo Co-authored-by: J. Ryan Stinnett --- src/performance/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/performance/index.ts b/src/performance/index.ts index 13ad0a55bb..7eb6d26567 100644 --- a/src/performance/index.ts +++ b/src/performance/index.ts @@ -168,7 +168,7 @@ export default class PerformanceMonitor { } -// Convienience exports +// Convenience exports export { PerformanceEntryNames, } From 07d74693af2e3e2c11d4e455e35e9ee1267e3272 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Tue, 18 May 2021 16:00:39 +0100 Subject: [PATCH 086/999] Start decryption process if needed --- src/Notifier.ts | 2 ++ src/stores/widgets/StopGapWidget.ts | 1 + 2 files changed, 3 insertions(+) diff --git a/src/Notifier.ts b/src/Notifier.ts index 3e927cea0c..4f55046e72 100644 --- a/src/Notifier.ts +++ b/src/Notifier.ts @@ -331,6 +331,8 @@ export const Notifier = { if (!this.isSyncing) return; // don't alert for any messages initially if (ev.sender && ev.sender.userId === MatrixClientPeg.get().credentials.userId) return; + MatrixClientPeg.get().decryptEventIfNeeded(ev); + // If it's an encrypted event and the type is still 'm.room.encrypted', // it hasn't yet been decrypted, so wait until it is. if (ev.isBeingDecrypted() || ev.isDecryptionFailure()) { diff --git a/src/stores/widgets/StopGapWidget.ts b/src/stores/widgets/StopGapWidget.ts index 17371d6d45..b0a76a35af 100644 --- a/src/stores/widgets/StopGapWidget.ts +++ b/src/stores/widgets/StopGapWidget.ts @@ -395,6 +395,7 @@ export class StopGapWidget extends EventEmitter { } private onEvent = (ev: MatrixEvent) => { + MatrixClientPeg.get().decryptEventIfNeeded(ev); if (ev.isBeingDecrypted() || ev.isDecryptionFailure()) return; if (ev.getRoomId() !== this.eventListenerRoomId) return; this.feedEvent(ev); 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 087/999] 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 cb2ee0451d5d919cd4c45773299f1c2d8f5565de Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 19 May 2021 09:06:01 +0100 Subject: [PATCH 088/999] move Settings watchers over to an ES6 Map --- src/settings/SettingsStore.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/settings/SettingsStore.ts b/src/settings/SettingsStore.ts index c32bbe731d..25bc23f572 100644 --- a/src/settings/SettingsStore.ts +++ b/src/settings/SettingsStore.ts @@ -26,7 +26,7 @@ import { _t } from '../languageHandler'; import dis from '../dispatcher/dispatcher'; import { ISetting, SETTINGS } from "./Settings"; import LocalEchoWrapper from "./handlers/LocalEchoWrapper"; -import { WatchManager } from "./WatchManager"; +import { WatchManager, CallbackFn as WatchCallbackFn } from "./WatchManager"; import { SettingLevel } from "./SettingLevel"; import SettingsHandler from "./handlers/SettingsHandler"; @@ -117,7 +117,7 @@ export default class SettingsStore { // We also maintain a list of monitors which are special watchers: they cause dispatches // when the setting changes. We track which rooms we're monitoring though to ensure we // don't duplicate updates on the bus. - private static watchers = {}; // { callbackRef => { callbackFn } } + private static watchers = new Map(); private static monitors = {}; // { settingName => { roomId => callbackRef } } // Counter used for generation of watcher IDs @@ -163,7 +163,7 @@ export default class SettingsStore { callbackFn(originalSettingName, changedInRoomId, atLevel, newValAtLevel, newValue); }; - SettingsStore.watchers[watcherId] = localizedCallback; + SettingsStore.watchers.set(watcherId, localizedCallback); defaultWatchManager.watchSetting(settingName, roomId, localizedCallback); return watcherId; @@ -176,13 +176,13 @@ export default class SettingsStore { * to cancel. */ public static unwatchSetting(watcherReference: string) { - if (!SettingsStore.watchers[watcherReference]) { + if (!SettingsStore.watchers.has(watcherReference)) { console.warn(`Ending non-existent watcher ID ${watcherReference}`); return; } - defaultWatchManager.unwatchSetting(SettingsStore.watchers[watcherReference]); - delete SettingsStore.watchers[watcherReference]; + defaultWatchManager.unwatchSetting(SettingsStore.watchers.get(watcherReference)); + SettingsStore.watchers.delete(watcherReference); } /** From cf501371fa313885c551b650d0959608be5b1f64 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 19 May 2021 09:11:14 +0100 Subject: [PATCH 089/999] move Settings monitors over to an ES6 Map --- src/settings/SettingsStore.ts | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/settings/SettingsStore.ts b/src/settings/SettingsStore.ts index 25bc23f572..e1e300e185 100644 --- a/src/settings/SettingsStore.ts +++ b/src/settings/SettingsStore.ts @@ -118,7 +118,7 @@ export default class SettingsStore { // when the setting changes. We track which rooms we're monitoring though to ensure we // don't duplicate updates on the bus. private static watchers = new Map(); - private static monitors = {}; // { settingName => { roomId => callbackRef } } + private static monitors = new Map>(); // { settingName => { roomId => callbackRef } } // Counter used for generation of watcher IDs private static watcherCount = 1; @@ -196,10 +196,10 @@ export default class SettingsStore { public static monitorSetting(settingName: string, roomId: string) { roomId = roomId || null; // the thing wants null specifically to work, so appease it. - if (!this.monitors[settingName]) this.monitors[settingName] = {}; + if (!this.monitors.has(settingName)) this.monitors.set(settingName, new Map()); const registerWatcher = () => { - this.monitors[settingName][roomId] = SettingsStore.watchSetting( + this.monitors.get(settingName).set(roomId, SettingsStore.watchSetting( settingName, roomId, (settingName, inRoomId, level, newValueAtLevel, newValue) => { dis.dispatch({ action: 'setting_updated', @@ -210,19 +210,20 @@ export default class SettingsStore { newValue, }); }, - ); + )); }; - const hasRoom = Object.keys(this.monitors[settingName]).find((r) => r === roomId || r === null); + const rooms = Array.from(this.monitors.get(settingName).keys()); + const hasRoom = rooms.find((r) => r === roomId || r === null); if (!hasRoom) { registerWatcher(); } else { if (roomId === null) { // Unregister all existing watchers and register the new one - for (const roomId of Object.keys(this.monitors[settingName])) { - SettingsStore.unwatchSetting(this.monitors[settingName][roomId]); - } - this.monitors[settingName] = {}; + rooms.forEach(roomId => { + SettingsStore.unwatchSetting(this.monitors.get(settingName).get(roomId)); + }); + this.monitors.get(settingName).clear(); registerWatcher(); } // else a watcher is already registered for the room, so don't bother registering it again } From 73b24ae2251f144d8bacd93492285085add34577 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 19 May 2021 09:24:46 +0100 Subject: [PATCH 090/999] move WatchManager over to an ES6 Map --- src/settings/WatchManager.ts | 44 ++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 24 deletions(-) diff --git a/src/settings/WatchManager.ts b/src/settings/WatchManager.ts index ea2f158ef6..56f911f180 100644 --- a/src/settings/WatchManager.ts +++ b/src/settings/WatchManager.ts @@ -18,11 +18,7 @@ import { SettingLevel } from "./SettingLevel"; export type CallbackFn = (changedInRoomId: string, atLevel: SettingLevel, newValAtLevel: any) => void; -const IRRELEVANT_ROOM: string = null; - -interface RoomWatcherMap { - [roomId: string]: CallbackFn[]; -} +const IRRELEVANT_ROOM = Symbol("irrelevant-room"); /** * Generalized management class for dealing with watchers on a per-handler (per-level) @@ -30,25 +26,25 @@ interface RoomWatcherMap { * class, which are then proxied outwards to any applicable watchers. */ export class WatchManager { - private watchers: {[settingName: string]: RoomWatcherMap} = {}; + private watchers = new Map>(); // settingName -> roomId -> CallbackFn[] // Proxy for handlers to delegate changes to this manager public watchSetting(settingName: string, roomId: string | null, cb: CallbackFn) { - if (!this.watchers[settingName]) this.watchers[settingName] = {}; - if (!this.watchers[settingName][roomId]) this.watchers[settingName][roomId] = []; - this.watchers[settingName][roomId].push(cb); + if (!this.watchers.has(settingName)) this.watchers.set(settingName, new Map()); + if (!this.watchers.get(settingName).has(roomId)) this.watchers.get(settingName).set(roomId, []); + this.watchers.get(settingName).get(roomId).push(cb); } // Proxy for handlers to delegate changes to this manager public unwatchSetting(cb: CallbackFn) { - for (const settingName of Object.keys(this.watchers)) { - for (const roomId of Object.keys(this.watchers[settingName])) { + this.watchers.forEach((map) => { + map.forEach((callbacks) => { let idx; - while ((idx = this.watchers[settingName][roomId].indexOf(cb)) !== -1) { - this.watchers[settingName][roomId].splice(idx, 1); + while ((idx = callbacks.indexOf(cb)) !== -1) { + callbacks.splice(idx, 1); } - } - } + }); + }); } public notifyUpdate(settingName: string, inRoomId: string | null, atLevel: SettingLevel, newValueAtLevel: any) { @@ -56,21 +52,21 @@ export class WatchManager { // we also don't have a reliable way to get the old value of a setting. Instead, we'll just // let it fall through regardless and let the receiver dedupe if they want to. - if (!this.watchers[settingName]) return; + if (!this.watchers.has(settingName)) return; - const roomWatchers = this.watchers[settingName]; + const roomWatchers = this.watchers.get(settingName); const callbacks = []; - if (inRoomId !== null && roomWatchers[inRoomId]) { - callbacks.push(...roomWatchers[inRoomId]); + if (inRoomId !== null && roomWatchers.has(inRoomId)) { + callbacks.push(...roomWatchers.get(inRoomId)); } if (!inRoomId) { - // Fire updates to all the individual room watchers too, as they probably - // care about the change higher up. - callbacks.push(...Object.values(roomWatchers).flat(1)); - } else if (roomWatchers[IRRELEVANT_ROOM]) { - callbacks.push(...roomWatchers[IRRELEVANT_ROOM]); + // Fire updates to all the individual room watchers too, as they probably care about the change higher up. + const callbacks = Array.from(roomWatchers.values()).flat(1); + callbacks.push(...callbacks); + } else if (roomWatchers.has(IRRELEVANT_ROOM)) { + callbacks.push(...roomWatchers.get(IRRELEVANT_ROOM)); } for (const callback of callbacks) { From bcbfbd508d110ae163597bdbdc384cc2b5ed1264 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Wed, 19 May 2021 09:45:37 +0100 Subject: [PATCH 091/999] Fix event start check --- src/performance/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/performance/index.ts b/src/performance/index.ts index 7eb6d26567..bfb5b4a9c7 100644 --- a/src/performance/index.ts +++ b/src/performance/index.ts @@ -56,7 +56,7 @@ export default class PerformanceMonitor { } const key = this.buildKey(name, id); - if (performance.getEntriesByName(key).length > 0) { + if (performance.getEntriesByName(this.START_PREFIX + key).length > 0) { console.warn(`Recording already started for: ${name}`); return; } From 8a3db3b40ac466c17368ba24571f06820ac84db7 Mon Sep 17 00:00:00 2001 From: Nique Woodhouse Date: Wed, 19 May 2021 10:25:54 +0100 Subject: [PATCH 092/999] Update _MessageActionBar.scss Refine UI of message action bar to increase usability and focus on bar content. --- res/css/views/messages/_MessageActionBar.scss | 37 ++++++++----------- 1 file changed, 15 insertions(+), 22 deletions(-) diff --git a/res/css/views/messages/_MessageActionBar.scss b/res/css/views/messages/_MessageActionBar.scss index d41ac3a4ba..a697c8d0be 100644 --- a/res/css/views/messages/_MessageActionBar.scss +++ b/res/css/views/messages/_MessageActionBar.scss @@ -20,11 +20,12 @@ limitations under the License. visibility: hidden; cursor: pointer; display: flex; - height: 24px; + height: 28px; line-height: $font-24px; - border-radius: 4px; - background: $message-action-bar-bg-color; - top: -26px; + border-radius: 8px; + background: $primary-bg-color; + border: 1px solid $input-border-color; + top: -28px; right: 8px; user-select: none; // Ensure the action bar appears above over things, like the read marker. @@ -51,31 +52,19 @@ limitations under the License. white-space: nowrap; display: inline-block; position: relative; - border: 1px solid $message-action-bar-border-color; - margin-left: -1px; + margin: 2px; &:hover { - border-color: $message-action-bar-hover-border-color; + background: $roomlist-button-bg-color; + border-radius: 6px; z-index: 1; } - - &:first-child { - border-radius: 3px 0 0 3px; - } - - &:last-child { - border-radius: 0 3px 3px 0; - } - - &:only-child { - border-radius: 3px; - } } } - .mx_MessageActionBar_maskButton { - width: 27px; + width: 24px; + height: 24px; } .mx_MessageActionBar_maskButton::after { @@ -88,7 +77,11 @@ limitations under the License. mask-size: 18px; mask-repeat: no-repeat; mask-position: center; - background-color: $message-action-bar-fg-color; + background-color: $secondary-fg-color; +} + +.mx_MessageActionBar_maskButton:hover:after { + background-color: $primary-fg-color; } .mx_MessageActionBar_reactButton::after { From 5daca41bfb049c12ecdb5edfb326f5c257287578 Mon Sep 17 00:00:00 2001 From: Nique Woodhouse Date: Wed, 19 May 2021 11:17:34 +0100 Subject: [PATCH 093/999] Update _MessageActionBar.scss Increase size of buttons --- res/css/views/messages/_MessageActionBar.scss | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/res/css/views/messages/_MessageActionBar.scss b/res/css/views/messages/_MessageActionBar.scss index a697c8d0be..81be6d1917 100644 --- a/res/css/views/messages/_MessageActionBar.scss +++ b/res/css/views/messages/_MessageActionBar.scss @@ -20,12 +20,12 @@ limitations under the License. visibility: hidden; cursor: pointer; display: flex; - height: 28px; + height: 32px; line-height: $font-24px; border-radius: 8px; background: $primary-bg-color; border: 1px solid $input-border-color; - top: -28px; + top: -32px; right: 8px; user-select: none; // Ensure the action bar appears above over things, like the read marker. @@ -63,8 +63,8 @@ limitations under the License. } .mx_MessageActionBar_maskButton { - width: 24px; - height: 24px; + width: 28px; + height: 28px; } .mx_MessageActionBar_maskButton::after { From e6c595a5635cec2eb48c84526c7dd66c0132eb9a Mon Sep 17 00:00:00 2001 From: Germain Date: Wed, 19 May 2021 11:27:32 +0100 Subject: [PATCH 094/999] Delete RoomView dead code --- src/components/structures/RoomView.tsx | 45 -------------------------- 1 file changed, 45 deletions(-) diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index c0f3c59457..7c9afacd55 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -1598,33 +1598,6 @@ export default class RoomView extends React.Component { this.setState({auxPanelMaxHeight: auxPanelMaxHeight}); }; - private onFullscreenClick = () => { - dis.dispatch({ - action: 'video_fullscreen', - fullscreen: true, - }, true); - }; - - private onMuteAudioClick = () => { - const call = this.getCallForRoom(); - if (!call) { - return; - } - const newState = !call.isMicrophoneMuted(); - call.setMicrophoneMuted(newState); - this.forceUpdate(); // TODO: just update the voip buttons - }; - - private onMuteVideoClick = () => { - const call = this.getCallForRoom(); - if (!call) { - return; - } - const newState = !call.isLocalVideoMuted(); - call.setLocalVideoMuted(newState); - this.forceUpdate(); // TODO: just update the voip buttons - }; - private onStatusBarVisible = () => { if (this.unmounted) return; this.setState({ @@ -1640,24 +1613,6 @@ export default class RoomView extends React.Component { }); }; - /** - * called by the parent component when PageUp/Down/etc is pressed. - * - * We pass it down to the scroll panel. - */ - private handleScrollKey = ev => { - let panel; - if (this.searchResultsPanel.current) { - panel = this.searchResultsPanel.current; - } else if (this.messagePanel) { - panel = this.messagePanel; - } - - if (panel) { - panel.handleScrollKey(ev); - } - }; - /** * get any current call for this room */ From 382a08bdb1bd510ac6aa0b7fce7e9ff8ef8e48cd Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Wed, 19 May 2021 11:38:10 +0100 Subject: [PATCH 095/999] Delete RoomView dead code --- src/components/structures/RoomView.tsx | 51 ++------------------------ 1 file changed, 3 insertions(+), 48 deletions(-) diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index dbfba13297..fb9c0eb3a3 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -811,7 +811,7 @@ export default class RoomView extends React.Component { }; private onEvent = (ev) => { - if (ev.isBeingDecrypted() || ev.isDecryptionFailure() || ev.shouldAttemptDecryption()) return; + if (ev.isBeingDecrypted() || ev.isDecryptionFailure()) return; this.handleEffects(ev); }; @@ -831,14 +831,14 @@ export default class RoomView extends React.Component { private onRoomName = (room: Room) => { if (this.state.room && room.roomId == this.state.room.roomId) { - this.forceUpdate(); + // this.forceUpdate(); } }; private onKeyBackupStatus = () => { // Key backup status changes affect whether the in-room recovery // reminder is displayed. - this.forceUpdate(); + // this.forceUpdate(); }; public canResetTimeline = () => { @@ -1598,33 +1598,6 @@ export default class RoomView extends React.Component { this.setState({auxPanelMaxHeight: auxPanelMaxHeight}); }; - private onFullscreenClick = () => { - dis.dispatch({ - action: 'video_fullscreen', - fullscreen: true, - }, true); - }; - - private onMuteAudioClick = () => { - const call = this.getCallForRoom(); - if (!call) { - return; - } - const newState = !call.isMicrophoneMuted(); - call.setMicrophoneMuted(newState); - this.forceUpdate(); // TODO: just update the voip buttons - }; - - private onMuteVideoClick = () => { - const call = this.getCallForRoom(); - if (!call) { - return; - } - const newState = !call.isLocalVideoMuted(); - call.setLocalVideoMuted(newState); - this.forceUpdate(); // TODO: just update the voip buttons - }; - private onStatusBarVisible = () => { if (this.unmounted) return; this.setState({ @@ -1640,24 +1613,6 @@ export default class RoomView extends React.Component { }); }; - /** - * called by the parent component when PageUp/Down/etc is pressed. - * - * We pass it down to the scroll panel. - */ - private handleScrollKey = ev => { - let panel; - if (this.searchResultsPanel.current) { - panel = this.searchResultsPanel.current; - } else if (this.messagePanel) { - panel = this.messagePanel; - } - - if (panel) { - panel.handleScrollKey(ev); - } - }; - /** * get any current call for this room */ From 8f945ce8467d1f4b6984c544e29e30e0c8b6b5d1 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Wed, 19 May 2021 11:57:32 +0100 Subject: [PATCH 096/999] Render nothin rather than an empty div --- src/components/views/elements/AccessibleTooltipButton.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/elements/AccessibleTooltipButton.tsx b/src/components/views/elements/AccessibleTooltipButton.tsx index 3bb264fb3e..c98a7c3156 100644 --- a/src/components/views/elements/AccessibleTooltipButton.tsx +++ b/src/components/views/elements/AccessibleTooltipButton.tsx @@ -73,7 +73,7 @@ export default class AccessibleTooltipButton extends React.PureComponent :
      ; + /> : null; return ( Date: Wed, 19 May 2021 12:11:24 +0100 Subject: [PATCH 097/999] Update _MessageActionBar.scss Correct error on css hover for message action button background colour change --- res/css/views/messages/_MessageActionBar.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/views/messages/_MessageActionBar.scss b/res/css/views/messages/_MessageActionBar.scss index 81be6d1917..e2fafe6c62 100644 --- a/res/css/views/messages/_MessageActionBar.scss +++ b/res/css/views/messages/_MessageActionBar.scss @@ -80,7 +80,7 @@ limitations under the License. background-color: $secondary-fg-color; } -.mx_MessageActionBar_maskButton:hover:after { +.mx_MessageActionBar_maskButton:hover::after { background-color: $primary-fg-color; } From cf8e49729acb733ff81f943831a6bdf39352f8b2 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Wed, 19 May 2021 14:32:49 +0100 Subject: [PATCH 098/999] prevent unwarranted RoomView re-render --- src/components/structures/RoomView.tsx | 15 ++++++++++++- src/components/structures/TimelinePanel.js | 25 ---------------------- 2 files changed, 14 insertions(+), 26 deletions(-) diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index fb9c0eb3a3..25ebbcf223 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -83,6 +83,7 @@ import { objectHasDiff } from "../../utils/objects"; import SpaceRoomView from "./SpaceRoomView"; import { IOpts } from "../../createRoom"; import {replaceableComponent} from "../../utils/replaceableComponent"; +import _ from 'lodash'; const DEBUG = false; let debuglog = function(msg: string) {}; @@ -175,6 +176,7 @@ export interface IState { statusBarVisible: boolean; // We load this later by asking the js-sdk to suggest a version for us. // This object is the result of Room#getRecommendedVersion() + upgradeRecommendation?: { version: string; needsUpgrade: boolean; @@ -528,7 +530,18 @@ export default class RoomView extends React.Component { } shouldComponentUpdate(nextProps, nextState) { - return (objectHasDiff(this.props, nextProps) || objectHasDiff(this.state, nextState)); + const hasPropsDiff = objectHasDiff(this.props, nextProps); + + const newUpgradeRecommendation = nextState.upgradeRecommendation || {} + + const state = _.omit(this.state, ['upgradeRecommendation']); + const newState = _.omit(nextState, ['upgradeRecommendation']) + + const hasStateDiff = + objectHasDiff(state, newState) || + (newUpgradeRecommendation && newUpgradeRecommendation.needsUpgrade === true) + + return hasPropsDiff || hasStateDiff; } componentDidUpdate() { diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index af20c31cb2..832043d3c6 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -36,7 +36,6 @@ import shouldHideEvent from '../../shouldHideEvent'; import EditorStateTransfer from '../../utils/EditorStateTransfer'; import {haveTileForEvent} from "../views/rooms/EventTile"; import {UIFeature} from "../../settings/UIFeature"; -import {objectHasDiff} from "../../utils/objects"; import {replaceableComponent} from "../../utils/replaceableComponent"; import { arrayFastClone } from "../../utils/arrays"; @@ -265,30 +264,6 @@ class TimelinePanel extends React.Component { } } - shouldComponentUpdate(nextProps, nextState) { - if (objectHasDiff(this.props, nextProps)) { - if (DEBUG) { - console.group("Timeline.shouldComponentUpdate: props change"); - console.log("props before:", this.props); - console.log("props after:", nextProps); - console.groupEnd(); - } - return true; - } - - if (objectHasDiff(this.state, nextState)) { - if (DEBUG) { - console.group("Timeline.shouldComponentUpdate: state change"); - console.log("state before:", this.state); - console.log("state after:", nextState); - console.groupEnd(); - } - return true; - } - - return false; - } - componentWillUnmount() { // set a boolean to say we've been unmounted, which any pending // promises can use to throw away their results. From 7d0f3b0d99656361484b601999d2ae8ab35905f7 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Wed, 19 May 2021 14:33:21 +0100 Subject: [PATCH 099/999] Upgrade matrix-js-sdk to 11.1.0-rc.1 --- package.json | 2 +- yarn.lock | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index f8d94e2d21..d7670f55ce 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,7 @@ "katex": "^0.12.0", "linkifyjs": "^2.1.9", "lodash": "^4.17.20", - "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", + "matrix-js-sdk": "11.1.0-rc.1", "matrix-widget-api": "^0.1.0-beta.14", "minimist": "^1.2.5", "opus-recorder": "^8.0.3", diff --git a/yarn.lock b/yarn.lock index 19c0646d32..0472555dd9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5669,9 +5669,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@github:matrix-org/matrix-js-sdk#develop": - version "11.0.0" - resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/52a893a8116d60bb76f1b015d3161a15404b3628" +matrix-js-sdk@11.1.0-rc.1: + version "11.1.0-rc.1" + resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-11.1.0-rc.1.tgz#98da580fa51bc8c70c57984e79dc63c617a671d1" + integrity sha512-yyZeL1mHttw+EnZcGfMH+wd33s0+ZKB+KyXHA3QiqiVRWLgANjIY9QCNjbJYa9FK8zZRqO2yHiFLtTAAU379Ag== dependencies: "@babel/runtime" "^7.12.5" another-json "^0.2.0" From cde23ab6e8e3571bfc1b82f64dd20888816db973 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Wed, 19 May 2021 14:40:36 +0100 Subject: [PATCH 100/999] Prepare changelog for v3.22.0-rc.1 --- CHANGELOG.md | 105 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2582668ef9..3b4ec8b457 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,108 @@ +Changes in [3.22.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.22.0-rc.1) (2021-05-19) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.21.0...v3.22.0-rc.1) + + * Upgrade to JS SDK 11.1.0-rc.1 + * Translations update from Weblate + [\#6068](https://github.com/matrix-org/matrix-react-sdk/pull/6068) + * Show DMs in space for invited members too, to match Android impl + [\#6062](https://github.com/matrix-org/matrix-react-sdk/pull/6062) + * Support filtering by alias in add existing to space dialog + [\#6057](https://github.com/matrix-org/matrix-react-sdk/pull/6057) + * Fix issue when a room without a name or alias is marked as suggested + [\#6064](https://github.com/matrix-org/matrix-react-sdk/pull/6064) + * Fix space room hierarchy not updating when removing a room + [\#6055](https://github.com/matrix-org/matrix-react-sdk/pull/6055) + * Revert "Try putting room list handling behind a lock" + [\#6060](https://github.com/matrix-org/matrix-react-sdk/pull/6060) + * Stop assuming encrypted messages are decrypted ahead of time + [\#6052](https://github.com/matrix-org/matrix-react-sdk/pull/6052) + * Add error detail when languges fail to load + [\#6059](https://github.com/matrix-org/matrix-react-sdk/pull/6059) + * Add space invaders chat effect + [\#6053](https://github.com/matrix-org/matrix-react-sdk/pull/6053) + * Create SpaceProvider and hide Spaces from the RoomProvider autocompleter + [\#6051](https://github.com/matrix-org/matrix-react-sdk/pull/6051) + * Don't mark a room as unread when redacted event is present + [\#6049](https://github.com/matrix-org/matrix-react-sdk/pull/6049) + * Add support for MSC2873: Client information for Widgets + [\#6023](https://github.com/matrix-org/matrix-react-sdk/pull/6023) + * Support UI for MSC2762: Widgets reading events from rooms + [\#5960](https://github.com/matrix-org/matrix-react-sdk/pull/5960) + * Fix crash on opening notification panel + [\#6047](https://github.com/matrix-org/matrix-react-sdk/pull/6047) + * Remove custom LoggedInView::shouldComponentUpdate logic + [\#6046](https://github.com/matrix-org/matrix-react-sdk/pull/6046) + * Fix edge cases with the new add reactions prompt button + [\#6045](https://github.com/matrix-org/matrix-react-sdk/pull/6045) + * Add ids to homeserver and passphrase fields + [\#6043](https://github.com/matrix-org/matrix-react-sdk/pull/6043) + * Update space order field validity requirements to match msc update + [\#6042](https://github.com/matrix-org/matrix-react-sdk/pull/6042) + * Try putting room list handling behind a lock + [\#6024](https://github.com/matrix-org/matrix-react-sdk/pull/6024) + * Improve progress bar progression for smaller voice messages + [\#6035](https://github.com/matrix-org/matrix-react-sdk/pull/6035) + * Fix share space edge case where space is public but not invitable + [\#6039](https://github.com/matrix-org/matrix-react-sdk/pull/6039) + * Add missing 'rel' to image view download button + [\#6033](https://github.com/matrix-org/matrix-react-sdk/pull/6033) + * Improve visible waveform for voice messages + [\#6034](https://github.com/matrix-org/matrix-react-sdk/pull/6034) + * Fix roving tab index intercepting home/end in space create menu + [\#6040](https://github.com/matrix-org/matrix-react-sdk/pull/6040) + * Decorate room avatars with publicity in add existing to space flow + [\#6030](https://github.com/matrix-org/matrix-react-sdk/pull/6030) + * Improve Spaces "Just Me" wizard + [\#6025](https://github.com/matrix-org/matrix-react-sdk/pull/6025) + * Increase hover feedback on room sub list buttons + [\#6037](https://github.com/matrix-org/matrix-react-sdk/pull/6037) + * Show alternative button during space creation wizard if no rooms + [\#6029](https://github.com/matrix-org/matrix-react-sdk/pull/6029) + * Swap rotation buttons in the image viewer + [\#6032](https://github.com/matrix-org/matrix-react-sdk/pull/6032) + * Typo: initilisation -> initialisation + [\#5915](https://github.com/matrix-org/matrix-react-sdk/pull/5915) + * Save edited state of a message when switching rooms + [\#6001](https://github.com/matrix-org/matrix-react-sdk/pull/6001) + * Fix shield icon in Untrusted Device Dialog + [\#6022](https://github.com/matrix-org/matrix-react-sdk/pull/6022) + * Do not eagerly decrypt breadcrumb rooms + [\#6028](https://github.com/matrix-org/matrix-react-sdk/pull/6028) + * Update spaces.png + [\#6031](https://github.com/matrix-org/matrix-react-sdk/pull/6031) + * Encourage more diverse reactions to content + [\#6027](https://github.com/matrix-org/matrix-react-sdk/pull/6027) + * Wrap decodeURIComponent in try-catch to protect against malformed URIs + [\#6026](https://github.com/matrix-org/matrix-react-sdk/pull/6026) + * Iterate beta feedback dialog + [\#6021](https://github.com/matrix-org/matrix-react-sdk/pull/6021) + * Disable space fields whilst their form is busy + [\#6020](https://github.com/matrix-org/matrix-react-sdk/pull/6020) + * Add missing space on beta feedback dialog + [\#6018](https://github.com/matrix-org/matrix-react-sdk/pull/6018) + * Fix colours used for the back button in space create menu + [\#6017](https://github.com/matrix-org/matrix-react-sdk/pull/6017) + * Prioritise and reduce the amount of events decrypted on application startup + [\#5980](https://github.com/matrix-org/matrix-react-sdk/pull/5980) + * Linkify topics in space room directory results + [\#6015](https://github.com/matrix-org/matrix-react-sdk/pull/6015) + * Persistent space collapsed states + [\#5972](https://github.com/matrix-org/matrix-react-sdk/pull/5972) + * Catch another instance of unlabeled avatars. + [\#6010](https://github.com/matrix-org/matrix-react-sdk/pull/6010) + * Rescale and smooth voice message playback waveform to better match + expectation + [\#5996](https://github.com/matrix-org/matrix-react-sdk/pull/5996) + * Scale voice message clock with user's font size + [\#5993](https://github.com/matrix-org/matrix-react-sdk/pull/5993) + * Remove "in development" flag from voice messages + [\#5995](https://github.com/matrix-org/matrix-react-sdk/pull/5995) + * Support voice messages on Safari + [\#5989](https://github.com/matrix-org/matrix-react-sdk/pull/5989) + * Translations update from Weblate + [\#6011](https://github.com/matrix-org/matrix-react-sdk/pull/6011) + Changes in [3.21.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.21.0) (2021-05-17) ===================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.21.0-rc.1...v3.21.0) From 10bc96dc374ec9093705a480a78f0feb06d0fb23 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Wed, 19 May 2021 14:40:37 +0100 Subject: [PATCH 101/999] v3.22.0-rc.1 --- package.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index d7670f55ce..8ecebc4eb3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "3.21.0", + "version": "3.22.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", @@ -200,5 +200,6 @@ "coverageReporters": [ "text" ] - } + }, + "typings": "./lib/index.d.ts" } From 83224dc7b66b6d3b51d6d58568759611726b0d04 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Wed, 19 May 2021 13:32:27 -0400 Subject: [PATCH 102/999] Ensure forward list room decorations are aligned Signed-off-by: Robin Townsend --- res/css/views/dialogs/_ForwardDialog.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/res/css/views/dialogs/_ForwardDialog.scss b/res/css/views/dialogs/_ForwardDialog.scss index 2a72ea4ffb..6fcd7d881a 100644 --- a/res/css/views/dialogs/_ForwardDialog.scss +++ b/res/css/views/dialogs/_ForwardDialog.scss @@ -82,6 +82,7 @@ limitations under the License. display: flex; justify-content: space-between; margin-top: 12px; + height: 32px; .mx_ForwardList_roomButton { display: flex; From 6cb6c7f3d0c538ac5e0742c806849d33ec685631 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Wed, 19 May 2021 13:33:48 -0400 Subject: [PATCH 103/999] Combine forward dialog room and DM lists Signed-off-by: Robin Townsend --- res/css/views/dialogs/_ForwardDialog.scss | 2 +- .../views/dialogs/ForwardDialog.tsx | 46 ++++--------------- 2 files changed, 9 insertions(+), 39 deletions(-) diff --git a/res/css/views/dialogs/_ForwardDialog.scss b/res/css/views/dialogs/_ForwardDialog.scss index 6fcd7d881a..47ab219150 100644 --- a/res/css/views/dialogs/_ForwardDialog.scss +++ b/res/css/views/dialogs/_ForwardDialog.scss @@ -63,7 +63,7 @@ limitations under the License. margin-top: 24px; } - .mx_ForwardList_section { + .mx_ForwardList_results { margin-right: 6px; &:not(:first-child) { diff --git a/src/components/views/dialogs/ForwardDialog.tsx b/src/components/views/dialogs/ForwardDialog.tsx index 1ef4841851..b7a176cfce 100644 --- a/src/components/views/dialogs/ForwardDialog.tsx +++ b/src/components/views/dialogs/ForwardDialog.tsx @@ -34,7 +34,6 @@ import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar"; import AccessibleButton from "../elements/AccessibleButton"; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import AutoHideScrollbar from "../../structures/AutoHideScrollbar"; -import DMRoomMap from "../../../utils/DMRoomMap"; import {RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks"; import {sortRooms} from "../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm"; @@ -162,26 +161,15 @@ const ForwardDialog: React.FC = ({ cli, event, permalinkCreator, onFinis getMxcAvatarUrl: () => profileInfo.avatar_url, }; - const visibleRooms = useMemo(() => sortRooms( + const [query, setQuery] = useState(""); + const lcQuery = query.toLowerCase(); + + const rooms = useMemo(() => sortRooms( cli.getVisibleRooms().filter( room => room.getMyMembership() === "join" && !(SettingsStore.getValue("feature_spaces") && room.isSpaceRoom()), ), - ), [cli]); - - const [query, setQuery] = useState(""); - const lcQuery = query.toLowerCase(); - - const [rooms, dms] = visibleRooms.reduce((arr, room) => { - if (room.name.toLowerCase().includes(lcQuery)) { - if (DMRoomMap.shared().getUserIdForRoomId(room.roomId)) { - arr[1].push(room); - } else { - arr[0].push(room); - } - } - return arr; - }, [[], []]); + ), [cli]).filter(room => room.name.toLowerCase().includes(lcQuery)); const previewLayout = SettingsStore.getValue("layout"); @@ -214,8 +202,7 @@ const ForwardDialog: React.FC = ({ cli, event, permalinkCreator, onFinis /> { rooms.length > 0 ? ( -
      -

      { _t("Rooms") }

      +
      { rooms.map(room => = ({ cli, event, permalinkCreator, onFinis />, ) }
      - ) : undefined } - - { dms.length > 0 ? ( -
      -

      { _t("Direct Messages") }

      - { dms.map(room => - , - ) } -
      - ) : undefined } - - { rooms.length + dms.length < 1 ? + ) : { _t("No results") } - : undefined } + }
      ; From d10a45c6a33997146bf825993e90240503f1efe0 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 19 May 2021 18:40:03 +0100 Subject: [PATCH 104/999] Convert RoomDirectory and NetworkDropdown to Typescript --- .../{RoomDirectory.js => RoomDirectory.tsx} | 351 ++++++++++-------- ...NetworkDropdown.js => NetworkDropdown.tsx} | 91 +++-- 2 files changed, 253 insertions(+), 189 deletions(-) rename src/components/structures/{RoomDirectory.js => RoomDirectory.tsx} (75%) rename src/components/views/directory/{NetworkDropdown.js => NetworkDropdown.tsx} (81%) diff --git a/src/components/structures/RoomDirectory.js b/src/components/structures/RoomDirectory.tsx similarity index 75% rename from src/components/structures/RoomDirectory.js rename to src/components/structures/RoomDirectory.tsx index 3613261da6..2dc9bf2b9f 100644 --- a/src/components/structures/RoomDirectory.js +++ b/src/components/structures/RoomDirectory.tsx @@ -1,7 +1,6 @@ /* -Copyright 2015, 2016 OpenMarket Ltd Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> -Copyright 2019, 2020 The Matrix.org Foundation C.I.C. +Copyright 2015, 2016, 2019, 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. @@ -16,39 +15,90 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; -import {MatrixClientPeg} from "../../MatrixClientPeg"; -import * as sdk from "../../index"; +import React from "react"; + +import { MatrixClientPeg } from "../../MatrixClientPeg"; import dis from "../../dispatcher/dispatcher"; import Modal from "../../Modal"; import { linkifyAndSanitizeHtml } from '../../HtmlUtils'; -import PropTypes from 'prop-types'; import { _t } from '../../languageHandler'; import SdkConfig from '../../SdkConfig'; import { instanceForInstanceId, protocolNameForInstanceId } from '../../utils/DirectoryUtils'; import Analytics from '../../Analytics'; -import {ALL_ROOMS} from "../views/directory/NetworkDropdown"; +import {ALL_ROOMS, IFieldType, IInstance, IProtocol, Protocols} from "../views/directory/NetworkDropdown"; import SettingsStore from "../../settings/SettingsStore"; import GroupFilterOrderStore from "../../stores/GroupFilterOrderStore"; import GroupStore from "../../stores/GroupStore"; import FlairStore from "../../stores/FlairStore"; import CountlyAnalytics from "../../CountlyAnalytics"; -import {replaceableComponent} from "../../utils/replaceableComponent"; -import {mediaFromMxc} from "../../customisations/Media"; +import { replaceableComponent } from "../../utils/replaceableComponent"; +import { mediaFromMxc } from "../../customisations/Media"; +import { IDialogProps } from "../views/dialogs/IDialogProps"; +import AccessibleButton, {ButtonEvent} from "../views/elements/AccessibleButton"; +import BaseAvatar from "../views/avatars/BaseAvatar"; +import ErrorDialog from "../views/dialogs/ErrorDialog"; +import QuestionDialog from "../views/dialogs/QuestionDialog"; +import BaseDialog from "../views/dialogs/BaseDialog"; +import DirectorySearchBox from "../views/elements/DirectorySearchBox"; +import NetworkDropdown from "../views/directory/NetworkDropdown"; +import ScrollPanel from "./ScrollPanel"; +import Spinner from "../views/elements/Spinner"; +import { ActionPayload } from "../../dispatcher/payloads"; + const MAX_NAME_LENGTH = 80; const MAX_TOPIC_LENGTH = 800; -function track(action) { +function track(action: string) { Analytics.trackEvent('RoomDirectory', action); } +interface IProps extends IDialogProps { + initialText?: string; +} + +interface IState { + publicRooms: IRoom[]; + loading: boolean; + protocolsLoading: boolean; + error?: string; + instanceId: string | symbol; + roomServer: string; + filterString: string; + selectedCommunityId?: string; + communityName?: string; +} + +/* eslint-disable camelcase */ +interface IRoom { + room_id: string; + name?: string; + avatar_url?: string; + topic?: string; + canonical_alias?: string; + aliases?: string[]; + world_readable: boolean; + guest_can_join: boolean; + num_joined_members: number; +} + +interface IPublicRoomsRequest { + limit?: number; + since?: string; + server?: string; + filter?: object; + include_all_networks?: boolean; + third_party_instance_id?: string; +} +/* eslint-enable camelcase */ + @replaceableComponent("structures.RoomDirectory") -export default class RoomDirectory extends React.Component { - static propTypes = { - initialText: PropTypes.string, - onFinished: PropTypes.func.isRequired, - }; +export default class RoomDirectory extends React.Component { + private readonly startTime: number; + private unmounted = false + private nextBatch: string = null; + private filterTimeout: NodeJS.Timeout; + private protocols: Protocols; constructor(props) { super(props); @@ -57,34 +107,12 @@ export default class RoomDirectory extends React.Component { this.startTime = CountlyAnalytics.getTimestamp(); const selectedCommunityId = GroupFilterOrderStore.getSelectedTags()[0]; - this.state = { - publicRooms: [], - loading: true, - protocolsLoading: true, - error: null, - instanceId: undefined, - roomServer: MatrixClientPeg.getHomeserverName(), - filterString: this.props.initialText || "", - selectedCommunityId: SettingsStore.getValue("feature_communities_v2_prototypes") - ? selectedCommunityId - : null, - communityName: null, - }; - this._unmounted = false; - this.nextBatch = null; - this.filterTimeout = null; - this.scrollPanel = null; - this.protocols = null; - - this.state.protocolsLoading = true; + let protocolsLoading = true; if (!MatrixClientPeg.get()) { // We may not have a client yet when invoked from welcome page - this.state.protocolsLoading = false; - return; - } - - if (!this.state.selectedCommunityId) { + protocolsLoading = false; + } else if (!this.state.selectedCommunityId) { MatrixClientPeg.get().getThirdpartyProtocols().then((response) => { this.protocols = response; this.setState({protocolsLoading: false}); @@ -109,13 +137,27 @@ export default class RoomDirectory extends React.Component { }); } else { // We don't use the protocols in the communities v2 prototype experience - this.state.protocolsLoading = false; + protocolsLoading = false; // Grab the profile info async FlairStore.getGroupProfileCached(MatrixClientPeg.get(), this.state.selectedCommunityId).then(profile => { this.setState({communityName: profile.name}); }); } + + this.state = { + publicRooms: [], + loading: true, + error: null, + instanceId: undefined, + roomServer: MatrixClientPeg.getHomeserverName(), + filterString: this.props.initialText || "", + selectedCommunityId: SettingsStore.getValue("feature_communities_v2_prototypes") + ? selectedCommunityId + : null, + communityName: null, + protocolsLoading, + }; } componentDidMount() { @@ -126,10 +168,10 @@ export default class RoomDirectory extends React.Component { if (this.filterTimeout) { clearTimeout(this.filterTimeout); } - this._unmounted = true; + this.unmounted = true; } - refreshRoomList = () => { + private refreshRoomList = () => { if (this.state.selectedCommunityId) { this.setState({ publicRooms: GroupStore.getGroupRooms(this.state.selectedCommunityId).map(r => { @@ -165,7 +207,7 @@ export default class RoomDirectory extends React.Component { this.getMoreRooms(); }; - getMoreRooms() { + private getMoreRooms() { if (this.state.selectedCommunityId) return Promise.resolve(); // no more rooms if (!MatrixClientPeg.get()) return Promise.resolve(); @@ -173,34 +215,34 @@ export default class RoomDirectory extends React.Component { loading: true, }); - const my_filter_string = this.state.filterString; - const my_server = this.state.roomServer; + const filterString = this.state.filterString; + const roomServer = this.state.roomServer; // remember the next batch token when we sent the request // too. If it's changed, appending to the list will corrupt it. - const my_next_batch = this.nextBatch; - const opts = {limit: 20}; - if (my_server != MatrixClientPeg.getHomeserverName()) { - opts.server = my_server; + const nextBatch = this.nextBatch; + const opts: IPublicRoomsRequest = { limit: 20 }; + if (roomServer != MatrixClientPeg.getHomeserverName()) { + opts.server = roomServer; } if (this.state.instanceId === ALL_ROOMS) { opts.include_all_networks = true; } else if (this.state.instanceId) { - opts.third_party_instance_id = this.state.instanceId; + opts.third_party_instance_id = this.state.instanceId as string; } if (this.nextBatch) opts.since = this.nextBatch; - if (my_filter_string) opts.filter = { generic_search_term: my_filter_string }; + if (filterString) opts.filter = { generic_search_term: filterString }; return MatrixClientPeg.get().publicRooms(opts).then((data) => { if ( - my_filter_string != this.state.filterString || - my_server != this.state.roomServer || - my_next_batch != this.nextBatch) { + filterString != this.state.filterString || + roomServer != this.state.roomServer || + nextBatch != this.nextBatch) { // if the filter or server has changed since this request was sent, // throw away the result (don't even clear the busy flag // since we must still have a request in flight) return; } - if (this._unmounted) { + if (this.unmounted) { // if we've been unmounted, we don't care either. return; } @@ -211,23 +253,23 @@ export default class RoomDirectory extends React.Component { } this.nextBatch = data.next_batch; - this.setState((s) => { - s.publicRooms.push(...(data.chunk || [])); - s.loading = false; - return s; - }); + this.setState((s) => ({ + ...s, + publicRooms: [...s.publicRooms, ...(data.chunk || [])], + loading: false, + })); return Boolean(data.next_batch); }, (err) => { if ( - my_filter_string != this.state.filterString || - my_server != this.state.roomServer || - my_next_batch != this.nextBatch) { + filterString != this.state.filterString || + roomServer != this.state.roomServer || + nextBatch != this.nextBatch) { // as above: we don't care about errors for old // requests either return; } - if (this._unmounted) { + if (this.unmounted) { // if we've been unmounted, we don't care either. return; } @@ -252,13 +294,10 @@ export default class RoomDirectory extends React.Component { * HS admins to do this through the RoomSettings interface, but * this needs SPEC-417. */ - removeFromDirectory(room) { - const alias = get_display_alias_for_room(room); + private removeFromDirectory(room: IRoom) { + const alias = getDisplayAliasForRoom(room); const name = room.name || alias || _t('Unnamed room'); - const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - let desc; if (alias) { desc = _t('Delete the room address %(alias)s and remove %(name)s from the directory?', {alias, name}); @@ -269,11 +308,10 @@ export default class RoomDirectory extends React.Component { Modal.createTrackedDialog('Remove from Directory', '', QuestionDialog, { title: _t('Remove from Directory'), description: desc, - onFinished: (should_delete) => { - if (!should_delete) return; + onFinished: (shouldDelete: boolean) => { + if (!shouldDelete) return; - const Loader = sdk.getComponent("elements.Spinner"); - const modal = Modal.createDialog(Loader); + const modal = Modal.createDialog(Spinner); let step = _t('remove %(name)s from the directory.', {name: name}); MatrixClientPeg.get().setRoomDirectoryVisibility(room.room_id, 'private').then(() => { @@ -289,14 +327,16 @@ export default class RoomDirectory extends React.Component { console.error("Failed to " + step + ": " + err); Modal.createTrackedDialog('Remove from Directory Error', '', ErrorDialog, { title: _t('Error'), - description: ((err && err.message) ? err.message : _t('The server may be unavailable or overloaded')), + description: (err && err.message) + ? err.message + : _t('The server may be unavailable or overloaded'), }); }); }, }); } - onRoomClicked = (room, ev) => { + private onRoomClicked = (room: IRoom, ev: ButtonEvent) => { if (ev.shiftKey && !this.state.selectedCommunityId) { ev.preventDefault(); this.removeFromDirectory(room); @@ -305,7 +345,7 @@ export default class RoomDirectory extends React.Component { } }; - onOptionChange = (server, instanceId) => { + private onOptionChange = (server: string, instanceId?: string | symbol) => { // clear next batch so we don't try to load more rooms this.nextBatch = null; this.setState({ @@ -325,13 +365,13 @@ export default class RoomDirectory extends React.Component { // Easiest to just blow away the state & re-fetch. }; - onFillRequest = (backwards) => { + private onFillRequest = (backwards: boolean) => { if (backwards || !this.nextBatch) return Promise.resolve(false); return this.getMoreRooms(); }; - onFilterChange = (alias) => { + private onFilterChange = (alias: string) => { this.setState({ filterString: alias || null, }); @@ -349,7 +389,7 @@ export default class RoomDirectory extends React.Component { }, 700); }; - onFilterClear = () => { + private onFilterClear = () => { // update immediately this.setState({ filterString: null, @@ -360,7 +400,7 @@ export default class RoomDirectory extends React.Component { } }; - onJoinFromSearchClick = (alias) => { + private onJoinFromSearchClick = (alias: string) => { // If we don't have a particular instance id selected, just show that rooms alias if (!this.state.instanceId || this.state.instanceId === ALL_ROOMS) { // If the user specified an alias without a domain, add on whichever server is selected @@ -373,9 +413,10 @@ export default class RoomDirectory extends React.Component { // This is a 3rd party protocol. Let's see if we can join it const protocolName = protocolNameForInstanceId(this.protocols, this.state.instanceId); const instance = instanceForInstanceId(this.protocols, this.state.instanceId); - const fields = protocolName ? this._getFieldsForThirdPartyLocation(alias, this.protocols[protocolName], instance) : null; + const fields = protocolName + ? this.getFieldsForThirdPartyLocation(alias, this.protocols[protocolName], instance) + : null; if (!fields) { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const brand = SdkConfig.get().brand; Modal.createTrackedDialog('Unable to join network', '', ErrorDialog, { title: _t('Unable to join network'), @@ -387,14 +428,12 @@ export default class RoomDirectory extends React.Component { if (resp.length > 0 && resp[0].alias) { this.showRoomAlias(resp[0].alias, true); } else { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createTrackedDialog('Room not found', '', ErrorDialog, { title: _t('Room not found'), description: _t('Couldn\'t find a matching Matrix room'), }); } }, (e) => { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createTrackedDialog('Fetching third party location failed', '', ErrorDialog, { title: _t('Fetching third party location failed'), description: _t('Unable to look up room ID from server'), @@ -403,22 +442,22 @@ export default class RoomDirectory extends React.Component { } }; - onPreviewClick = (ev, room) => { + private onPreviewClick = (ev: ButtonEvent, room: IRoom) => { this.showRoom(room, null, false, true); ev.stopPropagation(); }; - onViewClick = (ev, room) => { + private onViewClick = (ev: ButtonEvent, room: IRoom) => { this.showRoom(room); ev.stopPropagation(); }; - onJoinClick = (ev, room) => { + private onJoinClick = (ev: ButtonEvent, room: IRoom) => { this.showRoom(room, null, true); ev.stopPropagation(); }; - onCreateRoomClick = room => { + private onCreateRoomClick = () => { this.onFinished(); dis.dispatch({ action: 'view_create_room', @@ -426,13 +465,13 @@ export default class RoomDirectory extends React.Component { }); }; - showRoomAlias(alias, autoJoin=false) { + private showRoomAlias(alias: string, autoJoin = false) { this.showRoom(null, alias, autoJoin); } - showRoom(room, room_alias, autoJoin = false, shouldPeek = false) { + private showRoom(room: IRoom, roomAlias?: string, autoJoin = false, shouldPeek = false) { this.onFinished(); - const payload = { + const payload: ActionPayload = { action: 'view_room', auto_join: autoJoin, should_peek: shouldPeek, @@ -449,15 +488,15 @@ export default class RoomDirectory extends React.Component { } } - if (!room_alias) { - room_alias = get_display_alias_for_room(room); + if (!roomAlias) { + roomAlias = getDisplayAliasForRoom(room); } payload.oob_data = { avatarUrl: room.avatar_url, // XXX: This logic is duplicated from the JS SDK which // would normally decide what the name is. - name: room.name || room_alias || _t('Unnamed room'), + name: room.name || roomAlias || _t('Unnamed room'), }; if (this.state.roomServer) { @@ -471,21 +510,19 @@ export default class RoomDirectory extends React.Component { // which servers to start querying. However, there's no other way to join rooms in // this list without aliases at present, so if roomAlias isn't set here we have no // choice but to supply the ID. - if (room_alias) { - payload.room_alias = room_alias; + if (roomAlias) { + payload.room_alias = roomAlias; } else { payload.room_id = room.room_id; } dis.dispatch(payload); } - createRoomCells(room) { + private createRoomCells(room: IRoom) { const client = MatrixClientPeg.get(); const clientRoom = client.getRoom(room.room_id); const hasJoinedRoom = clientRoom && clientRoom.getMyMembership() === "join"; const isGuest = client.isGuest(); - const BaseAvatar = sdk.getComponent('avatars.BaseAvatar'); - const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); let previewButton; let joinOrViewButton; @@ -495,20 +532,26 @@ export default class RoomDirectory extends React.Component { // it is readable, the preview appears as normal. if (!hasJoinedRoom && (room.world_readable || isGuest)) { previewButton = ( - this.onPreviewClick(ev, room)}>{_t("Preview")} + this.onPreviewClick(ev, room)}> + { _t("Preview") } + ); } if (hasJoinedRoom) { joinOrViewButton = ( - this.onViewClick(ev, room)}>{_t("View")} + this.onViewClick(ev, room)}> + { _t("View") } + ); } else if (!isGuest) { joinOrViewButton = ( - this.onJoinClick(ev, room)}>{_t("Join")} + this.onJoinClick(ev, room)}> + { _t("Join") } + ); } - let name = room.name || get_display_alias_for_room(room) || _t('Unnamed room'); + let name = room.name || getDisplayAliasForRoom(room) || _t('Unnamed room'); if (name.length > MAX_NAME_LENGTH) { name = `${name.substring(0, MAX_NAME_LENGTH)}...`; } @@ -531,9 +574,13 @@ export default class RoomDirectory extends React.Component { onMouseDown={(ev) => {ev.preventDefault();}} className="mx_RoomDirectory_roomAvatar" > -
      ,
      { ev.stopPropagation(); } } dangerouslySetInnerHTML={{ __html: topic }} /> -
      { get_display_alias_for_room(room) }
      +
      { getDisplayAliasForRoom(room) }
      ,
      this.onRoomClicked(room, ev)} @@ -576,20 +623,16 @@ export default class RoomDirectory extends React.Component { ]; } - collectScrollPanel = (element) => { - this.scrollPanel = element; - }; - - _stringLooksLikeId(s, field_type) { + private stringLooksLikeId(s: string, fieldType: IFieldType) { let pat = /^#[^\s]+:[^\s]/; - if (field_type && field_type.regexp) { - pat = new RegExp(field_type.regexp); + if (fieldType && fieldType.regexp) { + pat = new RegExp(fieldType.regexp); } return pat.test(s); } - _getFieldsForThirdPartyLocation(userInput, protocol, instance) { + private getFieldsForThirdPartyLocation(userInput: string, protocol: IProtocol, instance: IInstance) { // make an object with the fields specified by that protocol. We // require that the values of all but the last field come from the // instance. The last is the user input. @@ -605,71 +648,52 @@ export default class RoomDirectory extends React.Component { return fields; } - /** - * called by the parent component when PageUp/Down/etc is pressed. - * - * We pass it down to the scroll panel. - */ - handleScrollKey = ev => { - if (this.scrollPanel) { - this.scrollPanel.handleScrollKey(ev); - } - }; - - onFinished = () => { + private onFinished = () => { CountlyAnalytics.instance.trackRoomDirectory(this.startTime); - this.props.onFinished(); + this.props.onFinished(false); }; render() { - const Loader = sdk.getComponent("elements.Spinner"); - const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); - const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); - let content; if (this.state.error) { content = this.state.error; } else if (this.state.protocolsLoading) { - content = ; + content = ; } else { const cells = (this.state.publicRooms || []) - .reduce((cells, room) => cells.concat(this.createRoomCells(room)), [],); + .reduce((cells, room) => cells.concat(this.createRoomCells(room)), []); // we still show the scrollpanel, at least for now, because // otherwise we don't fetch more because we don't get a fill // request from the scrollpanel because there isn't one let spinner; if (this.state.loading) { - spinner = ; + spinner = ; } - let scrollpanel_content; + let scrollPanelContent; if (cells.length === 0 && !this.state.loading) { - scrollpanel_content = { _t('No rooms to show') }; + scrollPanelContent = { _t('No rooms to show') }; } else { - scrollpanel_content =
      + scrollPanelContent =
      { cells }
      ; } - const ScrollPanel = sdk.getComponent("structures.ScrollPanel"); - content = - { scrollpanel_content } + { scrollPanelContent } { spinner } ; } let listHeader; if (!this.state.protocolsLoading) { - const NetworkDropdown = sdk.getComponent('directory.NetworkDropdown'); - const DirectorySearchBox = sdk.getComponent('elements.DirectorySearchBox'); - const protocolName = protocolNameForInstanceId(this.protocols, this.state.instanceId); - let instance_expected_field_type; + let instanceExpectedFieldType; if ( protocolName && this.protocols && @@ -677,21 +701,27 @@ export default class RoomDirectory extends React.Component { this.protocols[protocolName].location_fields.length > 0 && this.protocols[protocolName].field_types ) { - const last_field = this.protocols[protocolName].location_fields.slice(-1)[0]; - instance_expected_field_type = this.protocols[protocolName].field_types[last_field]; + const lastField = this.protocols[protocolName].location_fields.slice(-1)[0]; + instanceExpectedFieldType = this.protocols[protocolName].field_types[lastField]; } let placeholder = _t('Find a room…'); if (!this.state.instanceId || this.state.instanceId === ALL_ROOMS) { - placeholder = _t("Find a room… (e.g. %(exampleRoom)s)", {exampleRoom: "#example:" + this.state.roomServer}); - } else if (instance_expected_field_type) { - placeholder = instance_expected_field_type.placeholder; + placeholder = _t("Find a room… (e.g. %(exampleRoom)s)", { + exampleRoom: "#example:" + this.state.roomServer, + }); + } else if (instanceExpectedFieldType) { + placeholder = instanceExpectedFieldType.placeholder; } - let showJoinButton = this._stringLooksLikeId(this.state.filterString, instance_expected_field_type); + let showJoinButton = this.stringLooksLikeId(this.state.filterString, instanceExpectedFieldType); if (protocolName) { const instance = instanceForInstanceId(this.protocols, this.state.instanceId); - if (this._getFieldsForThirdPartyLocation(this.state.filterString, this.protocols[protocolName], instance) === null) { + if (this.getFieldsForThirdPartyLocation( + this.state.filterString, + this.protocols[protocolName], + instance, + ) === null) { showJoinButton = false; } } @@ -723,12 +753,11 @@ export default class RoomDirectory extends React.Component { } const explanation = _t("If you can't find the room you're looking for, ask for an invite or Create a new room.", null, - {a: sub => { - return ({sub}); - }}, + {a: sub => ( + + { sub } + + )}, ); const title = this.state.selectedCommunityId @@ -756,6 +785,6 @@ export default class RoomDirectory extends React.Component { // Similar to matrix-react-sdk's MatrixTools.getDisplayAliasForRoom // but works with the objects we get from the public room list -function get_display_alias_for_room(room) { - return room.canonical_alias || (room.aliases ? room.aliases[0] : ""); +function getDisplayAliasForRoom(room: IRoom) { + return room.canonical_alias || room.aliases?.[0] || ""; } diff --git a/src/components/views/directory/NetworkDropdown.js b/src/components/views/directory/NetworkDropdown.tsx similarity index 81% rename from src/components/views/directory/NetworkDropdown.js rename to src/components/views/directory/NetworkDropdown.tsx index e6ff6d9791..66b7321ce0 100644 --- a/src/components/views/directory/NetworkDropdown.js +++ b/src/components/views/directory/NetworkDropdown.tsx @@ -1,7 +1,6 @@ /* -Copyright 2016 OpenMarket Ltd Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> -Copyright 2020 The Matrix.org Foundation C.I.C. +Copyright 2016, 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. @@ -16,39 +15,42 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, {useEffect, useState} from 'react'; -import PropTypes from 'prop-types'; +import React, { useEffect, useState } from "react"; +import { MatrixError } from "matrix-js-sdk/src/http-api"; -import {MatrixClientPeg} from '../../../MatrixClientPeg'; -import {instanceForInstanceId} from '../../../utils/DirectoryUtils'; +import { MatrixClientPeg } from '../../../MatrixClientPeg'; +import { instanceForInstanceId } from '../../../utils/DirectoryUtils'; import { + ChevronFace, ContextMenu, - useContextMenu, ContextMenuButton, - MenuItemRadio, - MenuItem, MenuGroup, + MenuItem, + MenuItemRadio, + useContextMenu, } from "../../structures/ContextMenu"; -import {_t} from "../../../languageHandler"; +import { _t } from "../../../languageHandler"; import SdkConfig from "../../../SdkConfig"; -import {useSettingValue} from "../../../hooks/useSettings"; -import * as sdk from "../../../index"; +import { useSettingValue } from "../../../hooks/useSettings"; import Modal from "../../../Modal"; import SettingsStore from "../../../settings/SettingsStore"; import withValidation from "../elements/Validation"; +import { SettingLevel } from "../../../settings/SettingLevel"; +import TextInputDialog from "../dialogs/TextInputDialog"; +import QuestionDialog from "../dialogs/QuestionDialog"; export const ALL_ROOMS = Symbol("ALL_ROOMS"); const SETTING_NAME = "room_directory_servers"; -const inPlaceOf = (elementRect) => ({ +const inPlaceOf = (elementRect: Pick) => ({ right: window.innerWidth - elementRect.right, top: elementRect.top, chevronOffset: 0, - chevronFace: "none", + chevronFace: ChevronFace.None, }); -const validServer = withValidation({ +const validServer = withValidation({ deriveData: async ({ value }) => { try { // check if we can successfully load this server's room directory @@ -78,15 +80,49 @@ const validServer = withValidation({ ], }); +/* eslint-disable camelcase */ +export interface IFieldType { + regexp: string; + placeholder: string; +} + +export interface IInstance { + desc: string; + icon?: string; + 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; +} + +export interface IProtocol { + user_fields: string[]; + location_fields: string[]; + icon: string; + field_types: Record; + instances: IInstance[]; +} +/* eslint-enable camelcase */ + +export type Protocols = Record; + +interface IProps { + protocols: Protocols; + selectedServerName: string; + selectedInstanceId: string | symbol; + onOptionChange(server: string, instanceId?: string | symbol): void; +} + // This dropdown sources homeservers from three places: // + your currently connected homeserver // + homeservers in config.json["roomDirectory"] // + homeservers in SettingsStore["room_directory_servers"] // if a server exists in multiple, only keep the top-most entry. -const NetworkDropdown = ({onOptionChange, protocols = {}, selectedServerName, selectedInstanceId}) => { - const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu(); - const _userDefinedServers = useSettingValue(SETTING_NAME); +const NetworkDropdown = ({ onOptionChange, protocols = {}, selectedServerName, selectedInstanceId }: IProps) => { + const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu(); + const _userDefinedServers: string[] = useSettingValue(SETTING_NAME); const [userDefinedServers, _setUserDefinedServers] = useState(_userDefinedServers); const handlerFactory = (server, instanceId) => { @@ -98,7 +134,7 @@ const NetworkDropdown = ({onOptionChange, protocols = {}, selectedServerName, se const setUserDefinedServers = servers => { _setUserDefinedServers(servers); - SettingsStore.setValue(SETTING_NAME, null, "account", servers); + SettingsStore.setValue(SETTING_NAME, null, SettingLevel.ACCOUNT, servers); }; // keep local echo up to date with external changes useEffect(() => { @@ -112,7 +148,7 @@ const NetworkDropdown = ({onOptionChange, protocols = {}, selectedServerName, se const roomDirectory = config.roomDirectory || {}; const hsName = MatrixClientPeg.getHomeserverName(); - const configServers = new Set(roomDirectory.servers); + const configServers = new Set(roomDirectory.servers); // configured servers take preference over user-defined ones, if one occurs in both ignore the latter one. const removableServers = new Set(userDefinedServers.filter(s => !configServers.has(s) && s !== hsName)); @@ -136,9 +172,15 @@ const NetworkDropdown = ({onOptionChange, protocols = {}, selectedServerName, se // add a fake protocol with the ALL_ROOMS symbol protocolsList.push({ instances: [{ + fields: [], + network_id: "", instance_id: ALL_ROOMS, desc: _t("All rooms"), }], + location_fields: [], + user_fields: [], + field_types: {}, + icon: "", }); } @@ -172,7 +214,6 @@ const NetworkDropdown = ({onOptionChange, protocols = {}, selectedServerName, se if (removableServers.has(server)) { const onClick = async () => { closeMenu(); - const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); const {finished} = Modal.createTrackedDialog("Network Dropdown", "Remove server", QuestionDialog, { title: _t("Are you sure?"), description: _t("Are you sure you want to remove %(serverName)s", { @@ -191,7 +232,7 @@ const NetworkDropdown = ({onOptionChange, protocols = {}, selectedServerName, se setUserDefinedServers(servers.filter(s => s !== server)); // the selected server is being removed, reset to our HS - if (serverSelected === server) { + if (serverSelected) { onOptionChange(hsName, undefined); } }; @@ -223,7 +264,6 @@ const NetworkDropdown = ({onOptionChange, protocols = {}, selectedServerName, se const onClick = async () => { closeMenu(); - const TextInputDialog = sdk.getComponent("dialogs.TextInputDialog"); const { finished } = Modal.createTrackedDialog("Network Dropdown", "Add a new server", TextInputDialog, { title: _t("Add a new server"), description: _t("Enter the name of a new server you want to explore."), @@ -284,9 +324,4 @@ const NetworkDropdown = ({onOptionChange, protocols = {}, selectedServerName, se
      ; }; -NetworkDropdown.propTypes = { - onOptionChange: PropTypes.func.isRequired, - protocols: PropTypes.object, -}; - export default NetworkDropdown; From b3aade075d12b28bd0ff53e12967398c06d05f34 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 19 May 2021 19:18:28 +0100 Subject: [PATCH 105/999] Convert CreateRoomDialog to Typescript --- ...eateRoomDialog.js => CreateRoomDialog.tsx} | 174 ++++++++++-------- src/createRoom.ts | 2 +- 2 files changed, 102 insertions(+), 74 deletions(-) rename src/components/views/dialogs/{CreateRoomDialog.js => CreateRoomDialog.tsx} (61%) diff --git a/src/components/views/dialogs/CreateRoomDialog.js b/src/components/views/dialogs/CreateRoomDialog.tsx similarity index 61% rename from src/components/views/dialogs/CreateRoomDialog.js rename to src/components/views/dialogs/CreateRoomDialog.tsx index e9dc6e2be0..2d433f6b51 100644 --- a/src/components/views/dialogs/CreateRoomDialog.js +++ b/src/components/views/dialogs/CreateRoomDialog.tsx @@ -1,6 +1,6 @@ /* Copyright 2017 Michael Telatynski <7t3chguy@gmail.com> -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,45 @@ 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, {ChangeEvent, createRef, KeyboardEvent, SyntheticEvent} from "react"; import {Room} from "matrix-js-sdk/src/models/room"; -import * as sdk from '../../../index'; import SdkConfig from '../../../SdkConfig'; -import withValidation from '../elements/Validation'; -import { _t } from '../../../languageHandler'; +import withValidation, {IFieldState} from '../elements/Validation'; +import {_t} from '../../../languageHandler'; import {MatrixClientPeg} from '../../../MatrixClientPeg'; import {Key} from "../../../Keyboard"; -import {privateShouldBeEncrypted} from "../../../createRoom"; +import {IOpts, Preset, privateShouldBeEncrypted, Visibility} from "../../../createRoom"; import {CommunityPrototypeStore} from "../../../stores/CommunityPrototypeStore"; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import Field from "../elements/Field"; +import RoomAliasField from "../elements/RoomAliasField"; +import LabelledToggleSwitch from "../elements/LabelledToggleSwitch"; +import DialogButtons from "../elements/DialogButtons"; +import BaseDialog from "../dialogs/BaseDialog"; + +interface IProps { + defaultPublic?: boolean; + parentSpace?: Room; + onFinished(proceed: boolean, opts?: IOpts): void; +} + +interface IState { + isPublic: boolean; + isEncrypted: boolean; + name: string; + topic: string; + alias: string; + detailsOpen: boolean; + noFederate: boolean; + nameIsValid: boolean; + canChangeEncryption: boolean; +} @replaceableComponent("views.dialogs.CreateRoomDialog") -export default class CreateRoomDialog extends React.Component { - static propTypes = { - onFinished: PropTypes.func.isRequired, - defaultPublic: PropTypes.bool, - parentSpace: PropTypes.instanceOf(Room), - }; +export default class CreateRoomDialog extends React.Component { + private nameField = createRef(); + private aliasField = createRef(); constructor(props) { super(props); @@ -54,26 +72,25 @@ export default class CreateRoomDialog extends React.Component { }; MatrixClientPeg.get().doesServerForceEncryptionForPreset("private") - .then(isForced => this.setState({canChangeEncryption: !isForced})); + .then(isForced => this.setState({ canChangeEncryption: !isForced })); } - _roomCreateOptions() { - const opts = {}; - const createOpts = opts.createOpts = {}; + private roomCreateOptions() { + const opts: IOpts = {}; + const createOpts: IOpts["createOpts"] = opts.createOpts = {}; createOpts.name = this.state.name; if (this.state.isPublic) { - createOpts.visibility = "public"; - createOpts.preset = "public_chat"; + createOpts.visibility = Visibility.Public; + createOpts.preset = Preset.PublicChat; opts.guestAccess = false; - const {alias} = this.state; - const localPart = alias.substr(1, alias.indexOf(":") - 1); - createOpts['room_alias_name'] = localPart; + const { alias } = this.state; + createOpts.room_alias_name = alias.substr(1, alias.indexOf(":") - 1); } if (this.state.topic) { createOpts.topic = this.state.topic; } if (this.state.noFederate) { - createOpts.creation_content = {'m.federate': false}; + createOpts.creation_content = { 'm.federate': false }; } if (!this.state.isPublic) { @@ -98,16 +115,14 @@ export default class CreateRoomDialog extends React.Component { } componentDidMount() { - this._detailsRef.addEventListener("toggle", this.onDetailsToggled); // move focus to first field when showing dialog - this._nameFieldRef.focus(); + this.nameField.current.focus(); } componentWillUnmount() { - this._detailsRef.removeEventListener("toggle", this.onDetailsToggled); } - _onKeyDown = event => { + private onKeyDown = (event: KeyboardEvent) => { if (event.key === Key.ENTER) { this.onOk(); event.preventDefault(); @@ -115,26 +130,26 @@ export default class CreateRoomDialog extends React.Component { } }; - onOk = async () => { - const activeElement = document.activeElement; + private onOk = async () => { + const activeElement = document.activeElement as HTMLElement; if (activeElement) { activeElement.blur(); } - await this._nameFieldRef.validate({allowEmpty: false}); - if (this._aliasFieldRef) { - await this._aliasFieldRef.validate({allowEmpty: false}); + await this.nameField.current.validate({allowEmpty: false}); + if (this.aliasField.current) { + await this.aliasField.current.validate({allowEmpty: false}); } // Validation and state updates are async, so we need to wait for them to complete // first. Queue a `setState` callback and wait for it to resolve. - await new Promise(resolve => this.setState({}, resolve)); - if (this.state.nameIsValid && (!this._aliasFieldRef || this._aliasFieldRef.isValid)) { - this.props.onFinished(true, this._roomCreateOptions()); + await new Promise(resolve => this.setState({}, resolve)); + if (this.state.nameIsValid && (!this.aliasField.current || this.aliasField.current.isValid)) { + this.props.onFinished(true, this.roomCreateOptions()); } else { let field; if (!this.state.nameIsValid) { - field = this._nameFieldRef; - } else if (this._aliasFieldRef && !this._aliasFieldRef.isValid) { - field = this._aliasFieldRef; + field = this.nameField.current; + } else if (this.aliasField.current && !this.aliasField.current.isValid) { + field = this.aliasField.current; } if (field) { field.focus(); @@ -143,49 +158,45 @@ export default class CreateRoomDialog extends React.Component { } }; - onCancel = () => { + private onCancel = () => { this.props.onFinished(false); }; - onNameChange = ev => { - this.setState({name: ev.target.value}); + private onNameChange = (ev: ChangeEvent) => { + this.setState({ name: ev.target.value }); }; - onTopicChange = ev => { - this.setState({topic: ev.target.value}); + private onTopicChange = (ev: ChangeEvent) => { + this.setState({ topic: ev.target.value }); }; - onPublicChange = isPublic => { - this.setState({isPublic}); + private onPublicChange = (isPublic: boolean) => { + this.setState({ isPublic }); }; - onEncryptedChange = isEncrypted => { - this.setState({isEncrypted}); + private onEncryptedChange = (isEncrypted: boolean) => { + this.setState({ isEncrypted }); }; - onAliasChange = alias => { - this.setState({alias}); + private onAliasChange = (alias: string) => { + this.setState({ alias }); }; - onDetailsToggled = ev => { - this.setState({detailsOpen: ev.target.open}); + private onDetailsToggled = (ev: SyntheticEvent) => { + this.setState({ detailsOpen: (ev.target as HTMLDetailsElement).open }); }; - onNoFederateChange = noFederate => { - this.setState({noFederate}); + private onNoFederateChange = (noFederate: boolean) => { + this.setState({ noFederate }); }; - collectDetailsRef = ref => { - this._detailsRef = ref; - }; - - onNameValidate = async fieldState => { - const result = await CreateRoomDialog._validateRoomName(fieldState); + private onNameValidate = async (fieldState: IFieldState) => { + const result = await CreateRoomDialog.validateRoomName(fieldState); this.setState({nameIsValid: result.valid}); return result; }; - static _validateRoomName = withValidation({ + private static validateRoomName = withValidation({ rules: [ { key: "required", @@ -196,18 +207,17 @@ export default class CreateRoomDialog extends React.Component { }); render() { - const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); - const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); - const Field = sdk.getComponent('views.elements.Field'); - const LabelledToggleSwitch = sdk.getComponent('views.elements.LabelledToggleSwitch'); - const RoomAliasField = sdk.getComponent('views.elements.RoomAliasField'); - let aliasField; if (this.state.isPublic) { const domain = MatrixClientPeg.get().getDomain(); aliasField = (
      - this._aliasFieldRef = ref} onChange={this.onAliasChange} domain={domain} value={this.state.alias} /> +
      ); } @@ -270,16 +280,34 @@ export default class CreateRoomDialog extends React.Component { - +
      - this._nameFieldRef = ref} label={ _t('Name') } onChange={this.onNameChange} onValidate={this.onNameValidate} value={this.state.name} className="mx_CreateRoomDialog_name" /> - - + + + { publicPrivateLabel } { e2eeSection } { aliasField } -
      - { this.state.detailsOpen ? _t('Hide advanced') : _t('Show advanced') } +
      + + { this.state.detailsOpen ? _t('Hide advanced') : _t('Show advanced') } + Date: Wed, 19 May 2021 19:20:58 +0100 Subject: [PATCH 106/999] Pre-populate create room dialog name when going from room directory --- src/components/structures/MatrixChat.tsx | 9 ++++++--- src/components/structures/RoomDirectory.tsx | 1 + src/components/views/dialogs/CreateRoomDialog.tsx | 3 ++- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 4c7fca2fec..49386c5f65 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -665,7 +665,7 @@ export default class MatrixChat extends React.PureComponent { break; } case 'view_create_room': - this.createRoom(payload.public); + this.createRoom(payload.public, payload.defaultName); break; case 'view_create_group': { let CreateGroupDialog = sdk.getComponent("dialogs.CreateGroupDialog") @@ -1011,7 +1011,7 @@ export default class MatrixChat extends React.PureComponent { }); } - private async createRoom(defaultPublic = false) { + private async createRoom(defaultPublic = false, defaultName?: string) { const communityId = CommunityPrototypeStore.instance.getSelectedCommunityId(); if (communityId) { // double check the user will have permission to associate this room with the community @@ -1025,7 +1025,10 @@ export default class MatrixChat extends React.PureComponent { } const CreateRoomDialog = sdk.getComponent('dialogs.CreateRoomDialog'); - const modal = Modal.createTrackedDialog('Create Room', '', CreateRoomDialog, { defaultPublic }); + const modal = Modal.createTrackedDialog('Create Room', '', CreateRoomDialog, { + defaultPublic, + defaultName, + }); const [shouldCreate, opts] = await modal.finished; if (shouldCreate) { diff --git a/src/components/structures/RoomDirectory.tsx b/src/components/structures/RoomDirectory.tsx index 2dc9bf2b9f..f9021b2f5e 100644 --- a/src/components/structures/RoomDirectory.tsx +++ b/src/components/structures/RoomDirectory.tsx @@ -462,6 +462,7 @@ export default class RoomDirectory extends React.Component { dis.dispatch({ action: 'view_create_room', public: true, + defaultName: this.state.filterString.trim(), }); }; diff --git a/src/components/views/dialogs/CreateRoomDialog.tsx b/src/components/views/dialogs/CreateRoomDialog.tsx index 2d433f6b51..cce6b6c34c 100644 --- a/src/components/views/dialogs/CreateRoomDialog.tsx +++ b/src/components/views/dialogs/CreateRoomDialog.tsx @@ -34,6 +34,7 @@ import BaseDialog from "../dialogs/BaseDialog"; interface IProps { defaultPublic?: boolean; + defaultName?: string; parentSpace?: Room; onFinished(proceed: boolean, opts?: IOpts): void; } @@ -62,7 +63,7 @@ export default class CreateRoomDialog extends React.Component { this.state = { isPublic: this.props.defaultPublic || false, isEncrypted: privateShouldBeEncrypted(), - name: "", + name: this.props.defaultName || "", topic: "", alias: "", detailsOpen: false, From a61e977b5fa7bf129b1f45e1f6aa205e7ddcce4b Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 19 May 2021 19:30:56 +0100 Subject: [PATCH 107/999] Fix room directory ts migration --- src/components/structures/RoomDirectory.tsx | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/components/structures/RoomDirectory.tsx b/src/components/structures/RoomDirectory.tsx index f9021b2f5e..b5e16b8804 100644 --- a/src/components/structures/RoomDirectory.tsx +++ b/src/components/structures/RoomDirectory.tsx @@ -106,19 +106,21 @@ export default class RoomDirectory extends React.Component { CountlyAnalytics.instance.trackRoomDirectoryBegin(); this.startTime = CountlyAnalytics.getTimestamp(); - const selectedCommunityId = GroupFilterOrderStore.getSelectedTags()[0]; + const selectedCommunityId = SettingsStore.getValue("feature_communities_v2_prototypes") + ? GroupFilterOrderStore.getSelectedTags()[0] + : null; let protocolsLoading = true; if (!MatrixClientPeg.get()) { // We may not have a client yet when invoked from welcome page protocolsLoading = false; - } else if (!this.state.selectedCommunityId) { + } else if (!selectedCommunityId) { MatrixClientPeg.get().getThirdpartyProtocols().then((response) => { this.protocols = response; - this.setState({protocolsLoading: false}); + this.setState({ protocolsLoading: false }); }, (err) => { console.warn(`error loading third party protocols: ${err}`); - this.setState({protocolsLoading: false}); + this.setState({ protocolsLoading: false }); if (MatrixClientPeg.get().isGuest()) { // Guests currently aren't allowed to use this API, so // ignore this as otherwise this error is literally the @@ -131,7 +133,7 @@ export default class RoomDirectory extends React.Component { error: _t( '%(brand)s failed to get the protocol list from the homeserver. ' + 'The homeserver may be too old to support third party networks.', - {brand}, + { brand }, ), }); }); @@ -141,7 +143,7 @@ export default class RoomDirectory extends React.Component { // Grab the profile info async FlairStore.getGroupProfileCached(MatrixClientPeg.get(), this.state.selectedCommunityId).then(profile => { - this.setState({communityName: profile.name}); + this.setState({ communityName: profile.name }); }); } @@ -152,9 +154,7 @@ export default class RoomDirectory extends React.Component { instanceId: undefined, roomServer: MatrixClientPeg.getHomeserverName(), filterString: this.props.initialText || "", - selectedCommunityId: SettingsStore.getValue("feature_communities_v2_prototypes") - ? selectedCommunityId - : null, + selectedCommunityId, communityName: null, protocolsLoading, }; From aa7ccc1420a0db1723cd81d21da9534465fb77b5 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 19 May 2021 20:07:54 +0100 Subject: [PATCH 108/999] Show prompt to create new room from room directory results --- res/css/structures/_RoomDirectory.scss | 38 ++++++++++++++++++--- src/components/structures/RoomDirectory.tsx | 25 ++++++++++++-- src/i18n/strings/en_EN.json | 2 ++ 3 files changed, 58 insertions(+), 7 deletions(-) diff --git a/res/css/structures/_RoomDirectory.scss b/res/css/structures/_RoomDirectory.scss index 89cb21b7a6..ec07500af5 100644 --- a/res/css/structures/_RoomDirectory.scss +++ b/res/css/structures/_RoomDirectory.scss @@ -61,6 +61,39 @@ limitations under the License. .mx_RoomDirectory_tableWrapper { overflow-y: auto; flex: 1 1 0; + + .mx_RoomDirectory_footer { + margin-top: 24px; + text-align: center; + + > h5 { + margin: 0; + font-weight: $font-semi-bold; + font-size: $font-15px; + line-height: $font-18px; + color: $primary-fg-color; + } + + > p { + margin: 40px auto 60px; + font-size: $font-14px; + line-height: $font-20px; + color: $secondary-fg-color; + max-width: 464px; // easier reading + } + + > hr { + margin: 0; + border: none; + height: 1px; + background-color: $header-panel-bg-color; + } + + .mx_RoomDirectory_newRoom { + margin: 24px auto 0; + width: max-content; + } + } } .mx_RoomDirectory_table { @@ -138,11 +171,6 @@ limitations under the License. color: $settings-grey-fg-color; } -.mx_RoomDirectory_table tr { - padding-bottom: 10px; - cursor: pointer; -} - .mx_RoomDirectory .mx_RoomView_MessageList { padding: 0; } diff --git a/src/components/structures/RoomDirectory.tsx b/src/components/structures/RoomDirectory.tsx index b5e16b8804..04fad7d3fa 100644 --- a/src/components/structures/RoomDirectory.tsx +++ b/src/components/structures/RoomDirectory.tsx @@ -672,22 +672,43 @@ export default class RoomDirectory extends React.Component { spinner = ; } + const createNewButton = <> +
      + + { _t("Create new room") } + + ; + let scrollPanelContent; + let footer; if (cells.length === 0 && !this.state.loading) { - scrollPanelContent = { _t('No rooms to show') }; + footer = <> +
      { _t('No results for "%(query)s"', { query: this.state.filterString.trim() }) }
      +

      + { _t("Try different words or check for typos. " + + "Some results may not be visible as they're private and you need an invite to see them.") } +

      + { createNewButton } + ; } else { scrollPanelContent =
      { cells }
      ; + if (!this.state.loading && !this.nextBatch) { + footer = createNewButton; + } } content = { scrollPanelContent } { spinner } + { footer &&
      + { footer } +
      }
      ; } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index d1fe791319..7d12a1b827 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2646,6 +2646,8 @@ "Unable to look up room ID from server": "Unable to look up room ID from server", "Preview": "Preview", "View": "View", + "No results for \"%(query)s\"": "No results for \"%(query)s\"", + "Try different words or check for typos. Some results may not be visible as they're private and you need an invite to see them.": "Try different words or check for typos. Some results may not be visible as they're private and you need an invite to see them.", "Find a room…": "Find a room…", "Find a room… (e.g. %(exampleRoom)s)": "Find a room… (e.g. %(exampleRoom)s)", "If you can't find the room you're looking for, ask for an invite or Create a new room.": "If you can't find the room you're looking for, ask for an invite or Create a new room.", 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 109/999] 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 110/999] 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 d3623217068ca9d2bc8cb170ac1f9f8329b9de3a Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Thu, 20 May 2021 15:25:20 +0100 Subject: [PATCH 111/999] Simplify SenderProfile DOM structure --- res/css/views/rooms/_IRCLayout.scss | 15 ++++++--------- src/components/views/messages/SenderProfile.js | 17 +++++------------ 2 files changed, 11 insertions(+), 21 deletions(-) diff --git a/res/css/views/rooms/_IRCLayout.scss b/res/css/views/rooms/_IRCLayout.scss index b6b901757c..48505fbb53 100644 --- a/res/css/views/rooms/_IRCLayout.scss +++ b/res/css/views/rooms/_IRCLayout.scss @@ -177,16 +177,13 @@ $irc-line-height: $font-18px; .mx_SenderProfile_hover { background-color: $primary-bg-color; overflow: hidden; + display: flex; - > span { - display: flex; - - > .mx_SenderProfile_name { - overflow: hidden; - text-overflow: ellipsis; - min-width: var(--name-width); - text-align: end; - } + > .mx_SenderProfile_name { + overflow: hidden; + text-overflow: ellipsis; + min-width: var(--name-width); + text-align: end; } } diff --git a/src/components/views/messages/SenderProfile.js b/src/components/views/messages/SenderProfile.js index bd10526799..f1855de99e 100644 --- a/src/components/views/messages/SenderProfile.js +++ b/src/components/views/messages/SenderProfile.js @@ -110,19 +110,12 @@ export default class SenderProfile extends React.Component { const nameElem = name || ''; - // Name + flair - const nameFlair = - - { nameElem } - - { flair } - ; - return ( -
      -
      - { nameFlair } -
      +
      + + { nameElem } + + { flair }
      ); } From 171539d42d560bf37abbfdfba86bb0f22986f84e Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Thu, 20 May 2021 15:26:02 +0100 Subject: [PATCH 112/999] Simplify EventTile structure Only render MessageTimestamp to the DOM when a tile is hovered --- src/components/structures/MessagePanel.js | 65 +++++++++++------------ src/components/views/rooms/EventTile.tsx | 46 ++++++++++------ 2 files changed, 61 insertions(+), 50 deletions(-) diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index d1071a9e19..2fb9a2df29 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -645,39 +645,36 @@ export default class MessagePanel extends React.Component { // use txnId as key if available so that we don't remount during sending ret.push( -
    7. - - - -
    8. , + + + , ); return ret; @@ -779,7 +776,7 @@ export default class MessagePanel extends React.Component { } _collectEventNode = (eventId, node) => { - this.eventNodes[eventId] = node; + this.eventNodes[eventId] = node?.ref?.current; } // once dynamic content in the events load, make the scrollPanel check the diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 19c5a7acaa..884669e398 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -277,6 +277,9 @@ interface IProps { // Helper to build permalinks for the room permalinkCreator?: RoomPermalinkCreator; + + // Symbol of the root node + as?: string } interface IState { @@ -291,6 +294,8 @@ interface IState { previouslyRequestedKeys: boolean; // The Relations model from the JS SDK for reactions to `mxEvent` reactions: Relations; + + hover: boolean; } @replaceableComponent("views.rooms.EventTile") @@ -322,6 +327,8 @@ export default class EventTile extends React.Component { previouslyRequestedKeys: false, // The Relations model from the JS SDK for reactions to `mxEvent` reactions: this.getReactions(), + + hover: false, }; // don't do RR animations until we are mounted @@ -333,6 +340,8 @@ export default class EventTile extends React.Component { // to determine if we've already subscribed and use a combination of other flags to find // out if we should even be subscribed at all. this.isListeningForReceipts = false; + + this.ref = React.createRef(); } /** @@ -960,7 +969,7 @@ export default class EventTile extends React.Component { onFocusChange={this.onActionBarFocusChange} /> : undefined; - const timestamp = this.props.mxEvent.getTs() ? + const timestamp = this.props.mxEvent.getTs() && this.state.hover ? : null; const keyRequestHelpText = @@ -1131,11 +1140,20 @@ export default class EventTile extends React.Component { // tab-index=-1 to allow it to be focusable but do not add tab stop for it, primarily for screen readers return ( -
      - { ircTimestamp } - { sender } - { ircPadlock } -
      + React.createElement(this.props.as || "div", { + "ref": this.ref, + "className": classes, + "tabIndex": -1, + "aria-live": ariaLive, + "aria-atomic": "true", + "data-scroll-tokens": this.props["data-scroll-tokens"], + "onMouseEnter": () => this.setState({ hover: true }), + "onMouseLeave": () => this.setState({ hover: false }), + }, [ + ircTimestamp, + sender, + ircPadlock, +
      { groupTimestamp } { groupPadlock } { thread } @@ -1152,16 +1170,12 @@ export default class EventTile extends React.Component { { keyRequestInfo } { reactionsRow } { actionBar } -
      - {msgOption} - { - // The avatar goes after the event tile as it's absolutely positioned to be over the - // event tile line, so needs to be later in the DOM so it appears on top (this avoids - // the need for further z-indexing chaos) - } - { avatar } -
      - ); +
      , + msgOption, + avatar, + + ]) + ) } } } From f058fd8869dca84b05d70fc1390da262d2e38439 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Thu, 20 May 2021 15:39:25 +0100 Subject: [PATCH 113/999] Reduce amount of DOM nodes --- res/css/views/rooms/_IRCLayout.scss | 3 +-- src/components/views/elements/ReplyThread.js | 2 +- src/components/views/rooms/EventTile.tsx | 23 ++++++++++++-------- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/res/css/views/rooms/_IRCLayout.scss b/res/css/views/rooms/_IRCLayout.scss index 48505fbb53..cf61ce569d 100644 --- a/res/css/views/rooms/_IRCLayout.scss +++ b/res/css/views/rooms/_IRCLayout.scss @@ -115,8 +115,7 @@ $irc-line-height: $font-18px; .mx_EventTile_line { .mx_EventTile_e2eIcon, .mx_TextualEvent, - .mx_MTextBody, - .mx_ReplyThread_wrapper_empty { + .mx_MTextBody { display: inline-block; } } diff --git a/src/components/views/elements/ReplyThread.js b/src/components/views/elements/ReplyThread.js index 870803995d..c336f34b51 100644 --- a/src/components/views/elements/ReplyThread.js +++ b/src/components/views/elements/ReplyThread.js @@ -214,7 +214,7 @@ export default class ReplyThread extends React.Component { static makeThread(parentEv, onHeightChanged, permalinkCreator, ref, layout) { if (!ReplyThread.getParentEventId(parentEv)) { - return
      ; + return null; } return { let left = 0; const receipts = this.props.readReceipts || []; + + if (receipts.length === 0) { + return null; + } + for (let i = 0; i < receipts.length; ++i) { const receipt = receipts[i]; @@ -699,10 +704,14 @@ export default class EventTile extends React.Component { } } - return - { remText } - { avatars } - ; + return ( +
      + + { remText } + { avatars } + ; +
      + ) } onSenderProfileClick = event => { @@ -1032,11 +1041,7 @@ export default class EventTile extends React.Component { let msgOption; if (this.props.showReadReceipts) { const readAvatars = this.getReadAvatars(); - msgOption = ( -
      - { readAvatars } -
      - ); + msgOption = readAvatars; } switch (this.props.tileShape) { 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 114/999] 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 9e55f2409214f97014143549997e5d445d9787ed Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Thu, 20 May 2021 16:11:33 +0100 Subject: [PATCH 115/999] Remove extraenous DOM nodes --- src/components/structures/ContextMenu.tsx | 2 +- src/components/structures/ToastContainer.tsx | 22 ++++++++++--------- src/components/views/rooms/RoomHeader.js | 18 +++++++-------- .../views/rooms/SimpleRoomHeader.js | 12 +++++----- src/components/views/rooms/WhoIsTypingTile.js | 2 +- 5 files changed, 27 insertions(+), 29 deletions(-) diff --git a/src/components/structures/ContextMenu.tsx b/src/components/structures/ContextMenu.tsx index ad0f75e162..f9de113d07 100644 --- a/src/components/structures/ContextMenu.tsx +++ b/src/components/structures/ContextMenu.tsx @@ -389,7 +389,7 @@ export class ContextMenu extends React.PureComponent { } render(): React.ReactChild { - return ReactDOM.createPortal(this.renderMenu(), getOrCreateContainer()); + return ReactDOM.createPortal(this.renderMenu(), document.body); } } diff --git a/src/components/structures/ToastContainer.tsx b/src/components/structures/ToastContainer.tsx index 1fd3e3419f..273c8a079f 100644 --- a/src/components/structures/ToastContainer.tsx +++ b/src/components/structures/ToastContainer.tsx @@ -55,6 +55,7 @@ export default class ToastContainer extends React.Component<{}, IState> { const totalCount = this.state.toasts.length; const isStacked = totalCount > 1; let toast; + let containerClasses; if (totalCount !== 0) { const topToast = this.state.toasts[0]; const {title, icon, key, component, className, props} = topToast; @@ -79,16 +80,17 @@ export default class ToastContainer extends React.Component<{}, IState> {
      {React.createElement(component, toastProps)}
      ); + + containerClasses = classNames("mx_ToastContainer", { + "mx_ToastContainer_stacked": isStacked, + }); } - - const containerClasses = classNames("mx_ToastContainer", { - "mx_ToastContainer_stacked": isStacked, - }); - - return ( -
      - {toast} -
      - ); + return toast + ? ( +
      + {toast} +
      + ) + : null; } } diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js index 6d3b50c10d..a527d7625c 100644 --- a/src/components/views/rooms/RoomHeader.js +++ b/src/components/views/rooms/RoomHeader.js @@ -257,16 +257,14 @@ export default class RoomHeader extends React.Component { const e2eIcon = this.props.e2eStatus ? : undefined; return ( -
      -
      -
      { roomAvatar }
      -
      { e2eIcon }
      - { name } - { topicElement } - { cancelButton } - { rightRow } - -
      +
      +
      { roomAvatar }
      +
      { e2eIcon }
      + { name } + { topicElement } + { cancelButton } + { rightRow } +
      ); } diff --git a/src/components/views/rooms/SimpleRoomHeader.js b/src/components/views/rooms/SimpleRoomHeader.js index b2a66f6670..9aedb38654 100644 --- a/src/components/views/rooms/SimpleRoomHeader.js +++ b/src/components/views/rooms/SimpleRoomHeader.js @@ -62,13 +62,11 @@ export default class SimpleRoomHeader extends React.Component { } return ( -
      -
      -
      - { icon } - { this.props.title } - { cancelButton } -
      +
      +
      + { icon } + { this.props.title } + { cancelButton }
      ); diff --git a/src/components/views/rooms/WhoIsTypingTile.js b/src/components/views/rooms/WhoIsTypingTile.js index a25b43fc3a..396d64a6f8 100644 --- a/src/components/views/rooms/WhoIsTypingTile.js +++ b/src/components/views/rooms/WhoIsTypingTile.js @@ -204,7 +204,7 @@ export default class WhoIsTypingTile extends React.Component { this.props.whoIsTypingLimit, ); if (!typingString) { - return (
      ); + return null; } return ( From 21c1179f8d2726a909c5540f8e25a92fa0070dc3 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 20 May 2021 17:54:42 +0100 Subject: [PATCH 116/999] Update extensions for more files with types This migrates the another bucket of files using some amount of Flow typing to mark them as TypeScript instead. The remaining type errors are fixed in subsequent commits. --- ...eAuthEntryComponents.js => InteractiveAuthEntryComponents.tsx} | 0 .../views/dialogs/{DevtoolsDialog.js => DevtoolsDialog.tsx} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename src/components/views/auth/{InteractiveAuthEntryComponents.js => InteractiveAuthEntryComponents.tsx} (100%) rename src/components/views/dialogs/{DevtoolsDialog.js => DevtoolsDialog.tsx} (100%) diff --git a/src/components/views/auth/InteractiveAuthEntryComponents.js b/src/components/views/auth/InteractiveAuthEntryComponents.tsx similarity index 100% rename from src/components/views/auth/InteractiveAuthEntryComponents.js rename to src/components/views/auth/InteractiveAuthEntryComponents.tsx diff --git a/src/components/views/dialogs/DevtoolsDialog.js b/src/components/views/dialogs/DevtoolsDialog.tsx similarity index 100% rename from src/components/views/dialogs/DevtoolsDialog.js rename to src/components/views/dialogs/DevtoolsDialog.tsx From 6574ca98fa098e3690391cfc0152ccaca2ea4cfd Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Wed, 12 May 2021 14:06:10 +0100 Subject: [PATCH 117/999] Fix basic lint errors --- .../auth/InteractiveAuthEntryComponents.tsx | 4 +- .../views/dialogs/DevtoolsDialog.tsx | 57 +++++++++++++++---- 2 files changed, 49 insertions(+), 12 deletions(-) diff --git a/src/components/views/auth/InteractiveAuthEntryComponents.tsx b/src/components/views/auth/InteractiveAuthEntryComponents.tsx index e34349c474..5a492b14ee 100644 --- a/src/components/views/auth/InteractiveAuthEntryComponents.tsx +++ b/src/components/views/auth/InteractiveAuthEntryComponents.tsx @@ -786,7 +786,9 @@ export class FallbackAuthEntry extends React.Component { } return ( ); diff --git a/src/components/views/dialogs/DevtoolsDialog.tsx b/src/components/views/dialogs/DevtoolsDialog.tsx index 8a035263cc..1d544af315 100644 --- a/src/components/views/dialogs/DevtoolsDialog.tsx +++ b/src/components/views/dialogs/DevtoolsDialog.tsx @@ -169,8 +169,16 @@ export class SendCustomEvent extends GenericEditor { { !this.state.message && } { showTglFlip &&
      - -
      }
      ; @@ -253,8 +261,17 @@ class SendAccountData extends GenericEditor { { !this.state.message && } { !this.state.message &&
      - -
      }
      ; @@ -581,8 +598,16 @@ class AccountDataExplorer extends React.PureComponent {
      { !this.state.message &&
      - -
      }
      ; @@ -1062,27 +1087,37 @@ class SettingsExplorer extends React.Component {
      {_t("Value:")}  - {this.renderSettingValue(SettingsStore.getValue(this.state.viewSetting))} + {this.renderSettingValue( + SettingsStore.getValue(this.state.viewSetting), + )}
      {_t("Value in this room:")}  - {this.renderSettingValue(SettingsStore.getValue(this.state.viewSetting, room.roomId))} + {this.renderSettingValue( + SettingsStore.getValue(this.state.viewSetting, room.roomId), + )}
      {_t("Values at explicit levels:")} -
      {this.renderExplicitSettingValues(this.state.viewSetting, null)}
      +
      {this.renderExplicitSettingValues(
      +                                this.state.viewSetting, null,
      +                            )}
      {_t("Values at explicit levels in this room:")} -
      {this.renderExplicitSettingValues(this.state.viewSetting, room.roomId)}
      +
      {this.renderExplicitSettingValues(
      +                                this.state.viewSetting, room.roomId,
      +                            )}
      - +
      From df09bdf823e3c3cc018827c93f95ebf73e58a288 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Wed, 12 May 2021 19:28:22 +0100 Subject: [PATCH 118/999] Add types to InteractiveAuthEntryComponents --- src/Terms.ts | 14 +- .../auth/InteractiveAuthEntryComponents.tsx | 371 ++++++++++-------- src/languageHandler.tsx | 8 +- 3 files changed, 213 insertions(+), 180 deletions(-) diff --git a/src/Terms.ts b/src/Terms.ts index 1bdff36cbc..1b1c152fdd 100644 --- a/src/Terms.ts +++ b/src/Terms.ts @@ -36,14 +36,18 @@ export class Service { } } -interface Policy { +export interface LocalisedPolicy { + name: string; + url: string; +} + +export interface Policy { // @ts-ignore: No great way to express indexed types together with other keys version: string; - [lang: string]: { - url: string; - }; + [lang: string]: LocalisedPolicy; } -type Policies = { + +export type Policies = { [policy: string]: Policy, }; diff --git a/src/components/views/auth/InteractiveAuthEntryComponents.tsx b/src/components/views/auth/InteractiveAuthEntryComponents.tsx index 5a492b14ee..066c064cc1 100644 --- a/src/components/views/auth/InteractiveAuthEntryComponents.tsx +++ b/src/components/views/auth/InteractiveAuthEntryComponents.tsx @@ -1,7 +1,5 @@ /* -Copyright 2016 OpenMarket Ltd -Copyright 2017 Vector Creations Ltd -Copyright 2019, 2020 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,9 +14,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, {createRef} from 'react'; -import PropTypes from 'prop-types'; -import classnames from 'classnames'; +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'; @@ -27,6 +25,7 @@ import AccessibleButton from "../elements/AccessibleButton"; import Spinner from "../elements/Spinner"; import CountlyAnalytics from "../../../CountlyAnalytics"; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { LocalisedPolicy, Policies } from '../../../Terms'; /* This file contains a collection of components which are used by the * InteractiveAuth to prompt the user to enter the information needed @@ -74,21 +73,49 @@ import {replaceableComponent} from "../../../utils/replaceableComponent"; * focus: set the input focus appropriately in the form. */ +enum AuthType { + Password = "m.login.password", + Recaptcha = "m.login.recaptcha", + Terms = "m.login.terms", + Email = "m.login.email.identity", + Msisdn = "m.login.msisdn", + Sso = "m.login.sso", + SsoUnstable = "org.matrix.login.sso", +} + +/* eslint-disable camelcase */ +interface IAuthDict { + type?: AuthType; + // TODO: Remove `user` once servers support proper UIA + // See https://github.com/vector-im/element-web/issues/10312 + user?: string; + identifier?: object; + password?: string; + response?: string; + // TODO: Remove `threepid_creds` once servers support proper UIA + // See https://github.com/vector-im/element-web/issues/10312 + // See https://github.com/matrix-org/matrix-doc/issues/2220 + threepid_creds?: object; + threepidCreds?: object; +} +/* eslint-enable camelcase */ + export const DEFAULT_PHASE = 0; -@replaceableComponent("views.auth.PasswordAuthEntry") -export class PasswordAuthEntry extends React.Component { - static LOGIN_TYPE = "m.login.password"; +interface IAuthEntryProps { + matrixClient: MatrixClient; + loginType: string; + authSessionId: string; + submitAuthDict: (auth: IAuthDict) => void; + errorText?: string; + // Is the auth logic currently waiting for something to happen? + busy?: boolean; + onPhaseChange: (phase: number) => void; +} - static propTypes = { - matrixClient: PropTypes.object.isRequired, - submitAuthDict: PropTypes.func.isRequired, - errorText: PropTypes.string, - // is the auth logic currently waiting for something to - // happen? - busy: PropTypes.bool, - onPhaseChange: PropTypes.func.isRequired, - }; +@replaceableComponent("views.auth.PasswordAuthEntry") +export class PasswordAuthEntry extends React.Component { + static LOGIN_TYPE = AuthType.Password; componentDidMount() { this.props.onPhaseChange(DEFAULT_PHASE); @@ -98,12 +125,12 @@ export class PasswordAuthEntry extends React.Component { password: "", }; - _onSubmit = e => { + private onSubmit = (e: FormEvent) => { e.preventDefault(); if (this.props.busy) return; this.props.submitAuthDict({ - type: PasswordAuthEntry.LOGIN_TYPE, + type: AuthType.Password, // TODO: Remove `user` once servers support proper UIA // See https://github.com/vector-im/element-web/issues/10312 user: this.props.matrixClient.credentials.userId, @@ -115,7 +142,7 @@ export class PasswordAuthEntry extends React.Component { }); }; - _onPasswordFieldChange = ev => { + private onPasswordFieldChange = (ev: ChangeEvent) => { // enable the submit button iff the password is non-empty this.setState({ password: ev.target.value, @@ -123,7 +150,7 @@ export class PasswordAuthEntry extends React.Component { }; render() { - const passwordBoxClass = classnames({ + const passwordBoxClass = classNames({ "error": this.props.errorText, }); @@ -155,7 +182,7 @@ export class PasswordAuthEntry extends React.Component { return (

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

      - +
      { submitButtonOrSpinner } @@ -175,26 +202,26 @@ export class PasswordAuthEntry extends React.Component { } } -@replaceableComponent("views.auth.RecaptchaAuthEntry") -export class RecaptchaAuthEntry extends React.Component { - static LOGIN_TYPE = "m.login.recaptcha"; - - static propTypes = { - submitAuthDict: PropTypes.func.isRequired, - stageParams: PropTypes.object.isRequired, - errorText: PropTypes.string, - busy: PropTypes.bool, - onPhaseChange: PropTypes.func.isRequired, +/* eslint-disable camelcase */ +interface IRecaptchaAuthEntryProps extends IAuthEntryProps { + stageParams?: { + public_key?: string; }; +} +/* eslint-enable camelcase */ + +@replaceableComponent("views.auth.RecaptchaAuthEntry") +export class RecaptchaAuthEntry extends React.Component { + static LOGIN_TYPE = AuthType.Recaptcha; componentDidMount() { this.props.onPhaseChange(DEFAULT_PHASE); } - _onCaptchaResponse = response => { + private onCaptchaResponse = (response: string) => { CountlyAnalytics.instance.track("onboarding_grecaptcha_submit"); this.props.submitAuthDict({ - type: RecaptchaAuthEntry.LOGIN_TYPE, + type: AuthType.Recaptcha, response: response, }); }; @@ -230,7 +257,7 @@ export class RecaptchaAuthEntry extends React.Component { return (
      { errorSection }
      @@ -238,18 +265,28 @@ export class RecaptchaAuthEntry extends React.Component { } } -@replaceableComponent("views.auth.TermsAuthEntry") -export class TermsAuthEntry extends React.Component { - static LOGIN_TYPE = "m.login.terms"; - - static propTypes = { - submitAuthDict: PropTypes.func.isRequired, - stageParams: PropTypes.object.isRequired, - errorText: PropTypes.string, - busy: PropTypes.bool, - showContinue: PropTypes.bool, - onPhaseChange: PropTypes.func.isRequired, +interface ITermsAuthEntryProps extends IAuthEntryProps { + stageParams?: { + policies?: Policies; }; + showContinue: boolean; +} + +interface LocalisedPolicyWithId extends LocalisedPolicy { + id: string; +} + +interface ITermsAuthEntryState { + policies: LocalisedPolicyWithId[]; + toggledPolicies: { + [policy: string]: boolean; + }; + errorText?: string; +} + +@replaceableComponent("views.auth.TermsAuthEntry") +export class TermsAuthEntry extends React.Component { + static LOGIN_TYPE = AuthType.Terms; constructor(props) { super(props); @@ -294,8 +331,11 @@ export class TermsAuthEntry extends React.Component { initToggles[policyId] = false; - langPolicy.id = policyId; - pickedPolicies.push(langPolicy); + pickedPolicies.push({ + id: policyId, + name: langPolicy.name, + url: langPolicy.url, + }); } this.state = { @@ -312,10 +352,10 @@ export class TermsAuthEntry extends React.Component { } tryContinue = () => { - this._trySubmit(); + this.trySubmit(); }; - _togglePolicy(policyId) { + private togglePolicy(policyId: string) { const newToggles = {}; for (const policy of this.state.policies) { let checked = this.state.toggledPolicies[policy.id]; @@ -326,7 +366,7 @@ export class TermsAuthEntry extends React.Component { this.setState({"toggledPolicies": newToggles}); } - _trySubmit = () => { + private trySubmit = () => { let allChecked = true; for (const policy of this.state.policies) { const checked = this.state.toggledPolicies[policy.id]; @@ -334,7 +374,7 @@ export class TermsAuthEntry extends React.Component { } if (allChecked) { - this.props.submitAuthDict({type: TermsAuthEntry.LOGIN_TYPE}); + this.props.submitAuthDict({type: AuthType.Terms}); CountlyAnalytics.instance.track("onboarding_terms_complete"); } else { this.setState({errorText: _t("Please review and accept all of the homeserver's policies")}); @@ -356,7 +396,7 @@ export class TermsAuthEntry extends React.Component { checkboxes.push( // XXX: replace with StyledCheckbox , ); @@ -375,7 +415,7 @@ export class TermsAuthEntry extends React.Component { if (this.props.showContinue !== false) { // XXX: button classes submitButton = ; + onClick={this.trySubmit} disabled={!allChecked}>{_t("Accept")}; } return ( @@ -389,21 +429,18 @@ export class TermsAuthEntry extends React.Component { } } -@replaceableComponent("views.auth.EmailIdentityAuthEntry") -export class EmailIdentityAuthEntry extends React.Component { - static LOGIN_TYPE = "m.login.email.identity"; - - static propTypes = { - matrixClient: PropTypes.object.isRequired, - submitAuthDict: PropTypes.func.isRequired, - authSessionId: PropTypes.string.isRequired, - clientSecret: PropTypes.string.isRequired, - inputs: PropTypes.object.isRequired, - stageState: PropTypes.object.isRequired, - fail: PropTypes.func.isRequired, - setEmailSid: PropTypes.func.isRequired, - onPhaseChange: PropTypes.func.isRequired, +interface IEmailIdentityAuthEntryProps extends IAuthEntryProps { + inputs?: { + emailAddress?: string; }; + stageState?: { + emailSid: string; + }; +} + +@replaceableComponent("views.auth.EmailIdentityAuthEntry") +export class EmailIdentityAuthEntry extends React.Component { + static LOGIN_TYPE = AuthType.Email; componentDidMount() { this.props.onPhaseChange(DEFAULT_PHASE); @@ -427,7 +464,7 @@ export class EmailIdentityAuthEntry extends React.Component { return (

      { _t("A confirmation email has been sent to %(emailAddress)s", - { emailAddress: (sub) => { this.props.inputs.emailAddress } }, + { emailAddress: { this.props.inputs.emailAddress } }, ) }

      { _t("Open the link in the email to continue registration.") }

      @@ -437,37 +474,34 @@ export class EmailIdentityAuthEntry extends React.Component { } } -@replaceableComponent("views.auth.MsisdnAuthEntry") -export class MsisdnAuthEntry extends React.Component { - static LOGIN_TYPE = "m.login.msisdn"; - - static propTypes = { - inputs: PropTypes.shape({ - phoneCountry: PropTypes.string, - phoneNumber: PropTypes.string, - }), - fail: PropTypes.func, - clientSecret: PropTypes.func, - submitAuthDict: PropTypes.func.isRequired, - matrixClient: PropTypes.object, - onPhaseChange: PropTypes.func.isRequired, +interface IMsisdnAuthEntryProps extends IAuthEntryProps { + inputs: { + phoneCountry: string; + phoneNumber: string; }; + clientSecret: string; + fail: (error: Error) => void; +} + +@replaceableComponent("views.auth.MsisdnAuthEntry") +export class MsisdnAuthEntry extends React.Component { + static LOGIN_TYPE = AuthType.Msisdn; + + private submitUrl: string; + private sid: string; + private msisdn: string; state = { token: '', requestingToken: false, + errorText: '', }; componentDidMount() { this.props.onPhaseChange(DEFAULT_PHASE); - this._submitUrl = null; - this._sid = null; - this._msisdn = null; - this._tokenBox = null; - this.setState({requestingToken: true}); - this._requestMsisdnToken().catch((e) => { + this.requestMsisdnToken().catch((e) => { this.props.fail(e); }).finally(() => { this.setState({requestingToken: false}); @@ -477,26 +511,26 @@ export class MsisdnAuthEntry extends React.Component { /* * Requests a verification token by SMS. */ - _requestMsisdnToken() { + private requestMsisdnToken(): Promise { return this.props.matrixClient.requestRegisterMsisdnToken( this.props.inputs.phoneCountry, this.props.inputs.phoneNumber, this.props.clientSecret, 1, // TODO: Multiple send attempts? ).then((result) => { - this._submitUrl = result.submit_url; - this._sid = result.sid; - this._msisdn = result.msisdn; + this.submitUrl = result.submit_url; + this.sid = result.sid; + this.msisdn = result.msisdn; }); } - _onTokenChange = e => { + private onTokenChange = (e: ChangeEvent) => { this.setState({ token: e.target.value, }); }; - _onFormSubmit = async e => { + private onFormSubmit = async (e: FormEvent) => { e.preventDefault(); if (this.state.token == '') return; @@ -506,20 +540,20 @@ export class MsisdnAuthEntry extends React.Component { try { let result; - if (this._submitUrl) { + if (this.submitUrl) { result = await this.props.matrixClient.submitMsisdnTokenOtherUrl( - this._submitUrl, this._sid, this.props.clientSecret, this.state.token, + this.submitUrl, this.sid, this.props.clientSecret, this.state.token, ); } else { throw new Error("The registration with MSISDN flow is misconfigured"); } if (result.success) { const creds = { - sid: this._sid, + sid: this.sid, client_secret: this.props.clientSecret, }; this.props.submitAuthDict({ - type: MsisdnAuthEntry.LOGIN_TYPE, + type: AuthType.Msisdn, // TODO: Remove `threepid_creds` once servers support proper UIA // See https://github.com/vector-im/element-web/issues/10312 // See https://github.com/matrix-org/matrix-doc/issues/2220 @@ -543,7 +577,7 @@ export class MsisdnAuthEntry extends React.Component { return ; } else { const enableSubmit = Boolean(this.state.token); - const submitClasses = classnames({ + const submitClasses = classNames({ mx_InteractiveAuthEntryComponents_msisdnSubmit: true, mx_GeneralButton: true, }); @@ -558,16 +592,16 @@ export class MsisdnAuthEntry extends React.Component { return (

      { _t("A text message has been sent to %(msisdn)s", - { msisdn: { this._msisdn } }, + { msisdn: { this.msisdn } }, ) }

      { _t("Please enter the code it contains:") }

      - +
      @@ -584,40 +618,40 @@ export class MsisdnAuthEntry extends React.Component { } } -@replaceableComponent("views.auth.SSOAuthEntry") -export class SSOAuthEntry extends React.Component { - static propTypes = { - matrixClient: PropTypes.object.isRequired, - authSessionId: PropTypes.string.isRequired, - loginType: PropTypes.string.isRequired, - submitAuthDict: PropTypes.func.isRequired, - errorText: PropTypes.string, - onPhaseChange: PropTypes.func.isRequired, - continueText: PropTypes.string, - continueKind: PropTypes.string, - onCancel: PropTypes.func, - }; +interface ISSOAuthEntryProps extends IAuthEntryProps { + continueText?: string; + continueKind?: string; + onCancel?: () => void; +} - static LOGIN_TYPE = "m.login.sso"; - static UNSTABLE_LOGIN_TYPE = "org.matrix.login.sso"; +interface ISSOAuthEntryState { + phase: number; + attemptFailed: boolean; +} + +@replaceableComponent("views.auth.SSOAuthEntry") +export class SSOAuthEntry extends React.Component { + static LOGIN_TYPE = AuthType.Sso; + static UNSTABLE_LOGIN_TYPE = AuthType.SsoUnstable; static PHASE_PREAUTH = 1; // button to start SSO static PHASE_POSTAUTH = 2; // button to confirm SSO completed - _ssoUrl: string; + private ssoUrl: string; + private popupWindow: Window; constructor(props) { super(props); // We actually send the user through fallback auth so we don't have to // deal with a redirect back to us, losing application context. - this._ssoUrl = props.matrixClient.getFallbackAuthUrl( + this.ssoUrl = props.matrixClient.getFallbackAuthUrl( this.props.loginType, this.props.authSessionId, ); - this._popupWindow = null; - window.addEventListener("message", this._onReceiveMessage); + this.popupWindow = null; + window.addEventListener("message", this.onReceiveMessage); this.state = { phase: SSOAuthEntry.PHASE_PREAUTH, @@ -625,15 +659,15 @@ export class SSOAuthEntry extends React.Component { }; } - componentDidMount(): void { + componentDidMount() { this.props.onPhaseChange(SSOAuthEntry.PHASE_PREAUTH); } componentWillUnmount() { - window.removeEventListener("message", this._onReceiveMessage); - if (this._popupWindow) { - this._popupWindow.close(); - this._popupWindow = null; + window.removeEventListener("message", this.onReceiveMessage); + if (this.popupWindow) { + this.popupWindow.close(); + this.popupWindow = null; } } @@ -643,11 +677,11 @@ export class SSOAuthEntry extends React.Component { }); }; - _onReceiveMessage = event => { + private onReceiveMessage = (event: MessageEvent) => { if (event.data === "authDone" && event.origin === this.props.matrixClient.getHomeserverUrl()) { - if (this._popupWindow) { - this._popupWindow.close(); - this._popupWindow = null; + if (this.popupWindow) { + this.popupWindow.close(); + this.popupWindow = null; } } }; @@ -657,7 +691,7 @@ export class SSOAuthEntry extends React.Component { // certainly will need to open the thing in a new tab to avoid losing application // context. - this._popupWindow = window.open(this._ssoUrl, "_blank"); + this.popupWindow = window.open(this.ssoUrl, "_blank"); this.setState({phase: SSOAuthEntry.PHASE_POSTAUTH}); this.props.onPhaseChange(SSOAuthEntry.PHASE_POSTAUTH); }; @@ -716,46 +750,37 @@ export class SSOAuthEntry extends React.Component { } @replaceableComponent("views.auth.FallbackAuthEntry") -export class FallbackAuthEntry extends React.Component { - static propTypes = { - matrixClient: PropTypes.object.isRequired, - authSessionId: PropTypes.string.isRequired, - loginType: PropTypes.string.isRequired, - submitAuthDict: PropTypes.func.isRequired, - errorText: PropTypes.string, - onPhaseChange: PropTypes.func.isRequired, - }; +export class FallbackAuthEntry extends React.Component { + private popupWindow: Window; + private fallbackButton = createRef(); constructor(props) { super(props); // we have to make the user click a button, as browsers will block // the popup if we open it immediately. - this._popupWindow = null; - window.addEventListener("message", this._onReceiveMessage); - - this._fallbackButton = createRef(); + this.popupWindow = null; + window.addEventListener("message", this.onReceiveMessage); } - componentDidMount() { this.props.onPhaseChange(DEFAULT_PHASE); } componentWillUnmount() { - window.removeEventListener("message", this._onReceiveMessage); - if (this._popupWindow) { - this._popupWindow.close(); + window.removeEventListener("message", this.onReceiveMessage); + if (this.popupWindow) { + this.popupWindow.close(); } } focus = () => { - if (this._fallbackButton.current) { - this._fallbackButton.current.focus(); + if (this.fallbackButton.current) { + this.fallbackButton.current.focus(); } }; - _onShowFallbackClick = e => { + private onShowFallbackClick = (e: MouseEvent) => { e.preventDefault(); e.stopPropagation(); @@ -763,10 +788,10 @@ export class FallbackAuthEntry extends React.Component { this.props.loginType, this.props.authSessionId, ); - this._popupWindow = window.open(url, "_blank"); + this.popupWindow = window.open(url, "_blank"); }; - _onReceiveMessage = event => { + private onReceiveMessage = (event: MessageEvent) => { if ( event.data === "authDone" && event.origin === this.props.matrixClient.getHomeserverUrl() @@ -786,7 +811,7 @@ export class FallbackAuthEntry extends React.Component { } return (
      - { + { _t("Start authentication") } {errorSection} @@ -795,20 +820,22 @@ export class FallbackAuthEntry extends React.Component { } } -const AuthEntryComponents = [ - PasswordAuthEntry, - RecaptchaAuthEntry, - EmailIdentityAuthEntry, - MsisdnAuthEntry, - TermsAuthEntry, - SSOAuthEntry, -]; - -export default function getEntryComponentForLoginType(loginType) { - for (const c of AuthEntryComponents) { - if (c.LOGIN_TYPE === loginType || c.UNSTABLE_LOGIN_TYPE === loginType) { - return c; - } +export default function getEntryComponentForLoginType(loginType: AuthType): typeof React.Component { + switch (loginType) { + case AuthType.Password: + return PasswordAuthEntry; + case AuthType.Recaptcha: + return RecaptchaAuthEntry; + case AuthType.Email: + return EmailIdentityAuthEntry; + case AuthType.Msisdn: + return MsisdnAuthEntry; + case AuthType.Terms: + return TermsAuthEntry; + case AuthType.Sso: + case AuthType.SsoUnstable: + return SSOAuthEntry; + default: + return FallbackAuthEntry; } - return FallbackAuthEntry; } diff --git a/src/languageHandler.tsx b/src/languageHandler.tsx index 26c89afec6..16950dc008 100644 --- a/src/languageHandler.tsx +++ b/src/languageHandler.tsx @@ -105,12 +105,14 @@ function safeCounterpartTranslate(text: string, options?: object) { return translated; } +type SubstitutionValue = number | string | React.ReactNode | ((sub: string) => React.ReactNode); + export interface IVariables { count?: number; - [key: string]: number | string; + [key: string]: SubstitutionValue; } -type Tags = Record React.ReactNode>; +type Tags = Record; export type TranslatedString = string | React.ReactNode; @@ -247,7 +249,7 @@ export function replaceByRegexes(text: string, mapping: IVariables | Tags): stri let replaced; // If substitution is a function, call it if (mapping[regexpString] instanceof Function) { - replaced = (mapping as Tags)[regexpString].apply(null, capturedGroups); + replaced = ((mapping as Tags)[regexpString] as Function)(...capturedGroups); } else { replaced = mapping[regexpString]; } From d9e490926b5bd4f0601f826a6918c954d41791d8 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Tue, 18 May 2021 15:20:08 +0100 Subject: [PATCH 119/999] Add types to DevtoolsDialog --- .../views/dialogs/DevtoolsDialog.tsx | 392 ++++++++++-------- 1 file changed, 221 insertions(+), 171 deletions(-) diff --git a/src/components/views/dialogs/DevtoolsDialog.tsx b/src/components/views/dialogs/DevtoolsDialog.tsx index 1d544af315..81d3a77327 100644 --- a/src/components/views/dialogs/DevtoolsDialog.tsx +++ b/src/components/views/dialogs/DevtoolsDialog.tsx @@ -1,5 +1,6 @@ /* Copyright 2017 Michael Telatynski <7t3chguy@gmail.com> +Copyright 2018-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 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, {useState, useEffect} from 'react'; -import PropTypes from 'prop-types'; +import React, {useState, useEffect, ChangeEvent, MouseEvent} from 'react'; import * as sdk from '../../../index'; import SyntaxHighlight from '../elements/SyntaxHighlight'; import { _t } from '../../../languageHandler'; @@ -30,8 +30,9 @@ import { PHASE_DONE, PHASE_STARTED, PHASE_CANCELLED, + VerificationRequest, } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; -import WidgetStore from "../../../stores/WidgetStore"; +import WidgetStore, { IApp } from "../../../stores/WidgetStore"; import {UPDATE_EVENT} from "../../../stores/AsyncStore"; import {SETTINGS} from "../../../settings/Settings"; import SettingsStore, {LEVEL_ORDER} from "../../../settings/SettingsStore"; @@ -40,17 +41,22 @@ import ErrorDialog from "./ErrorDialog"; import {replaceableComponent} from "../../../utils/replaceableComponent"; import {Room} from "matrix-js-sdk/src/models/room"; import {MatrixEvent} from "matrix-js-sdk/src/models/event"; +import { SettingLevel } from '../../../settings/SettingLevel'; -class GenericEditor extends React.PureComponent { - // static propTypes = {onBack: PropTypes.func.isRequired}; +interface IGenericEditorProps { + onBack: () => void; +} - constructor(props) { - super(props); - this._onChange = this._onChange.bind(this); - this.onBack = this.onBack.bind(this); - } +interface IGenericEditorState { + message?: string; + [inputId: string]: boolean | string; +} - onBack() { +abstract class GenericEditor< + P extends IGenericEditorProps = IGenericEditorProps, + S extends IGenericEditorState = IGenericEditorState, +> extends React.PureComponent { + protected onBack = () => { if (this.state.message) { this.setState({ message: null }); } else { @@ -58,47 +64,60 @@ class GenericEditor extends React.PureComponent { } } - _onChange(e) { + protected onChange = (e: ChangeEvent) => { + // @ts-ignore: Unsure how to convince TS this is okay when the state + // type can be extended. this.setState({[e.target.id]: e.target.type === 'checkbox' ? e.target.checked : e.target.value}); } - _buttons() { - return
      + protected abstract send(); + + protected buttons(): React.ReactNode { + return
      - { !this.state.message && } + { !this.state.message && }
      ; } - textInput(id, label) { + protected textInput(id: string, label: string): React.ReactNode { return ; } } -export class SendCustomEvent extends GenericEditor { - static getLabel() { return _t('Send Custom Event'); } - - static propTypes = { - onBack: PropTypes.func.isRequired, - room: PropTypes.instanceOf(Room).isRequired, - forceStateEvent: PropTypes.bool, - forceGeneralEvent: PropTypes.bool, - inputs: PropTypes.object, +interface ISendCustomEventProps extends IGenericEditorProps { + room: Room; + forceStateEvent?: boolean; + forceGeneralEvent?: boolean; + inputs?: { + eventType?: string; + stateKey?: string; + evContent?: string; }; +} + +interface ISendCustomEventState extends IGenericEditorState { + isStateEvent: boolean; + eventType: string; + stateKey: string; + evContent: string; +} + +export class SendCustomEvent extends GenericEditor { + static getLabel() { return _t('Send Custom Event'); } static contextType = MatrixClientContext; constructor(props) { super(props); - this._send = this._send.bind(this); const {eventType, stateKey, evContent} = Object.assign({ eventType: '', @@ -115,7 +134,7 @@ export class SendCustomEvent extends GenericEditor { }; } - send(content) { + private doSend(content: object): Promise { const cli = this.context; if (this.state.isStateEvent) { return cli.sendStateEvent(this.props.room.roomId, this.state.eventType, content, this.state.stateKey); @@ -124,7 +143,7 @@ export class SendCustomEvent extends GenericEditor { } } - async _send() { + protected send = async () => { if (this.state.eventType === '') { this.setState({ message: _t('You must specify an event type!') }); return; @@ -133,7 +152,7 @@ export class SendCustomEvent extends GenericEditor { let message; try { const content = JSON.parse(this.state.evContent); - await this.send(content); + await this.doSend(content); message = _t('Event sent!'); } catch (e) { message = _t('Failed to send custom event.') + ' (' + e.toString() + ')'; @@ -147,7 +166,7 @@ export class SendCustomEvent extends GenericEditor {
      { this.state.message }
      - { this._buttons() } + { this.buttons() }
      ; } @@ -163,16 +182,16 @@ export class SendCustomEvent extends GenericEditor {
      + autoComplete="off" value={this.state.evContent} onChange={this.onChange} element="textarea" />
      -
      +
      - { !this.state.message && } + { !this.state.message && } { showTglFlip &&
      ; } @@ -255,17 +282,17 @@ class SendAccountData extends GenericEditor {
      + autoComplete="off" value={this.state.evContent} onChange={this.onChange} element="textarea" />
      -
      +
      - { !this.state.message && } + { !this.state.message && } { !this.state.message &&
      -
      +
      @@ -482,31 +517,29 @@ class RoomStateExplorer extends React.PureComponent {
      { list }
      -
      +
      ; } } -class AccountDataExplorer extends React.PureComponent { - static getLabel() { return _t('Explore Account Data'); } +interface IAccountDataExplorerState { + isRoomAccountData: boolean; + event?: MatrixEvent; + editing: boolean; + queryEventType: string; + [inputId: string]: boolean | string; +} - static propTypes = { - onBack: PropTypes.func.isRequired, - room: PropTypes.instanceOf(Room).isRequired, - }; +class AccountDataExplorer extends React.PureComponent { + static getLabel() { return _t('Explore Account Data'); } static contextType = MatrixClientContext; constructor(props) { super(props); - this.onBack = this.onBack.bind(this); - this.editEv = this.editEv.bind(this); - this._onChange = this._onChange.bind(this); - this.onQueryEventType = this.onQueryEventType.bind(this); - this.state = { isRoomAccountData: false, event: null, @@ -516,20 +549,20 @@ class AccountDataExplorer extends React.PureComponent { }; } - getData() { + private getData(): Record { if (this.state.isRoomAccountData) { return this.props.room.accountData; } return this.context.store.accountData; } - onViewSourceClick(event) { + private onViewSourceClick(event: MatrixEvent) { return () => { this.setState({ event }); }; } - onBack() { + private onBack = () => { if (this.state.editing) { this.setState({ editing: false }); } else if (this.state.event) { @@ -539,15 +572,15 @@ class AccountDataExplorer extends React.PureComponent { } } - _onChange(e) { + private onChange = (e: ChangeEvent) => { this.setState({[e.target.id]: e.target.type === 'checkbox' ? e.target.checked : e.target.value}); } - editEv() { + private editEv = () => { this.setState({ editing: true }); } - onQueryEventType(queryEventType) { + private onQueryEventType = (queryEventType: string) => { this.setState({ queryEventType }); } @@ -570,7 +603,7 @@ class AccountDataExplorer extends React.PureComponent { { JSON.stringify(this.state.event.event, null, 2) }
      -
      +
      @@ -595,40 +628,41 @@ class AccountDataExplorer extends React.PureComponent { { rows }
      -
      +
      - { !this.state.message &&
      +
      } +
      ; } } -class ServersInRoomList extends React.PureComponent { +interface IServersInRoomListState { + query: string; +} + +class ServersInRoomList extends React.PureComponent { static getLabel() { return _t('View Servers in Room'); } - static propTypes = { - onBack: PropTypes.func.isRequired, - room: PropTypes.instanceOf(Room).isRequired, - }; - static contextType = MatrixClientContext; + private servers: React.ReactElement[]; + constructor(props) { super(props); const room = this.props.room; - const servers = new Set(); + const servers: Set = new Set(); room.currentState.getStateEvents("m.room.member").forEach(ev => servers.add(ev.getSender().split(":")[1])); this.servers = Array.from(servers).map(s =>
      -
      +
      ; @@ -667,7 +701,10 @@ const PHASE_MAP = { [PHASE_CANCELLED]: "cancelled", }; -function VerificationRequest({txnId, request}) { +const VerificationRequest: React.FC<{ + txnId: string; + request: VerificationRequest; +}> = ({txnId, request}) => { const [, updateState] = useState(); const [timeout, setRequestTimeout] = useState(request.timeout); @@ -704,7 +741,7 @@ function VerificationRequest({txnId, request}) {
      ); } -class VerificationExplorer extends React.Component { +class VerificationExplorer extends React.PureComponent { static getLabel() { return _t("Verification Requests"); } @@ -712,7 +749,7 @@ class VerificationExplorer extends React.Component { /* Ensure this.context is the cli */ static contextType = MatrixClientContext; - onNewRequest = () => { + private onNewRequest = () => { this.forceUpdate(); } @@ -738,14 +775,19 @@ class VerificationExplorer extends React.Component { , )}
      -
      +
      ); } } -class WidgetExplorer extends React.Component { +interface IWidgetExplorerState { + query: string; + editWidget?: IApp; +} + +class WidgetExplorer extends React.Component { static getLabel() { return _t("Active Widgets"); } @@ -759,19 +801,19 @@ class WidgetExplorer extends React.Component { }; } - onWidgetStoreUpdate = () => { + private onWidgetStoreUpdate = () => { this.forceUpdate(); }; - onQueryChange = (query) => { + private onQueryChange = (query: string) => { this.setState({query}); }; - onEditWidget = (widget) => { + private onEditWidget = (widget: IApp) => { this.setState({editWidget: widget}); }; - onBack = () => { + private onBack = () => { const widgets = WidgetStore.instance.getApps(this.props.room.roomId); if (this.state.editWidget && widgets.includes(this.state.editWidget)) { this.setState({editWidget: null}); @@ -794,13 +836,16 @@ class WidgetExplorer extends React.Component { const editWidget = this.state.editWidget; const widgets = WidgetStore.instance.getApps(room.roomId); if (editWidget && widgets.includes(editWidget)) { - const allState = Array.from(Array.from(room.currentState.events.values()).map(e => e.values())) - .reduce((p, c) => {p.push(...c); return p;}, []); + const allState = Array.from( + Array.from(room.currentState.events.values()).map((e: Map) => { + return e.values(); + }), + ).reduce((p, c) => { p.push(...c); return p; }, []); const stateEv = allState.find(ev => ev.getId() === editWidget.eventId); if (!stateEv) { // "should never happen" return
      {_t("There was an error finding this widget.")} -
      +
      ; @@ -829,14 +874,22 @@ class WidgetExplorer extends React.Component { })}
      -
      +
      ); } } -class SettingsExplorer extends React.Component { +interface ISettingsExplorerState { + query: string; + editSetting?: string; + viewSetting?: string; + explicitValues?: string; + explicitRoomValues?: string; + } + +class SettingsExplorer extends React.PureComponent { static getLabel() { return _t("Settings Explorer"); } @@ -854,19 +907,19 @@ class SettingsExplorer extends React.Component { }; } - onQueryChange = (ev) => { + private onQueryChange = (ev: ChangeEvent) => { this.setState({query: ev.target.value}); }; - onExplValuesEdit = (ev) => { + private onExplValuesEdit = (ev: ChangeEvent) => { this.setState({explicitValues: ev.target.value}); }; - onExplRoomValuesEdit = (ev) => { + private onExplRoomValuesEdit = (ev: ChangeEvent) => { this.setState({explicitRoomValues: ev.target.value}); }; - onBack = () => { + private onBack = () => { if (this.state.editSetting) { this.setState({editSetting: null}); } else if (this.state.viewSetting) { @@ -876,12 +929,12 @@ class SettingsExplorer extends React.Component { } }; - onViewClick = (ev, settingId) => { + private onViewClick = (ev: MouseEvent, settingId: string) => { ev.preventDefault(); this.setState({viewSetting: settingId}); }; - onEditClick = (ev, settingId) => { + private onEditClick = (ev: MouseEvent, settingId: string) => { ev.preventDefault(); this.setState({ editSetting: settingId, @@ -890,7 +943,7 @@ class SettingsExplorer extends React.Component { }); }; - onSaveClick = async () => { + private onSaveClick = async () => { try { const settingId = this.state.editSetting; const parsedExplicit = JSON.parse(this.state.explicitValues); @@ -899,7 +952,7 @@ class SettingsExplorer extends React.Component { console.log(`[Devtools] Setting value of ${settingId} at ${level} from user input`); try { const val = parsedExplicit[level]; - await SettingsStore.setValue(settingId, null, level, val); + await SettingsStore.setValue(settingId, null, level as SettingLevel, val); } catch (e) { console.warn(e); } @@ -909,7 +962,7 @@ class SettingsExplorer extends React.Component { console.log(`[Devtools] Setting value of ${settingId} at ${level} in ${roomId} from user input`); try { const val = parsedExplicitRoom[level]; - await SettingsStore.setValue(settingId, roomId, level, val); + await SettingsStore.setValue(settingId, roomId, level as SettingLevel, val); } catch (e) { console.warn(e); } @@ -926,7 +979,7 @@ class SettingsExplorer extends React.Component { } }; - renderSettingValue(val) { + private renderSettingValue(val: any): string { // Note: we don't .toString() a string because we want JSON.stringify to inject quotes for us const toStringTypes = ['boolean', 'number']; if (toStringTypes.includes(typeof(val))) { @@ -936,7 +989,7 @@ class SettingsExplorer extends React.Component { } } - renderExplicitSettingValues(setting, roomId) { + private renderExplicitSettingValues(setting: string, roomId: string): string { const vals = {}; for (const level of LEVEL_ORDER) { try { @@ -951,7 +1004,7 @@ class SettingsExplorer extends React.Component { return JSON.stringify(vals, null, 4); } - renderCanEditLevel(roomId, level) { + private renderCanEditLevel(roomId: string, level: SettingLevel): React.ReactNode { const canEdit = SettingsStore.canSetValue(this.state.editSetting, roomId, level); const className = canEdit ? 'mx_DevTools_SettingsExplorer_mutable' : 'mx_DevTools_SettingsExplorer_immutable'; return {canEdit.toString()}; @@ -1006,7 +1059,7 @@ class SettingsExplorer extends React.Component {
      -
      +
      @@ -1068,7 +1121,7 @@ class SettingsExplorer extends React.Component {
      -
      +
      @@ -1114,7 +1167,7 @@ class SettingsExplorer extends React.Component {
      -
      +
      @@ -1126,7 +1179,11 @@ class SettingsExplorer extends React.Component { } } -const Entries = [ +type DevtoolsDialogEntry = React.JSXElementConstructor & { + getLabel: () => string; +}; + +const Entries: DevtoolsDialogEntry[] = [ SendCustomEvent, RoomStateExplorer, SendAccountData, @@ -1137,43 +1194,36 @@ const Entries = [ SettingsExplorer, ]; -@replaceableComponent("views.dialogs.DevtoolsDialog") -export default class DevtoolsDialog extends React.PureComponent { - static propTypes = { - roomId: PropTypes.string.isRequired, - onFinished: PropTypes.func.isRequired, - }; +interface IProps { + roomId: string; + onFinished: (finished: boolean) => void; +} +interface IState { + mode?: DevtoolsDialogEntry; +} + +@replaceableComponent("views.dialogs.DevtoolsDialog") +export default class DevtoolsDialog extends React.PureComponent { constructor(props) { super(props); - this.onBack = this.onBack.bind(this); - this.onCancel = this.onCancel.bind(this); this.state = { mode: null, }; } - componentWillUnmount() { - this._unmounted = true; - } - - _setMode(mode) { + private setMode(mode: DevtoolsDialogEntry) { return () => { this.setState({ mode }); }; } - onBack() { - if (this.prevMode) { - this.setState({ mode: this.prevMode }); - this.prevMode = null; - } else { - this.setState({ mode: null }); - } + private onBack = () => { + this.setState({ mode: null }); } - onCancel() { + private onCancel = () => { this.props.onFinished(false); } @@ -1200,12 +1250,12 @@ export default class DevtoolsDialog extends React.PureComponent {
      { Entries.map((Entry) => { const label = Entry.getLabel(); - const onClick = this._setMode(Entry); + const onClick = this.setMode(Entry); return ; }) }
      -
      +
      ; From 229c4b98b44d14117a272c817ffb95ed622ec15d Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Thu, 20 May 2021 18:01:38 +0100 Subject: [PATCH 120/999] use userGroups cached value to avoid re-render --- src/components/views/elements/Flair.js | 2 +- .../views/messages/SenderProfile.js | 35 ++++++++++++------- src/stores/FlairStore.js | 4 +++ 3 files changed, 28 insertions(+), 13 deletions(-) diff --git a/src/components/views/elements/Flair.js b/src/components/views/elements/Flair.js index 73d5b91511..23858b860d 100644 --- a/src/components/views/elements/Flair.js +++ b/src/components/views/elements/Flair.js @@ -116,7 +116,7 @@ export default class Flair extends React.Component { render() { if (this.state.profiles.length === 0) { - return ; + return null; } const avatars = this.state.profiles.map((profile, index) => { return ; diff --git a/src/components/views/messages/SenderProfile.js b/src/components/views/messages/SenderProfile.js index f1855de99e..8f10954370 100644 --- a/src/components/views/messages/SenderProfile.js +++ b/src/components/views/messages/SenderProfile.js @@ -31,21 +31,23 @@ export default class SenderProfile extends React.Component { static contextType = MatrixClientContext; - state = { - userGroups: null, - relatedGroups: [], - }; + constructor(props) { + super(props); + const senderId = this.props.mxEvent.getSender(); + this.state = { + userGroups: FlairStore.cachedPublicisedGroups(senderId) || [], + relatedGroups: [], + }; + } componentDidMount() { this.unmounted = false; this._updateRelatedGroups(); - FlairStore.getPublicisedGroupsCached( - this.context, this.props.mxEvent.getSender(), - ).then((userGroups) => { - if (this.unmounted) return; - this.setState({userGroups}); - }); + if (this.state.userGroups.length === 0) { + this.getPublicisedGroups(); + } + this.context.on('RoomState.events', this.onRoomStateEvents); } @@ -55,6 +57,15 @@ export default class SenderProfile extends React.Component { this.context.removeListener('RoomState.events', this.onRoomStateEvents); } + async getPublicisedGroups() { + if (!this.unmounted) { + const userGroups = await FlairStore.getPublicisedGroupsCached( + this.context, this.props.mxEvent.getSender(), + ); + this.setState({userGroups}); + } + } + onRoomStateEvents = event => { if (event.getType() === 'm.room.related_groups' && event.getRoomId() === this.props.mxEvent.getRoomId() @@ -93,10 +104,10 @@ export default class SenderProfile extends React.Component { const {msgtype} = mxEvent.getContent(); if (msgtype === 'm.emote') { - return ; // emote message must include the name so don't duplicate it + return null; // emote message must include the name so don't duplicate it } - let flair =
      ; + let flair = null; if (this.props.enableFlair) { const displayedGroups = this._getDisplayedGroups( this.state.userGroups, this.state.relatedGroups, diff --git a/src/stores/FlairStore.js b/src/stores/FlairStore.js index 53d07d0452..23254b98ab 100644 --- a/src/stores/FlairStore.js +++ b/src/stores/FlairStore.js @@ -65,6 +65,10 @@ class FlairStore extends EventEmitter { delete this._userGroups[userId]; } + cachedPublicisedGroups(userId) { + return this._userGroups[userId]; + } + getPublicisedGroupsCached(matrixClient, userId) { if (this._userGroups[userId]) { return Promise.resolve(this._userGroups[userId]); From 0f63098c59674623b754e2b74a22b58cf985d443 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Thu, 20 May 2021 18:02:44 +0100 Subject: [PATCH 121/999] Remove typo semicolon --- src/components/views/rooms/EventTile.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index c85945be7a..82d97bff98 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -709,7 +709,7 @@ export default class EventTile extends React.Component { { remText } { avatars } - ; +
      ) } From 431b4607a4c7489e91f4886c6f5b8d8e9beeb855 Mon Sep 17 00:00:00 2001 From: c-cal Date: Thu, 20 May 2021 15:07:41 +0000 Subject: [PATCH 122/999] Translated using Weblate (French) Currently translated at 99.7% (2966 of 2974 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/fr/ --- src/i18n/strings/fr.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index dc8b701e35..6b2ee03773 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -936,7 +936,7 @@ "Failed to load group members": "Échec du chargement des membres du groupe", "Failed to invite users to the room:": "Échec de l’invitation d'utilisateurs dans le salon :", "There was an error joining the room": "Une erreur est survenue en rejoignant le salon", - "You do not have permission to invite people to this room.": "Vous n’avez pas la permission d’envoyer des invitations dans ce salon.", + "You do not have permission to invite people to this room.": "Vous n'avez pas la permission d'inviter des personnes dans ce salon.", "User %(user_id)s does not exist": "L’utilisateur %(user_id)s n’existe pas", "Unknown server error": "Erreur de serveur inconnue", "Show a reminder to enable Secure Message Recovery in encrypted rooms": "Afficher un rappel pour activer la récupération de messages sécurisée dans les salons chiffrés", From 47e007e08f9bedaf47cf59a63c9bd04219195d76 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Fri, 21 May 2021 10:20:24 +0100 Subject: [PATCH 123/999] batch load events in ReplyThread before adding them to the state --- src/components/views/elements/ReplyThread.js | 42 +++++++++----------- 1 file changed, 19 insertions(+), 23 deletions(-) diff --git a/src/components/views/elements/ReplyThread.js b/src/components/views/elements/ReplyThread.js index c336f34b51..bbced5328f 100644 --- a/src/components/views/elements/ReplyThread.js +++ b/src/components/views/elements/ReplyThread.js @@ -269,36 +269,27 @@ export default class ReplyThread extends React.Component { const {parentEv} = this.props; // at time of making this component we checked that props.parentEv has a parentEventId const ev = await this.getEvent(ReplyThread.getParentEventId(parentEv)); + if (this.unmounted) return; if (ev) { + const loadedEv = await this.getNextEvent(ev); this.setState({ events: [ev], - }, this.loadNextEvent); + loadedEv, + loading: false, + }); } else { this.setState({err: true}); } } - async loadNextEvent() { - if (this.unmounted) return; - const ev = this.state.events[0]; - const inReplyToEventId = ReplyThread.getParentEventId(ev); - - if (!inReplyToEventId) { - this.setState({ - loading: false, - }); - return; - } - - const loadedEv = await this.getEvent(inReplyToEventId); - if (this.unmounted) return; - - if (loadedEv) { - this.setState({loadedEv}); - } else { - this.setState({err: true}); + async getNextEvent(ev) { + try { + const inReplyToEventId = ReplyThread.getParentEventId(ev); + return await this.getEvent(inReplyToEventId); + } catch (e) { + return null; } } @@ -326,13 +317,18 @@ export default class ReplyThread extends React.Component { this.initialize(); } - onQuoteClick() { + async onQuoteClick() { const events = [this.state.loadedEv, ...this.state.events]; + let loadedEv = null; + if (events.length > 0) { + loadedEv = await this.getNextEvent(events[0]); + } + this.setState({ - loadedEv: null, + loadedEv, events, - }, this.loadNextEvent); + }); dis.fire(Action.FocusComposer); } From 5ba419db54476ea8a268e3816401c68c1745ee75 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Fri, 21 May 2021 10:21:54 +0100 Subject: [PATCH 124/999] split room header and header wrapper --- src/components/views/rooms/RoomHeader.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js index a527d7625c..6d3b50c10d 100644 --- a/src/components/views/rooms/RoomHeader.js +++ b/src/components/views/rooms/RoomHeader.js @@ -257,14 +257,16 @@ export default class RoomHeader extends React.Component { const e2eIcon = this.props.e2eStatus ? : undefined; return ( -
      -
      { roomAvatar }
      -
      { e2eIcon }
      - { name } - { topicElement } - { cancelButton } - { rightRow } - +
      +
      +
      { roomAvatar }
      +
      { e2eIcon }
      + { name } + { topicElement } + { cancelButton } + { rightRow } + +
      ); } From d0da4b2a2578688dc4892ecd68f0f6c0c9317e90 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 21 May 2021 12:37:34 +0100 Subject: [PATCH 125/999] Use separate name for verification request component --- src/components/views/dialogs/DevtoolsDialog.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/dialogs/DevtoolsDialog.tsx b/src/components/views/dialogs/DevtoolsDialog.tsx index 81d3a77327..c4be186da1 100644 --- a/src/components/views/dialogs/DevtoolsDialog.tsx +++ b/src/components/views/dialogs/DevtoolsDialog.tsx @@ -701,7 +701,7 @@ const PHASE_MAP = { [PHASE_CANCELLED]: "cancelled", }; -const VerificationRequest: React.FC<{ +const VerificationRequestExplorer: React.FC<{ txnId: string; request: VerificationRequest; }> = ({txnId, request}) => { @@ -772,7 +772,7 @@ class VerificationExplorer extends React.PureComponent { return (
      {Array.from(inRoomRequests.entries()).reverse().map(([txnId, request]) => - , + , )}
      From d59b2b357936d4b66595eaea2833996ab81fbd79 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 21 May 2021 12:38:32 +0100 Subject: [PATCH 126/999] Fix unintended buttons class change --- .../views/dialogs/DevtoolsDialog.tsx | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/components/views/dialogs/DevtoolsDialog.tsx b/src/components/views/dialogs/DevtoolsDialog.tsx index c4be186da1..7df57b030f 100644 --- a/src/components/views/dialogs/DevtoolsDialog.tsx +++ b/src/components/views/dialogs/DevtoolsDialog.tsx @@ -73,7 +73,7 @@ abstract class GenericEditor< protected abstract send(); protected buttons(): React.ReactNode { - return
      + return
      { !this.state.message && }
      ; @@ -184,7 +184,7 @@ export class SendCustomEvent extends GenericEditor
      -
      +
      { !this.state.message && } { showTglFlip &&
      @@ -284,7 +284,7 @@ class SendAccountData extends GenericEditor
      -
      +
      { !this.state.message && } { !this.state.message &&
      @@ -472,7 +472,7 @@ class RoomStateExplorer extends React.PureComponent
      -
      +
      @@ -517,7 +517,7 @@ class RoomStateExplorer extends React.PureComponent { list }
      -
      +
      ; @@ -603,7 +603,7 @@ class AccountDataExplorer extends React.PureComponent
      -
      +
      @@ -628,7 +628,7 @@ class AccountDataExplorer extends React.PureComponent
      -
      +
      -
      +
      ; @@ -775,7 +775,7 @@ class VerificationExplorer extends React.PureComponent { , )}
      -
      +
      ); @@ -845,7 +845,7 @@ class WidgetExplorer extends React.Component {_t("There was an error finding this widget.")} -
      +
      ; @@ -874,7 +874,7 @@ class WidgetExplorer extends React.Component
      -
      +
      ); @@ -1059,7 +1059,7 @@ class SettingsExplorer extends React.PureComponent
      -
      +
      @@ -1121,7 +1121,7 @@ class SettingsExplorer extends React.PureComponent
      -
      +
      @@ -1167,7 +1167,7 @@ class SettingsExplorer extends React.PureComponent
      -
      +
      @@ -1255,7 +1255,7 @@ export default class DevtoolsDialog extends React.PureComponent }) }
      -
      +
      ; From f8e61a982b6399f5c2133cf31f5f8d0d01ab0611 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 21 May 2021 12:41:59 +0100 Subject: [PATCH 127/999] One less Set --- src/components/views/dialogs/DevtoolsDialog.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/dialogs/DevtoolsDialog.tsx b/src/components/views/dialogs/DevtoolsDialog.tsx index 7df57b030f..0ea77cc9e8 100644 --- a/src/components/views/dialogs/DevtoolsDialog.tsx +++ b/src/components/views/dialogs/DevtoolsDialog.tsx @@ -662,7 +662,7 @@ class ServersInRoomList extends React.PureComponent = new Set(); + const servers = new Set(); room.currentState.getStateEvents("m.room.member").forEach(ev => servers.add(ev.getSender().split(":")[1])); this.servers = Array.from(servers).map(s =>
      ); } diff --git a/src/components/views/elements/TooltipButton.js b/src/components/views/elements/TooltipButton.tsx similarity index 90% rename from src/components/views/elements/TooltipButton.js rename to src/components/views/elements/TooltipButton.tsx index c5ebb3b1aa..1232f48695 100644 --- a/src/components/views/elements/TooltipButton.js +++ b/src/components/views/elements/TooltipButton.tsx @@ -19,8 +19,16 @@ import React from 'react'; import * as sdk from '../../../index'; import {replaceableComponent} from "../../../utils/replaceableComponent"; +interface IProps { + helpText: string; +} + +interface IState { + hover: boolean; +} + @replaceableComponent("views.elements.TooltipButton") -export default class TooltipButton extends React.Component { +export default class TooltipButton extends React.Component { state = { hover: false, }; From 36d95ff73799a7dcfc1135a626d4e7b4030c13a5 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 24 May 2021 15:02:26 +0100 Subject: [PATCH 193/999] Display spinner in user menu when joining a room --- src/components/structures/UserMenu.tsx | 58 ++++++++++++++++++++++---- src/i18n/strings/en_EN.json | 2 + 2 files changed, 53 insertions(+), 7 deletions(-) diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx index 65861624e6..c05f74a436 100644 --- a/src/components/structures/UserMenu.tsx +++ b/src/components/structures/UserMenu.tsx @@ -57,7 +57,8 @@ import { IHostSignupConfig } from "../views/dialogs/HostSignupDialogTypes"; import SpaceStore, { UPDATE_SELECTED_SPACE } from "../../stores/SpaceStore"; import RoomName from "../views/elements/RoomName"; import {replaceableComponent} from "../../utils/replaceableComponent"; - +import InlineSpinner from "../views/elements/InlineSpinner"; +import TooltipButton from "../views/elements/TooltipButton"; interface IProps { isMinimized: boolean; } @@ -68,6 +69,7 @@ interface IState { contextMenuPosition: PartialDOMRect; isDarkTheme: boolean; selectedSpace?: Room; + pendingRoomJoin: string[] } @replaceableComponent("structures.UserMenu") @@ -84,6 +86,7 @@ export default class UserMenu extends React.Component { this.state = { contextMenuPosition: null, isDarkTheme: this.isUserOnDarkTheme(), + pendingRoomJoin: [], }; OwnProfileStore.instance.on(UPDATE_EVENT, this.onProfileUpdate); @@ -147,15 +150,48 @@ export default class UserMenu extends React.Component { }; private onAction = (ev: ActionPayload) => { - if (ev.action !== Action.ToggleUserMenu) return; // not interested - - if (this.state.contextMenuPosition) { - this.setState({contextMenuPosition: null}); - } else { - if (this.buttonRef.current) this.buttonRef.current.click(); + switch (ev.action) { + case Action.ToggleUserMenu: + if (this.state.contextMenuPosition) { + this.setState({contextMenuPosition: null}); + } else { + if (this.buttonRef.current) this.buttonRef.current.click(); + } + break; + case Action.JoinRoom: + this.addPendingJoinRoom(ev.roomId); + break; + case Action.JoinRoomReady: + case Action.JoinRoomError: + this.removePendingJoinRoom(ev.roomId); + break; } }; + private addPendingJoinRoom(roomId) { + this.setState({ + pendingRoomJoin: [ + ...this.state.pendingRoomJoin, + roomId, + ], + }); + } + + private removePendingJoinRoom(roomId) { + const newPendingRoomJoin = this.state.pendingRoomJoin.filter(pendingJoinRoomId => { + return pendingJoinRoomId !== roomId; + }); + if (newPendingRoomJoin.length !== this.state.pendingRoomJoin.length) { + this.setState({ + pendingRoomJoin: newPendingRoomJoin, + }) + } + } + + get hasPendingActions(): boolean { + return this.state.pendingRoomJoin.length > 0; + } + private onOpenMenuClick = (ev: React.MouseEvent) => { ev.preventDefault(); ev.stopPropagation(); @@ -617,6 +653,14 @@ export default class UserMenu extends React.Component { /> {name} + {this.hasPendingActions && ( + + + + )} {dnd} {buttons}
      diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 7ceb039822..5aba8d998d 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2753,6 +2753,8 @@ "Switch theme": "Switch theme", "User menu": "User menu", "Community and user menu": "Community and user menu", + "Currently joining %(count)s rooms|one": "Currently joining %(count)s room", + "Currently joining %(count)s rooms|other": "Currently joining %(count)s rooms", "Could not load user profile": "Could not load user profile", "Decrypted event source": "Decrypted event source", "Original event source": "Original event source", From f478cd98f7c0faf94e31ef277a09ec0068d7e34c Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 24 May 2021 15:12:17 +0100 Subject: [PATCH 194/999] fix i18n for UserMenu spinner --- src/i18n/strings/en_EN.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 5aba8d998d..1b04ae3b89 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2753,8 +2753,8 @@ "Switch theme": "Switch theme", "User menu": "User menu", "Community and user menu": "Community and user menu", - "Currently joining %(count)s rooms|one": "Currently joining %(count)s room", "Currently joining %(count)s rooms|other": "Currently joining %(count)s rooms", + "Currently joining %(count)s rooms|one": "Currently joining %(count)s room", "Could not load user profile": "Could not load user profile", "Decrypted event source": "Decrypted event source", "Original event source": "Original event source", From 671f1694579253afcc4a6ea8e53bb11fb11f768c Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 24 May 2021 16:08:48 +0100 Subject: [PATCH 195/999] Remove unused middlePanelResized event listener --- src/components/views/rooms/RoomTile.tsx | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/src/components/views/rooms/RoomTile.tsx b/src/components/views/rooms/RoomTile.tsx index 60368ce250..3ed040c173 100644 --- a/src/components/views/rooms/RoomTile.tsx +++ b/src/components/views/rooms/RoomTile.tsx @@ -54,6 +54,7 @@ import { replaceableComponent } from "../../../utils/replaceableComponent"; import { getUnsentMessages } from "../../structures/RoomStatusBar"; import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState"; import { ResizeNotifier } from "../../../utils/ResizeNotifier"; +import { checkObjectHasNoAdditionalKeys } from "matrix-js-sdk/src/utils"; interface IProps { room: Room; @@ -106,9 +107,6 @@ export default class RoomTile extends React.PureComponent { this.notificationState = RoomNotificationStateStore.instance.getRoomState(this.props.room); this.roomProps = EchoChamber.forRoom(this.props.room); - if (this.props.resizeNotifier) { - this.props.resizeNotifier.on("middlePanelResized", this.onResize); - } } private countUnsentEvents(): number { @@ -123,12 +121,6 @@ export default class RoomTile extends React.PureComponent { this.forceUpdate(); // notification state changed - update }; - private onResize = () => { - if (this.showMessagePreview && !this.state.messagePreview) { - this.generatePreview(); - } - }; - private onLocalEchoUpdated = (ev: MatrixEvent, room: Room) => { if (!room?.roomId === this.props.room.roomId) return; this.setState({hasUnsentEvents: this.countUnsentEvents() > 0}); @@ -148,7 +140,9 @@ export default class RoomTile extends React.PureComponent { } public componentDidUpdate(prevProps: Readonly, prevState: Readonly) { - if (prevProps.showMessagePreview !== this.props.showMessagePreview && this.showMessagePreview) { + const showMessageChanged = prevProps.showMessagePreview !== this.props.showMessagePreview; + const minimizedChanged = prevProps.isMinimized !== this.props.isMinimized; + if (showMessageChanged || minimizedChanged) { this.generatePreview(); } if (prevProps.room?.roomId !== this.props.room?.roomId) { @@ -208,9 +202,6 @@ export default class RoomTile extends React.PureComponent { ); this.props.room.off("Room.name", this.onRoomNameUpdate); } - if (this.props.resizeNotifier) { - this.props.resizeNotifier.off("middlePanelResized", this.onResize); - } ActiveRoomObserver.removeListener(this.props.room.roomId, this.onActiveRoomUpdate); defaultDispatcher.unregister(this.dispatcherRef); this.notificationState.off(NOTIFICATION_STATE_UPDATE, this.onNotificationUpdate); From a0b052d3dea81143a6d766583d7fc28b8a31721b Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Mon, 24 May 2021 11:11:45 -0400 Subject: [PATCH 196/999] bump to olm 3.2.3 --- package.json | 2 +- yarn.lock | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index aaad80d068..21c1ffb6ea 100644 --- a/package.json +++ b/package.json @@ -161,7 +161,7 @@ "matrix-mock-request": "^1.2.3", "matrix-react-test-utils": "^0.2.2", "matrix-web-i18n": "github:matrix-org/matrix-web-i18n", - "@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.2.tgz", + "@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz", "react-test-renderer": "^16.14.0", "rimraf": "^3.0.2", "stylelint": "^13.9.0", diff --git a/yarn.lock b/yarn.lock index 041bb8b4b9..5b42436039 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1325,9 +1325,9 @@ "@types/yargs" "^15.0.0" chalk "^4.0.0" -"@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.2.tgz": - version "3.2.2" - resolved "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.2.tgz#5e3d784461ca3bbeb791ac8f3c175375aeb81318" +"@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" + 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": version "2.1.8-no-fsevents" From 0bbfb1a6d953344083fb7554a303ee38039adc7d Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 24 May 2021 16:18:55 +0100 Subject: [PATCH 197/999] remove unused variable checkObjectHasNoAdditionalKeys --- src/components/views/rooms/RoomTile.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/views/rooms/RoomTile.tsx b/src/components/views/rooms/RoomTile.tsx index 3ed040c173..579a275155 100644 --- a/src/components/views/rooms/RoomTile.tsx +++ b/src/components/views/rooms/RoomTile.tsx @@ -54,7 +54,6 @@ import { replaceableComponent } from "../../../utils/replaceableComponent"; import { getUnsentMessages } from "../../structures/RoomStatusBar"; import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState"; import { ResizeNotifier } from "../../../utils/ResizeNotifier"; -import { checkObjectHasNoAdditionalKeys } from "matrix-js-sdk/src/utils"; interface IProps { room: Room; From 73221e4d08cb5e7fe71b7df59223d232d403bae3 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Fri, 21 May 2021 16:09:28 -0400 Subject: [PATCH 198/999] Bump libolm version. --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 8ecebc4eb3..ba4c8bc98c 100644 --- a/package.json +++ b/package.json @@ -161,7 +161,7 @@ "matrix-mock-request": "^1.2.3", "matrix-react-test-utils": "^0.2.2", "matrix-web-i18n": "github:matrix-org/matrix-web-i18n", - "olm": "https://packages.matrix.org/npm/olm/olm-3.2.1.tgz", + "@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.2.tgz", "react-test-renderer": "^16.14.0", "rimraf": "^3.0.2", "stylelint": "^13.9.0", diff --git a/yarn.lock b/yarn.lock index 0472555dd9..a32b81af4b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1325,6 +1325,10 @@ "@types/yargs" "^15.0.0" chalk "^4.0.0" +"@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.2.tgz": + version "3.2.2" + resolved "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.2.tgz#5e3d784461ca3bbeb791ac8f3c175375aeb81318" + "@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents": version "2.1.8-no-fsevents" resolved "https://registry.yarnpkg.com/@nicolo-ribaudo/chokidar-2/-/chokidar-2-2.1.8-no-fsevents.tgz#da7c3996b8e6e19ebd14d82eaced2313e7769f9b" @@ -6145,10 +6149,6 @@ object.values@^1.1.1, object.values@^1.1.2: es-abstract "^1.18.0-next.1" has "^1.0.3" -"olm@https://packages.matrix.org/npm/olm/olm-3.2.1.tgz": - version "3.2.1" - resolved "https://packages.matrix.org/npm/olm/olm-3.2.1.tgz#d623d76f99c3518dde68be8c86618d68bc7b004a" - once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" From 140258d3d6f53117957ce903fbcd078729a4d2f7 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Mon, 24 May 2021 11:11:45 -0400 Subject: [PATCH 199/999] bump to olm 3.2.3 --- package.json | 2 +- yarn.lock | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index ba4c8bc98c..a8d29eca48 100644 --- a/package.json +++ b/package.json @@ -161,7 +161,7 @@ "matrix-mock-request": "^1.2.3", "matrix-react-test-utils": "^0.2.2", "matrix-web-i18n": "github:matrix-org/matrix-web-i18n", - "@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.2.tgz", + "@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz", "react-test-renderer": "^16.14.0", "rimraf": "^3.0.2", "stylelint": "^13.9.0", diff --git a/yarn.lock b/yarn.lock index a32b81af4b..fe106d7e7a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1325,9 +1325,9 @@ "@types/yargs" "^15.0.0" chalk "^4.0.0" -"@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.2.tgz": - version "3.2.2" - resolved "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.2.tgz#5e3d784461ca3bbeb791ac8f3c175375aeb81318" +"@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" + 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": version "2.1.8-no-fsevents" From fdc22bfdf747e618dd510c8a257f74ffd8dcca6d Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 24 May 2021 16:40:55 +0100 Subject: [PATCH 200/999] Adhere to TypeScript codestyle better --- src/components/views/elements/TooltipButton.tsx | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/components/views/elements/TooltipButton.tsx b/src/components/views/elements/TooltipButton.tsx index 1232f48695..191018cc19 100644 --- a/src/components/views/elements/TooltipButton.tsx +++ b/src/components/views/elements/TooltipButton.tsx @@ -29,17 +29,20 @@ interface IState { @replaceableComponent("views.elements.TooltipButton") export default class TooltipButton extends React.Component { - state = { - hover: false, - }; + constructor(props) { + super(props); + this.state = { + hover: false, + }; + } - onMouseOver = () => { + private onMouseOver = () => { this.setState({ hover: true, }); }; - onMouseLeave = () => { + private onMouseLeave = () => { this.setState({ hover: false, }); From 3b69c0203c1fba42249b16110db3c7fd976465d2 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 24 May 2021 17:05:59 +0100 Subject: [PATCH 201/999] Remove resize notifier prop from RoomTile --- src/components/views/rooms/RoomTile.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/views/rooms/RoomTile.tsx b/src/components/views/rooms/RoomTile.tsx index 579a275155..aae182eca4 100644 --- a/src/components/views/rooms/RoomTile.tsx +++ b/src/components/views/rooms/RoomTile.tsx @@ -53,14 +53,12 @@ import { CommunityPrototypeStore, IRoomProfile } from "../../../stores/Community import { replaceableComponent } from "../../../utils/replaceableComponent"; import { getUnsentMessages } from "../../structures/RoomStatusBar"; import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState"; -import { ResizeNotifier } from "../../../utils/ResizeNotifier"; interface IProps { room: Room; showMessagePreview: boolean; isMinimized: boolean; tag: TagID; - resizeNotifier: ResizeNotifier; } type PartialDOMRect = Pick; From ad30e89967e4abc4a3fbab53b17c589f8ad97d0b Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Mon, 24 May 2021 17:15:16 +0100 Subject: [PATCH 202/999] Upgrade matrix-js-sdk to 11.1.0 --- package.json | 4 ++-- yarn.lock | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index a8d29eca48..dff6afa609 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,7 @@ "katex": "^0.12.0", "linkifyjs": "^2.1.9", "lodash": "^4.17.20", - "matrix-js-sdk": "11.1.0-rc.1", + "matrix-js-sdk": "11.1.0", "matrix-widget-api": "^0.1.0-beta.14", "minimist": "^1.2.5", "opus-recorder": "^8.0.3", @@ -121,6 +121,7 @@ "@babel/preset-typescript": "^7.12.7", "@babel/register": "^7.12.10", "@babel/traverse": "^7.12.12", + "@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz", "@peculiar/webcrypto": "^1.1.4", "@sinonjs/fake-timers": "^7.0.2", "@types/classnames": "^2.2.11", @@ -161,7 +162,6 @@ "matrix-mock-request": "^1.2.3", "matrix-react-test-utils": "^0.2.2", "matrix-web-i18n": "github:matrix-org/matrix-web-i18n", - "@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz", "react-test-renderer": "^16.14.0", "rimraf": "^3.0.2", "stylelint": "^13.9.0", diff --git a/yarn.lock b/yarn.lock index fe106d7e7a..ff7611da84 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5673,10 +5673,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@11.1.0-rc.1: - version "11.1.0-rc.1" - resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-11.1.0-rc.1.tgz#98da580fa51bc8c70c57984e79dc63c617a671d1" - integrity sha512-yyZeL1mHttw+EnZcGfMH+wd33s0+ZKB+KyXHA3QiqiVRWLgANjIY9QCNjbJYa9FK8zZRqO2yHiFLtTAAU379Ag== +matrix-js-sdk@11.1.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-11.1.0.tgz#59119f9fe76adbc38b309947c5532baea8499bf1" + integrity sha512-yBvSGb33MDz9mfbjtVGO7557kgtY/kJcrFyhtN7LwSyi/TDhhYleq5xAqsi7MJrmIb/E0JIF10JIwlF9dAW64Q== dependencies: "@babel/runtime" "^7.12.5" another-json "^0.2.0" From d0e41c6461738773c261574dd1da2b1b98d0ff3a Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Mon, 24 May 2021 17:20:30 +0100 Subject: [PATCH 203/999] Prepare changelog for v3.22.0 --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b4ec8b457..f3d9afd51d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +Changes in [3.22.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.22.0) (2021-05-24) +===================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.22.0-rc.1...v3.22.0) + + * Upgrade to JS SDK 11.1.0 + * [Release] Bump libolm version + [\#6087](https://github.com/matrix-org/matrix-react-sdk/pull/6087) + Changes in [3.22.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.22.0-rc.1) (2021-05-19) =============================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.21.0...v3.22.0-rc.1) From fbc01d4930dfd4c9376041ac6b5ef40a038aeb4d Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Mon, 24 May 2021 17:20:31 +0100 Subject: [PATCH 204/999] v3.22.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index dff6afa609..5f07c0c0a4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "3.22.0-rc.1", + "version": "3.22.0", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { From f8ef2d42f5966c06a0ac3a392023bec17bb6fb1c Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Mon, 24 May 2021 17:25:58 +0100 Subject: [PATCH 205/999] Resetting package fields for development --- package.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 5f07c0c0a4..e26ce17f42 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", @@ -200,6 +200,5 @@ "coverageReporters": [ "text" ] - }, - "typings": "./lib/index.d.ts" + } } From 7123abc122aa08b51c7847b06d6e3a623b0c5d1e Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Mon, 24 May 2021 17:26:24 +0100 Subject: [PATCH 206/999] Reset matrix-js-sdk back to develop branch --- package.json | 2 +- yarn.lock | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index e26ce17f42..13047b69cf 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,7 @@ "katex": "^0.12.0", "linkifyjs": "^2.1.9", "lodash": "^4.17.20", - "matrix-js-sdk": "11.1.0", + "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", "matrix-widget-api": "^0.1.0-beta.14", "minimist": "^1.2.5", "opus-recorder": "^8.0.3", diff --git a/yarn.lock b/yarn.lock index ff7611da84..35aac66e26 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5673,10 +5673,9 @@ 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@11.1.0: +"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop": version "11.1.0" - resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-11.1.0.tgz#59119f9fe76adbc38b309947c5532baea8499bf1" - integrity sha512-yBvSGb33MDz9mfbjtVGO7557kgtY/kJcrFyhtN7LwSyi/TDhhYleq5xAqsi7MJrmIb/E0JIF10JIwlF9dAW64Q== + resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/acb9bc8cc5234326a7583514a8e120a4ac42eedc" dependencies: "@babel/runtime" "^7.12.5" another-json "^0.2.0" From cdecc156df1800179a619e2a8063295ece3d63b0 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 24 May 2021 17:30:37 +0100 Subject: [PATCH 207/999] Remove unused prop --- src/components/views/rooms/RoomSublist.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/views/rooms/RoomSublist.tsx b/src/components/views/rooms/RoomSublist.tsx index f9881d33ae..8a2059a247 100644 --- a/src/components/views/rooms/RoomSublist.tsx +++ b/src/components/views/rooms/RoomSublist.tsx @@ -530,7 +530,6 @@ export default class RoomSublist extends React.Component { tiles.push( Date: Mon, 24 May 2021 18:08:43 +0100 Subject: [PATCH 208/999] Add support to keyboard shortcuts dialog for [digits] --- src/accessibility/KeyboardShortcuts.tsx | 3 +++ src/i18n/strings/en_EN.json | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/accessibility/KeyboardShortcuts.tsx b/src/accessibility/KeyboardShortcuts.tsx index 2a3e576e31..1cd5408210 100644 --- a/src/accessibility/KeyboardShortcuts.tsx +++ b/src/accessibility/KeyboardShortcuts.tsx @@ -57,6 +57,8 @@ export enum Modifiers { // Meta-modifier: isMac ? CMD : CONTROL export const CMD_OR_CTRL = isMac ? Modifiers.COMMAND : Modifiers.CONTROL; +// Meta-key representing the digits [0-9] often found at the top of standard keyboard layouts +export const DIGITS = "digits"; interface IKeybind { modifiers?: Modifiers[]; @@ -319,6 +321,7 @@ const alternateKeyName: Record = { [Key.SPACE]: _td("Space"), [Key.HOME]: _td("Home"), [Key.END]: _td("End"), + [DIGITS]: _td("[number]"), }; const keyIcon: Record = { [Key.ARROW_UP]: "↑", diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 7ceb039822..251c70e241 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2973,5 +2973,6 @@ "Esc": "Esc", "Enter": "Enter", "Space": "Space", - "End": "End" + "End": "End", + "[number]": "[number]" } From 63ae84a72ed441853bb0c95857afc755eefe8486 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 24 May 2021 18:23:04 +0100 Subject: [PATCH 209/999] Wire space switch shortcut via dispatcher to prevent app load explosion due to skinning --- src/dispatcher/actions.ts | 5 ++++ src/dispatcher/payloads/SwitchSpacePayload.ts | 27 +++++++++++++++++++ src/stores/SpaceStore.tsx | 7 +++++ 3 files changed, 39 insertions(+) create mode 100644 src/dispatcher/payloads/SwitchSpacePayload.ts diff --git a/src/dispatcher/actions.ts b/src/dispatcher/actions.ts index cd32c3743f..79e1edeee9 100644 --- a/src/dispatcher/actions.ts +++ b/src/dispatcher/actions.ts @@ -138,4 +138,9 @@ export enum Action { * Fired when an upload is cancelled by the user. Should be used with UploadCanceledPayload. */ UploadCanceled = "upload_canceled", + + /** + * Switches space. Should be used with SwitchSpacePayload. + */ + SwitchSpace = "switch_space", } diff --git a/src/dispatcher/payloads/SwitchSpacePayload.ts b/src/dispatcher/payloads/SwitchSpacePayload.ts new file mode 100644 index 0000000000..04eb744334 --- /dev/null +++ b/src/dispatcher/payloads/SwitchSpacePayload.ts @@ -0,0 +1,27 @@ +/* +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 { ActionPayload } from "../payloads"; +import { Action } from "../actions"; + +export interface SwitchSpacePayload extends ActionPayload { + action: Action.SwitchSpace; + + /** + * The number of the space to switch to, 1-indexed, 0 is Home. + */ + num: number; +} diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index 40997d30a8..0d09357fc1 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -33,6 +33,7 @@ import {EnhancedMap, mapDiff} from "../utils/maps"; import {setHasDiff} from "../utils/sets"; import {ISpaceSummaryEvent, ISpaceSummaryRoom} from "../components/structures/SpaceRoomDirectory"; import RoomViewStore from "./RoomViewStore"; +import {Action} from "../dispatcher/actions"; interface IState {} @@ -565,6 +566,12 @@ export class SpaceStoreClass extends AsyncStoreWithClient { this.setActiveSpace(null, false); } break; + case Action.SwitchSpace: + if (payload.num === 0) { + this.setActiveSpace(null); + } else if (this.spacePanelSpaces.length >= payload.num) { + this.setActiveSpace(this.spacePanelSpaces[payload.num - 1]); + } } } From 5b66975605bc5ef14828e7f4c2f4c6e2d3d17344 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 24 May 2021 18:57:24 +0100 Subject: [PATCH 210/999] Use local room state to render space hierarchy if the room is known --- .../structures/SpaceRoomDirectory.tsx | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/src/components/structures/SpaceRoomDirectory.tsx b/src/components/structures/SpaceRoomDirectory.tsx index dde8dd8331..3f1679c97e 100644 --- a/src/components/structures/SpaceRoomDirectory.tsx +++ b/src/components/structures/SpaceRoomDirectory.tsx @@ -101,15 +101,13 @@ const Tile: React.FC = ({ numChildRooms, children, }) => { - const name = room.name || room.canonical_alias || room.aliases?.[0] + const cli = MatrixClientPeg.get(); + const joinedRoom = cli.getRoom(room.room_id)?.getMyMembership() === "join" && cli.getRoom(room.room_id); + const name = joinedRoom?.name || room.name || room.canonical_alias || room.aliases?.[0] || (room.room_type === RoomType.Space ? _t("Unnamed Space") : _t("Unnamed Room")); const [showChildren, toggleShowChildren] = useStateToggle(true); - const cli = MatrixClientPeg.get(); - const cliRoom = cli.getRoom(room.room_id); - const myMembership = cliRoom?.getMyMembership(); - const onPreviewClick = (ev: ButtonEvent) => { ev.preventDefault(); ev.stopPropagation(); @@ -122,7 +120,7 @@ const Tile: React.FC = ({ } let button; - if (myMembership === "join") { + if (joinedRoom) { button = { _t("View") } ; @@ -146,17 +144,27 @@ const Tile: React.FC = ({ } } - let url: string; - if (room.avatar_url) { - url = mediaFromMxc(room.avatar_url).getSquareThumbnailHttp(20); + let avatar; + if (joinedRoom) { + avatar = ; + } else { + avatar = ; } let description = _t("%(count)s members", { count: room.num_joined_members }); if (numChildRooms !== undefined) { description += " · " + _t("%(count)s rooms", { count: numChildRooms }); } - if (room.topic) { - description += " · " + room.topic; + + const topic = joinedRoom?.currentState.getStateEvents(EventType.RoomTopic, "")?.getContent()?.topic || room.topic; + if (topic) { + description += " · " + topic; } let suggestedSection; @@ -167,7 +175,7 @@ const Tile: React.FC = ({ } const content = - + { avatar }
      { name } { suggestedSection } From 4be8bbeef9360351873f6a23239c3ab6413fa408 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 24 May 2021 21:17:30 +0100 Subject: [PATCH 211/999] Close creation menu when expanding space panel via expand hierarchy --- src/components/views/spaces/SpacePanel.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/components/views/spaces/SpacePanel.tsx b/src/components/views/spaces/SpacePanel.tsx index 411b0f9b5e..163a5cfb0b 100644 --- a/src/components/views/spaces/SpacePanel.tsx +++ b/src/components/views/spaces/SpacePanel.tsx @@ -221,14 +221,20 @@ const SpacePanel = () => { space={s} activeSpaces={activeSpaces} isPanelCollapsed={isPanelCollapsed} - onExpand={() => setPanelCollapsed(false)} + onExpand={() => { + closeMenu(); + setPanelCollapsed(false); + }} />) } { spaces.map(s => setPanelCollapsed(false)} + onExpand={() => { + closeMenu(); + setPanelCollapsed(false); + }} />) }
      Date: Mon, 24 May 2021 21:57:59 +0100 Subject: [PATCH 212/999] post-merge fixup --- src/components/views/rooms/BasicMessageComposer.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/views/rooms/BasicMessageComposer.tsx b/src/components/views/rooms/BasicMessageComposer.tsx index a8a33af867..31651d807d 100644 --- a/src/components/views/rooms/BasicMessageComposer.tsx +++ b/src/components/views/rooms/BasicMessageComposer.tsx @@ -726,9 +726,8 @@ export default class BasicMessageEditor extends React.Component member.rawDisplayName : userId; const caret = this.getCaret(); const position = model.positionForOffset(caret.offset, caret.atNodeEnd); - // index is -1 if there are no parts but we only care for if this would be the part in position 0 - const insertIndex = position.index > 0 ? position.index : 0; - const parts = partCreator.createMentionParts(insertIndex, displayName, userId); + // Insert suffix only if the caret is at the start of the composer + const parts = partCreator.createMentionParts(caret.offset === 0, displayName, userId); model.transform(() => { const addedLen = model.insert(parts, position); return model.positionForOffset(caret.offset + addedLen, true); From 4a5c634d82e46088d618466edadffc1151ca2bcb Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 24 May 2021 22:02:50 +0100 Subject: [PATCH 213/999] Iterate PR --- src/components/structures/TimelinePanel.js | 3 ++- .../views/context_menus/MessageContextMenu.js | 6 ++++-- src/components/views/messages/TextualBody.js | 6 ++++-- src/components/views/right_panel/UserInfo.tsx | 5 +++-- src/components/views/rooms/EventTile.tsx | 6 ++++-- src/components/views/rooms/MessageComposer.tsx | 14 ++++++++------ 6 files changed, 25 insertions(+), 15 deletions(-) diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 0972eeb0fb..d6d29e1cde 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -39,6 +39,7 @@ import {UIFeature} from "../../settings/UIFeature"; import {objectHasDiff} from "../../utils/objects"; import {replaceableComponent} from "../../utils/replaceableComponent"; import { arrayFastClone } from "../../utils/arrays"; +import {Action} from "../../dispatcher/actions"; const PAGINATE_SIZE = 20; const INITIAL_SIZE = 20; @@ -470,7 +471,7 @@ class TimelinePanel extends React.Component { break; } - case "composer_insert": { + case Action.ComposerInsert: { // re-dispatch to the correct composer if (this.state.editState) { dis.dispatch({ diff --git a/src/components/views/context_menus/MessageContextMenu.js b/src/components/views/context_menus/MessageContextMenu.js index 1cc4ce6dfb..cb4632b2ac 100644 --- a/src/components/views/context_menus/MessageContextMenu.js +++ b/src/components/views/context_menus/MessageContextMenu.js @@ -31,6 +31,8 @@ import { isContentActionable } from '../../../utils/EventUtils'; import {MenuItem} from "../../structures/ContextMenu"; import {EventType} from "matrix-js-sdk/src/@types/event"; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import {ComposerInsertPayload} from "../../../dispatcher/payloads/ComposerInsertPayload"; +import {Action} from "../../../dispatcher/actions"; export function canCancel(eventStatus) { return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT; @@ -199,8 +201,8 @@ export default class MessageContextMenu extends React.Component { }; onQuoteClick = () => { - dis.dispatch({ - action: "composer_insert", + dis.dispatch({ + action: Action.ComposerInsert, event: this.props.mxEvent, }); this.closeMenu(); diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js index bd5aa8d356..cb8a73ae9a 100644 --- a/src/components/views/messages/TextualBody.js +++ b/src/components/views/messages/TextualBody.js @@ -36,6 +36,8 @@ import {toRightOf} from "../../structures/ContextMenu"; import {copyPlaintext} from "../../../utils/strings"; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import {ComposerInsertPayload} from "../../../dispatcher/payloads/ComposerInsertPayload"; +import {Action} from "../../../dispatcher/actions"; @replaceableComponent("views.messages.TextualBody") export default class TextualBody extends React.Component { @@ -389,8 +391,8 @@ export default class TextualBody extends React.Component { onEmoteSenderClick = event => { const mxEvent = this.props.mxEvent; - dis.dispatch({ - action: "composer_insert", + dis.dispatch({ + action: Action.ComposerInsert, userId: mxEvent.getSender(), }); }; diff --git a/src/components/views/right_panel/UserInfo.tsx b/src/components/views/right_panel/UserInfo.tsx index c4a9a4fe34..e4303864d3 100644 --- a/src/components/views/right_panel/UserInfo.tsx +++ b/src/components/views/right_panel/UserInfo.tsx @@ -66,6 +66,7 @@ import { SetRightPanelPhasePayload } from "../../../dispatcher/payloads/SetRight import RoomAvatar from "../avatars/RoomAvatar"; import RoomName from "../elements/RoomName"; import {mediaFromMxc} from "../../../customisations/Media"; +import {ComposerInsertPayload} from "../../../dispatcher/payloads/ComposerInsertPayload"; export interface IDevice { deviceId: string; @@ -366,8 +367,8 @@ const UserOptionsSection: React.FC<{ }; const onInsertPillButton = function() { - dis.dispatch({ - action: "composer_insert", + dis.dispatch({ + action: Action.ComposerInsert, userId: member.userId, }); }; diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index c03ecb4a77..db8cf67930 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -46,6 +46,8 @@ import { EditorStateTransfer } from "../../../utils/EditorStateTransfer"; import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks'; import {StaticNotificationState} from "../../../stores/notifications/StaticNotificationState"; import NotificationBadge from "./NotificationBadge"; +import {ComposerInsertPayload} from "../../../dispatcher/payloads/ComposerInsertPayload"; +import { Action } from '../../../dispatcher/actions'; const eventTileTypes = { [EventType.RoomMessage]: 'messages.MessageEvent', @@ -698,8 +700,8 @@ export default class EventTile extends React.Component { onSenderProfileClick = event => { const mxEvent = this.props.mxEvent; - dis.dispatch({ - action: "composer_insert", + dis.dispatch({ + action: Action.ComposerInsert, userId: mxEvent.getSender(), }); }; diff --git a/src/components/views/rooms/MessageComposer.tsx b/src/components/views/rooms/MessageComposer.tsx index fdc2a9411e..dc3e2658c0 100644 --- a/src/components/views/rooms/MessageComposer.tsx +++ b/src/components/views/rooms/MessageComposer.tsx @@ -15,16 +15,16 @@ limitations under the License. */ import React from 'react'; import classNames from 'classnames'; -import { _t } from '../../../languageHandler'; +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"; import dis from '../../../dispatcher/dispatcher'; -import { ActionPayload } from "../../../dispatcher/payloads"; +import {ActionPayload} from "../../../dispatcher/payloads"; import Stickerpicker from './Stickerpicker'; -import { makeRoomPermalink, RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks'; +import {makeRoomPermalink, RoomPermalinkCreator} from '../../../utils/permalinks/Permalinks'; import ContentMessages from '../../../ContentMessages'; import E2EIcon from './E2EIcon'; import SettingsStore from "../../../settings/SettingsStore"; @@ -39,8 +39,10 @@ import {VoiceRecordingStore} from "../../../stores/VoiceRecordingStore"; import {RecordingState} from "../../../voice/VoiceRecording"; import Tooltip, {Alignment} from "../elements/Tooltip"; import ResizeNotifier from "../../../utils/ResizeNotifier"; -import { E2EStatus } from '../../../utils/ShieldUtils'; +import {E2EStatus} from '../../../utils/ShieldUtils'; import SendMessageComposer from "./SendMessageComposer"; +import {ComposerInsertPayload} from "../../../dispatcher/payloads/ComposerInsertPayload"; +import {Action} from "../../../dispatcher/actions"; interface IComposerAvatarProps { me: object; @@ -317,8 +319,8 @@ export default class MessageComposer extends React.Component { } addEmoji(emoji) { - dis.dispatch({ - action: "composer_insert", + dis.dispatch({ + action: Action.ComposerInsert, text: emoji, }); } From 5c674365d370abab45f0b1b4815c064c893eb53c Mon Sep 17 00:00:00 2001 From: David Schilling Date: Mon, 22 Feb 2021 17:43:15 +0100 Subject: [PATCH 214/999] Add url param `defaultUsername` to prefill the login username field Signed-off-by: David Schilling --- src/components/structures/MatrixChat.tsx | 1 + src/components/structures/auth/Login.tsx | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 49386c5f65..365ac10d3d 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -2090,6 +2090,7 @@ export default class MatrixChat extends React.PureComponent { onForgotPasswordClick={showPasswordReset ? this.onForgotPasswordClick : undefined} onServerConfigChange={this.onServerConfigChange} fragmentAfterLogin={fragmentAfterLogin} + defaultUsername={this.props.startingFragmentQueryParams.defaultUsername} {...this.getServerProperties()} /> ); diff --git a/src/components/structures/auth/Login.tsx b/src/components/structures/auth/Login.tsx index 34a5410928..d34582b0c3 100644 --- a/src/components/structures/auth/Login.tsx +++ b/src/components/structures/auth/Login.tsx @@ -59,6 +59,7 @@ interface IProps { fallbackHsUrl?: string; defaultDeviceDisplayName?: string; fragmentAfterLogin?: string; + defaultUsername?: string; // Called when the user has logged in. Params: // - The object returned by the login API @@ -119,7 +120,7 @@ export default class LoginComponent extends React.PureComponent flows: null, - username: "", + username: props.defaultUsername? props.defaultUsername: '', phoneCountry: null, phoneNumber: "", From 525e3eaf432db2460d915d7ead21f43be614ef11 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Tue, 25 May 2021 09:46:49 +0100 Subject: [PATCH 215/999] Prevent reflow when getting screen orientation It is better to access the device orientation using media queries as it will not force a reflow compared to accessing innerWidth/innerHeight --- src/CountlyAnalytics.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/CountlyAnalytics.ts b/src/CountlyAnalytics.ts index 974c08df18..61c471e4d4 100644 --- a/src/CountlyAnalytics.ts +++ b/src/CountlyAnalytics.ts @@ -684,7 +684,9 @@ export default class CountlyAnalytics { } private getOrientation = (): Orientation => { - return window.innerWidth > window.innerHeight ? Orientation.Landscape : Orientation.Portrait; + return window.matchMedia("(orientation: landscape)").matches + ? Orientation.Landscape + : Orientation.Portrait }; private reportOrientation = () => { From 73d51a91d6dbd91ac65cd25df050f071f4a01995 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Tue, 25 May 2021 09:47:45 +0100 Subject: [PATCH 216/999] Prevent unneeded state updates to hide StickerPicker --- src/components/views/rooms/Stickerpicker.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/components/views/rooms/Stickerpicker.js b/src/components/views/rooms/Stickerpicker.js index 82e8cf640c..3d2300b83c 100644 --- a/src/components/views/rooms/Stickerpicker.js +++ b/src/components/views/rooms/Stickerpicker.js @@ -40,7 +40,7 @@ const STICKERPICKER_Z_INDEX = 3500; const PERSISTED_ELEMENT_KEY = "stickerPicker"; @replaceableComponent("views.rooms.Stickerpicker") -export default class Stickerpicker extends React.Component { +export default class Stickerpicker extends React.PureComponent { static currentWidget; constructor(props) { @@ -341,21 +341,27 @@ export default class Stickerpicker extends React.Component { * @param {Event} ev Event that triggered the function call */ _onHideStickersClick(ev) { - this.setState({showStickers: false}); + if (this.state.showStickers) { + this.setState({showStickers: false}); + } } /** * Called when the window is resized */ _onResize() { - this.setState({showStickers: false}); + if (this.state.showStickers) { + this.setState({showStickers: false}); + } } /** * The stickers picker was hidden */ _onFinished() { - this.setState({showStickers: false}); + if (this.state.showStickers) { + this.setState({showStickers: false}); + } } /** From 2710062df70892f422da904ee7a1edc226e7b168 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Tue, 25 May 2021 09:49:15 +0100 Subject: [PATCH 217/999] Create a UIStore to track important data This helper should hold data related to the UI and access save in a smart to avoid performance pitfalls in other parts of the application --- src/stores/UIStore.ts | 69 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 src/stores/UIStore.ts diff --git a/src/stores/UIStore.ts b/src/stores/UIStore.ts new file mode 100644 index 0000000000..62b73a14f6 --- /dev/null +++ b/src/stores/UIStore.ts @@ -0,0 +1,69 @@ +/* +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 EventEmitter from "events"; + +export enum UI_EVENTS { + Resize = "resize" +} + +export type ResizeObserverCallbackFunction = (entries: ResizeObserverEntry[]) => void; + + +export default class UIStore extends EventEmitter { + private static _instance: UIStore = null; + + private resizeObserver: ResizeObserver; + + public windowWith: number; + public windowHeight: number; + + constructor() { + super(); + + this.windowWith = window.innerWidth; + this.windowHeight = window.innerHeight; + + this.resizeObserver = new ResizeObserver(this.resizeObserverCallback); + this.resizeObserver.observe(document.body); + } + + public static get instance(): UIStore { + if (!UIStore._instance) { + UIStore._instance = new UIStore(); + } + return UIStore._instance; + } + + public static destroy(): void { + if (UIStore._instance) { + UIStore._instance.resizeObserver.disconnect(); + UIStore._instance.removeAllListeners(); + UIStore._instance = null; + } + } + + private resizeObserverCallback = (entries: ResizeObserverEntry[]) => { + const { width, height } = entries + .find(entry => entry.target === document.body) + .contentRect; + + this.windowWith = width; + this.windowHeight = height; + + this.emit(UI_EVENTS.Resize, entries); + } +} From ac93cc514f0e0a2be46c300d142496a6325a00f7 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Tue, 25 May 2021 09:50:09 +0100 Subject: [PATCH 218/999] Prevent layout trashing when resizing the window --- src/components/structures/LeftPanel.tsx | 13 +------------ src/components/structures/LeftPanelWidget.tsx | 10 ++-------- src/components/structures/MatrixChat.tsx | 17 +++++++---------- src/components/views/rooms/RoomList.tsx | 6 +----- src/components/views/rooms/RoomSublist.tsx | 2 -- src/utils/ResizeNotifier.js | 6 ------ 6 files changed, 11 insertions(+), 43 deletions(-) diff --git a/src/components/structures/LeftPanel.tsx b/src/components/structures/LeftPanel.tsx index 7f9ef7516e..465d4cac49 100644 --- a/src/components/structures/LeftPanel.tsx +++ b/src/components/structures/LeftPanel.tsx @@ -90,10 +90,6 @@ export default class LeftPanel extends React.Component { this.groupFilterPanelWatcherRef = SettingsStore.watchSetting("TagPanel.enableTagPanel", null, () => { this.setState({showGroupFilterPanel: SettingsStore.getValue("TagPanel.enableTagPanel")}); }); - - // We watch the middle panel because we don't actually get resized, the middle panel does. - // We listen to the noisy channel to avoid choppy reaction times. - this.props.resizeNotifier.on("middlePanelResizedNoisy", this.onResize); } public componentWillUnmount() { @@ -103,7 +99,6 @@ export default class LeftPanel extends React.Component { RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.onBreadcrumbsUpdate); OwnProfileStore.instance.off(UPDATE_EVENT, this.onBackgroundImageUpdate); SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.updateActiveSpace); - this.props.resizeNotifier.off("middlePanelResizedNoisy", this.onResize); } private updateActiveSpace = (activeSpace: Room) => { @@ -281,11 +276,6 @@ export default class LeftPanel extends React.Component { this.handleStickyHeaders(list); }; - private onResize = () => { - if (!this.listContainerRef.current) return; // ignore: no headers to sticky - this.handleStickyHeaders(this.listContainerRef.current); - }; - private onFocus = (ev: React.FocusEvent) => { this.focusedElement = ev.target; }; @@ -420,7 +410,6 @@ export default class LeftPanel extends React.Component { onFocus={this.onFocus} onBlur={this.onBlur} isMinimized={this.props.isMinimized} - onResize={this.onResize} activeSpace={this.state.activeSpace} />; @@ -454,7 +443,7 @@ export default class LeftPanel extends React.Component { {roomList}
      - { !this.props.isMinimized && } + { !this.props.isMinimized && }
      ); diff --git a/src/components/structures/LeftPanelWidget.tsx b/src/components/structures/LeftPanelWidget.tsx index e88af282ba..89c0744cf8 100644 --- a/src/components/structures/LeftPanelWidget.tsx +++ b/src/components/structures/LeftPanelWidget.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, {useContext, useEffect, useMemo} from "react"; +import React, {useContext, useMemo} from "react"; import {Resizable} from "re-resizable"; import classNames from "classnames"; @@ -28,15 +28,11 @@ import {useAccountData} from "../../hooks/useAccountData"; import AppTile from "../views/elements/AppTile"; import {useSettingValue} from "../../hooks/useSettings"; -interface IProps { - onResize(): void; -} - const MIN_HEIGHT = 100; const MAX_HEIGHT = 500; // or 50% of the window height const INITIAL_HEIGHT = 280; -const LeftPanelWidget: React.FC = ({ onResize }) => { +const LeftPanelWidget: React.FC = () => { const cli = useContext(MatrixClientContext); const mWidgetsEvent = useAccountData>(cli, "m.widgets"); @@ -56,7 +52,6 @@ const LeftPanelWidget: React.FC = ({ onResize }) => { const [height, setHeight] = useLocalStorageState("left-panel-widget-height", INITIAL_HEIGHT); const [expanded, setExpanded] = useLocalStorageState("left-panel-widget-expanded", true); - useEffect(onResize, [expanded, onResize]); const [onFocus, isActive, ref] = useRovingTabIndex(); const tabIndex = isActive ? 0 : -1; @@ -69,7 +64,6 @@ const LeftPanelWidget: React.FC = ({ onResize }) => { size={{height} as any} minHeight={MIN_HEIGHT} maxHeight={Math.min(window.innerHeight / 2, MAX_HEIGHT)} - onResize={onResize} onResizeStop={(e, dir, ref, d) => { setHeight(height + d.height); }} diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 49386c5f65..1d794b05c4 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -87,6 +87,7 @@ import defaultDispatcher from "../../dispatcher/dispatcher"; import SecurityCustomisations from "../../customisations/Security"; import PerformanceMonitor, { PerformanceEntryNames } from "../../performance"; +import UIStore, { UI_EVENTS } from "../../stores/UIStore"; /** constants for MatrixChat.state.view */ export enum Views { @@ -225,7 +226,6 @@ export default class MatrixChat extends React.PureComponent { firstSyncPromise: IDeferred; private screenAfterLogin?: IScreen; - private windowWidth: number; private pageChanging: boolean; private tokenLogin?: boolean; private accountPassword?: string; @@ -277,9 +277,7 @@ export default class MatrixChat extends React.PureComponent { } } - this.windowWidth = 10000; - this.handleResize(); - window.addEventListener('resize', this.handleResize); + UIStore.instance.on(UI_EVENTS.Resize, this.handleResize); this.pageChanging = false; @@ -436,7 +434,7 @@ export default class MatrixChat extends React.PureComponent { dis.unregister(this.dispatcherRef); this.themeWatcher.stop(); this.fontWatcher.stop(); - window.removeEventListener('resize', this.handleResize); + UIStore.destroy(); this.state.resizeNotifier.removeListener("middlePanelResized", this.dispatchTimelineResize); if (this.accountPasswordTimer !== null) clearTimeout(this.accountPasswordTimer); @@ -1820,18 +1818,17 @@ export default class MatrixChat extends React.PureComponent { } handleResize = () => { - const hideLhsThreshold = 1000; - const showLhsThreshold = 1000; + const LHS_THRESHOLD = 1000; + const width = UIStore.instance.windowWith; - if (this.windowWidth > hideLhsThreshold && window.innerWidth <= hideLhsThreshold) { + if (width <= LHS_THRESHOLD && !this.state.collapseLhs) { dis.dispatch({ action: 'hide_left_panel' }); } - if (this.windowWidth <= showLhsThreshold && window.innerWidth > showLhsThreshold) { + if (width > LHS_THRESHOLD && this.state.collapseLhs) { dis.dispatch({ action: 'show_left_panel' }); } this.state.resizeNotifier.notifyWindowResized(); - this.windowWidth = window.innerWidth; }; private dispatchTimelineResize() { diff --git a/src/components/views/rooms/RoomList.tsx b/src/components/views/rooms/RoomList.tsx index 7b0dadeca5..896021f918 100644 --- a/src/components/views/rooms/RoomList.tsx +++ b/src/components/views/rooms/RoomList.tsx @@ -55,7 +55,6 @@ interface IProps { onKeyDown: (ev: React.KeyboardEvent) => void; onFocus: (ev: React.FocusEvent) => void; onBlur: (ev: React.FocusEvent) => void; - onResize: () => void; resizeNotifier: ResizeNotifier; isMinimized: boolean; activeSpace: Room; @@ -404,9 +403,7 @@ export default class RoomList extends React.PureComponent { const newSublists = objectWithOnly(newLists, newListIds); const sublists = objectShallowClone(newSublists, (k, v) => arrayFastClone(v)); - this.setState({sublists, isNameFiltering}, () => { - this.props.onResize(); - }); + this.setState({sublists, isNameFiltering}); } }; @@ -537,7 +534,6 @@ export default class RoomList extends React.PureComponent { addRoomLabel={aesthetics.addRoomLabel ? _t(aesthetics.addRoomLabel) : aesthetics.addRoomLabel} addRoomContextMenu={aesthetics.addRoomContextMenu} isMinimized={this.props.isMinimized} - onResize={this.props.onResize} showSkeleton={showSkeleton} extraTiles={extraTiles} resizeNotifier={this.props.resizeNotifier} diff --git a/src/components/views/rooms/RoomSublist.tsx b/src/components/views/rooms/RoomSublist.tsx index f9881d33ae..74987b066a 100644 --- a/src/components/views/rooms/RoomSublist.tsx +++ b/src/components/views/rooms/RoomSublist.tsx @@ -74,7 +74,6 @@ interface IProps { addRoomLabel: string; isMinimized: boolean; tagId: TagID; - onResize: () => void; showSkeleton?: boolean; alwaysVisible?: boolean; resizeNotifier: ResizeNotifier; @@ -473,7 +472,6 @@ export default class RoomSublist extends React.Component { private toggleCollapsed = () => { this.layout.isCollapsed = this.state.isExpanded; this.setState({isExpanded: !this.layout.isCollapsed}); - setImmediate(() => this.props.onResize()); // needs to happen when the DOM is updated }; private onHeaderKeyDown = (ev: React.KeyboardEvent) => { diff --git a/src/utils/ResizeNotifier.js b/src/utils/ResizeNotifier.js index fd12a454f6..4d46d10f6c 100644 --- a/src/utils/ResizeNotifier.js +++ b/src/utils/ResizeNotifier.js @@ -74,12 +74,6 @@ export default class ResizeNotifier extends EventEmitter { // can be called in quick succession notifyWindowResized() { - // no need to throttle this one, - // also it could make scrollbars appear for - // a split second when the room list manual layout is now - // taller than the available space - this.emit("leftPanelResized"); - this._updateMiddlePanel(); } } From a57887cc61154bc2e3b280b047b7d45a26cad173 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Tue, 25 May 2021 09:53:22 +0100 Subject: [PATCH 219/999] Prevent layout trashing on EffectsOverlay --- src/components/views/elements/EffectsOverlay.tsx | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/components/views/elements/EffectsOverlay.tsx b/src/components/views/elements/EffectsOverlay.tsx index 7bed0222b0..00d9d147f1 100644 --- a/src/components/views/elements/EffectsOverlay.tsx +++ b/src/components/views/elements/EffectsOverlay.tsx @@ -17,7 +17,8 @@ import React, { FunctionComponent, useEffect, useRef } from 'react'; import dis from '../../../dispatcher/dispatcher'; import ICanvasEffect from '../../../effects/ICanvasEffect'; -import {CHAT_EFFECTS} from '../../../effects' +import { CHAT_EFFECTS } from '../../../effects' +import UIStore, { UI_EVENTS } from "../../../stores/UIStore"; interface IProps { roomWidth: number; @@ -45,8 +46,8 @@ const EffectsOverlay: FunctionComponent = ({ roomWidth }) => { useEffect(() => { const resize = () => { - if (canvasRef.current) { - canvasRef.current.height = window.innerHeight; + if (canvasRef.current && canvasRef.current?.height !== UIStore.instance.windowHeight) { + canvasRef.current.height = UIStore.instance.windowHeight; } }; const onAction = (payload: { action: string }) => { @@ -58,12 +59,12 @@ const EffectsOverlay: FunctionComponent = ({ roomWidth }) => { } const dispatcherRef = dis.register(onAction); const canvas = canvasRef.current; - canvas.height = window.innerHeight; - window.addEventListener('resize', resize, true); + canvas.height = UIStore.instance.windowHeight; + UIStore.instance.on(UI_EVENTS.Resize, resize); return () => { dis.unregister(dispatcherRef); - window.removeEventListener('resize', resize); + UIStore.instance.off(UI_EVENTS.Resize, resize); // eslint-disable-next-line react-hooks/exhaustive-deps const currentEffects = effectsRef.current; // this is not a react node ref, warning can be safely ignored for (const effect in currentEffects) { From f156c2db15e7e1bef5fd5e444be5dec644aac18d Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Tue, 25 May 2021 10:25:36 +0100 Subject: [PATCH 220/999] prevent reflow in app when accessing window dimensions --- .eslintrc.js | 18 ++++++++++++++++++ src/components/structures/ContextMenu.tsx | 15 ++++++++------- src/components/structures/LeftPanel.tsx | 4 +++- src/components/structures/LeftPanelWidget.tsx | 3 ++- src/components/structures/MatrixChat.tsx | 2 +- src/components/structures/RoomView.tsx | 3 ++- .../views/directory/NetworkDropdown.tsx | 3 ++- src/components/views/elements/Tooltip.tsx | 7 ++++--- src/components/views/messages/TextualBody.js | 3 ++- .../views/right_panel/RoomSummaryCard.tsx | 5 +++-- src/components/views/right_panel/UserInfo.tsx | 5 +++-- .../views/right_panel/WidgetCard.tsx | 3 ++- src/components/views/rooms/AppsDrawer.js | 3 ++- src/stores/UIStore.ts | 10 +++++++--- 14 files changed, 59 insertions(+), 25 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 4959b133a0..9ae51f9bc5 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -30,6 +30,24 @@ module.exports = { "quotes": "off", "no-extra-boolean-cast": "off", + "no-restricted-properties": [ + "error", + ...buildRestrictedPropertiesOptions( + ["window.innerHeight", "window.innerWidth", "window.visualViewport"], + "Use UIStore to access window dimensions instead", + ), + ], }, }], }; + +function buildRestrictedPropertiesOptions(properties, message) { + return properties.map(prop => { + const [object, property] = prop.split("."); + return { + object, + property, + message, + }; + }); +} diff --git a/src/components/structures/ContextMenu.tsx b/src/components/structures/ContextMenu.tsx index ad0f75e162..9d8665c176 100644 --- a/src/components/structures/ContextMenu.tsx +++ b/src/components/structures/ContextMenu.tsx @@ -23,6 +23,7 @@ import classNames from "classnames"; import {Key} from "../../Keyboard"; import {Writeable} from "../../@types/common"; import {replaceableComponent} from "../../utils/replaceableComponent"; +import UIStore from "../../stores/UIStore"; // Shamelessly ripped off Modal.js. There's probably a better way // of doing reusable widgets like dialog boxes & menus where we go and @@ -410,12 +411,12 @@ export const aboveLeftOf = (elementRect: DOMRect, chevronFace = ChevronFace.None const buttonBottom = elementRect.bottom + window.pageYOffset; const buttonTop = elementRect.top + window.pageYOffset; // Align the right edge of the menu to the right edge of the button - menuOptions.right = window.innerWidth - buttonRight; + menuOptions.right = UIStore.instance.windowWidth - buttonRight; // Align the menu vertically on whichever side of the button has more space available. - if (buttonBottom < window.innerHeight / 2) { + if (buttonBottom < UIStore.instance.windowHeight / 2) { menuOptions.top = buttonBottom + vPadding; } else { - menuOptions.bottom = (window.innerHeight - buttonTop) + vPadding; + menuOptions.bottom = (UIStore.instance.windowHeight - buttonTop) + vPadding; } return menuOptions; @@ -430,12 +431,12 @@ export const alwaysAboveLeftOf = (elementRect: DOMRect, chevronFace = ChevronFac const buttonBottom = elementRect.bottom + window.pageYOffset; const buttonTop = elementRect.top + window.pageYOffset; // Align the right edge of the menu to the right edge of the button - menuOptions.right = window.innerWidth - buttonRight; + menuOptions.right = UIStore.instance.windowWidth - buttonRight; // Align the menu vertically on whichever side of the button has more space available. - if (buttonBottom < window.innerHeight / 2) { + if (buttonBottom < UIStore.instance.windowHeight / 2) { menuOptions.top = buttonBottom + vPadding; } else { - menuOptions.bottom = (window.innerHeight - buttonTop) + vPadding; + menuOptions.bottom = (UIStore.instance.windowHeight - buttonTop) + vPadding; } return menuOptions; @@ -451,7 +452,7 @@ export const alwaysAboveRightOf = (elementRect: DOMRect, chevronFace = ChevronFa // Align the left edge of the menu to the left edge of the button menuOptions.left = buttonLeft; // Align the menu vertically above the menu - menuOptions.bottom = (window.innerHeight - buttonTop) + vPadding; + menuOptions.bottom = (UIStore.instance.windowHeight - buttonTop) + vPadding; return menuOptions; }; diff --git a/src/components/structures/LeftPanel.tsx b/src/components/structures/LeftPanel.tsx index 465d4cac49..e929306940 100644 --- a/src/components/structures/LeftPanel.tsx +++ b/src/components/structures/LeftPanel.tsx @@ -43,6 +43,7 @@ import {replaceableComponent} from "../../utils/replaceableComponent"; import {mediaFromMxc} from "../../customisations/Media"; import SpaceStore, {UPDATE_SELECTED_SPACE} from "../../stores/SpaceStore"; import { getKeyBindingsManager, RoomListAction } from "../../KeyBindingsManager"; +import UIStore from "../../stores/UIStore"; interface IProps { isMinimized: boolean; @@ -223,7 +224,8 @@ export default class LeftPanel extends React.Component { header.classList.add("mx_RoomSublist_headerContainer_stickyBottom"); } - const offset = window.innerHeight - (list.parentElement.offsetTop + list.parentElement.offsetHeight); + const offset = UIStore.instance.windowHeight - + (list.parentElement.offsetTop + list.parentElement.offsetHeight); const newBottom = `${offset}px`; if (header.style.bottom !== newBottom) { header.style.bottom = newBottom; diff --git a/src/components/structures/LeftPanelWidget.tsx b/src/components/structures/LeftPanelWidget.tsx index 89c0744cf8..16142069c4 100644 --- a/src/components/structures/LeftPanelWidget.tsx +++ b/src/components/structures/LeftPanelWidget.tsx @@ -27,6 +27,7 @@ import WidgetUtils, {IWidgetEvent} from "../../utils/WidgetUtils"; import {useAccountData} from "../../hooks/useAccountData"; import AppTile from "../views/elements/AppTile"; import {useSettingValue} from "../../hooks/useSettings"; +import UIStore from "../../stores/UIStore"; const MIN_HEIGHT = 100; const MAX_HEIGHT = 500; // or 50% of the window height @@ -63,7 +64,7 @@ const LeftPanelWidget: React.FC = () => { content = { setHeight(height + d.height); }} diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 1d794b05c4..c01437b313 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -1819,7 +1819,7 @@ export default class MatrixChat extends React.PureComponent { handleResize = () => { const LHS_THRESHOLD = 1000; - const width = UIStore.instance.windowWith; + const width = UIStore.instance.windowWidth; if (width <= LHS_THRESHOLD && !this.state.collapseLhs) { dis.dispatch({ action: 'hide_left_panel' }); diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index d822b6a839..e3b0c10fb2 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -83,6 +83,7 @@ import { objectHasDiff } from "../../utils/objects"; import SpaceRoomView from "./SpaceRoomView"; import { IOpts } from "../../createRoom"; import {replaceableComponent} from "../../utils/replaceableComponent"; +import UIStore from "../../stores/UIStore"; const DEBUG = false; let debuglog = function(msg: string) {}; @@ -1585,7 +1586,7 @@ export default class RoomView extends React.Component { // a maxHeight on the underlying remote video tag. // header + footer + status + give us at least 120px of scrollback at all times. - let auxPanelMaxHeight = window.innerHeight - + let auxPanelMaxHeight = UIStore.instance.windowHeight - (54 + // height of RoomHeader 36 + // height of the status area 51 + // minimum height of the message compmoser diff --git a/src/components/views/directory/NetworkDropdown.tsx b/src/components/views/directory/NetworkDropdown.tsx index 66b7321ce0..08787812f6 100644 --- a/src/components/views/directory/NetworkDropdown.tsx +++ b/src/components/views/directory/NetworkDropdown.tsx @@ -38,13 +38,14 @@ import withValidation from "../elements/Validation"; import { SettingLevel } from "../../../settings/SettingLevel"; import TextInputDialog from "../dialogs/TextInputDialog"; import QuestionDialog from "../dialogs/QuestionDialog"; +import UIStore from "../../../stores/UIStore"; export const ALL_ROOMS = Symbol("ALL_ROOMS"); const SETTING_NAME = "room_directory_servers"; const inPlaceOf = (elementRect: Pick) => ({ - right: window.innerWidth - elementRect.right, + right: UIStore.instance.windowWidth - elementRect.right, top: elementRect.top, chevronOffset: 0, chevronFace: ChevronFace.None, diff --git a/src/components/views/elements/Tooltip.tsx b/src/components/views/elements/Tooltip.tsx index 062d26c852..7e9ce9745c 100644 --- a/src/components/views/elements/Tooltip.tsx +++ b/src/components/views/elements/Tooltip.tsx @@ -22,6 +22,7 @@ import React, {Component, CSSProperties} from 'react'; import ReactDOM from 'react-dom'; import classNames from 'classnames'; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import UIStore from "../../../stores/UIStore"; const MIN_TOOLTIP_HEIGHT = 25; @@ -97,15 +98,15 @@ export default class Tooltip extends React.Component { // we need so that we're still centered. offset = Math.floor(parentBox.height - MIN_TOOLTIP_HEIGHT); } - + const width = UIStore.instance.windowWidth; const baseTop = (parentBox.top - 2 + this.props.yOffset) + window.pageYOffset; const top = baseTop + offset; - const right = window.innerWidth - parentBox.right - window.pageXOffset - 16; + const right = width - parentBox.right - window.pageXOffset - 16; const left = parentBox.right + window.pageXOffset + 6; const horizontalCenter = parentBox.right - window.pageXOffset - (parentBox.width / 2); switch (this.props.alignment) { case Alignment.Natural: - if (parentBox.right > window.innerWidth / 2) { + if (parentBox.right > width / 2) { style.right = right; style.top = top; break; diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js index b963e741a1..dc644f1009 100644 --- a/src/components/views/messages/TextualBody.js +++ b/src/components/views/messages/TextualBody.js @@ -36,6 +36,7 @@ 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"; @replaceableComponent("views.messages.TextualBody") export default class TextualBody extends React.Component { @@ -143,7 +144,7 @@ export default class TextualBody extends React.Component { _addCodeExpansionButton(div, pre) { // 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 / window.innerHeight * 100; + const percentageOfViewport = pre.offsetHeight / UIStore.instance.windowHeight * 100; if (percentageOfViewport < 30) return; const button = document.createElement("span"); diff --git a/src/components/views/right_panel/RoomSummaryCard.tsx b/src/components/views/right_panel/RoomSummaryCard.tsx index 88928290f4..937037f644 100644 --- a/src/components/views/right_panel/RoomSummaryCard.tsx +++ b/src/components/views/right_panel/RoomSummaryCard.tsx @@ -46,6 +46,7 @@ import WidgetContextMenu from "../context_menus/WidgetContextMenu"; import {useRoomMemberCount} from "../../../hooks/useRoomMembers"; import { Container, MAX_PINNED, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore"; import RoomName from "../elements/RoomName"; +import UIStore from "../../../stores/UIStore"; interface IProps { room: Room; @@ -116,8 +117,8 @@ const AppRow: React.FC = ({ app, room }) => { const rect = handle.current.getBoundingClientRect(); contextMenu = ; diff --git a/src/components/views/right_panel/UserInfo.tsx b/src/components/views/right_panel/UserInfo.tsx index 9798b282f6..6e56b9259b 100644 --- a/src/components/views/right_panel/UserInfo.tsx +++ b/src/components/views/right_panel/UserInfo.tsx @@ -66,6 +66,7 @@ import { SetRightPanelPhasePayload } from "../../../dispatcher/payloads/SetRight import RoomAvatar from "../avatars/RoomAvatar"; import RoomName from "../elements/RoomName"; import {mediaFromMxc} from "../../../customisations/Media"; +import UIStore from "../../../stores/UIStore"; export interface IDevice { deviceId: string; @@ -1448,8 +1449,8 @@ const UserInfoHeader: React.FC<{ = ({ room, widgetId, onClose }) => { contextMenu = ( entry.target === document.body) .contentRect; - this.windowWith = width; + this.windowWidth = width; this.windowHeight = height; this.emit(UI_EVENTS.Resize, entries); From 45678de9a12cd3ea748a09b2c8b99a9869346262 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Tue, 25 May 2021 11:29:54 +0100 Subject: [PATCH 221/999] Stop overscroll in Firefox Nightly for macOS Firefox is working on an overscroll feature for macOS, similar to the one Safari has had for some time now. It doesn't really make sense in an application context, so this disables it. --- res/css/_common.scss | 2 ++ 1 file changed, 2 insertions(+) diff --git a/res/css/_common.scss b/res/css/_common.scss index d6f85edb86..a05ec7eadd 100644 --- a/res/css/_common.scss +++ b/res/css/_common.scss @@ -45,6 +45,8 @@ html { N.B. Breaks things when we have legitimate horizontal overscroll */ height: 100%; overflow: hidden; + // Stop similar overscroll bounce in Firefox Nightly for macOS + overscroll-behavior: none; } body { From 85a73f2504408b58405762d1d54d36f3f358be1f Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Tue, 25 May 2021 11:48:45 +0100 Subject: [PATCH 222/999] Fix copyright header in UIStore file --- src/stores/UIStore.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stores/UIStore.ts b/src/stores/UIStore.ts index 8e3a6fb31a..e7f6070627 100644 --- a/src/stores/UIStore.ts +++ b/src/stores/UIStore.ts @@ -1,5 +1,5 @@ /* -Copyright 2020 The Matrix.org Foundation C.I.C. +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. From 170b11d130e28b75285baa0a807b41f833435831 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 25 May 2021 12:13:16 +0100 Subject: [PATCH 223/999] Convert RightPanel to Typescript --- .../{RightPanel.js => RightPanel.tsx} | 126 +++++++++--------- src/components/views/right_panel/UserInfo.tsx | 20 ++- 2 files changed, 70 insertions(+), 76 deletions(-) rename src/components/structures/{RightPanel.js => RightPanel.tsx} (79%) diff --git a/src/components/structures/RightPanel.js b/src/components/structures/RightPanel.tsx similarity index 79% rename from src/components/structures/RightPanel.js rename to src/components/structures/RightPanel.tsx index d8c763eabd..15fa94e035 100644 --- a/src/components/structures/RightPanel.js +++ b/src/components/structures/RightPanel.tsx @@ -1,6 +1,6 @@ /* Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> -Copyright 2015 - 2020 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,13 +16,14 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import {Room} from "matrix-js-sdk/src/models/room"; +import {User} from "matrix-js-sdk/src/models/user"; +import {RoomMember} from "matrix-js-sdk/src/models/room-member"; +import {MatrixEvent} from "matrix-js-sdk/src/models/event"; +import {VerificationRequest} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; -import * as sdk from '../../index'; import dis from '../../dispatcher/dispatcher'; import RateLimitedFunc from '../../ratelimitedfunc'; -import { showGroupInviteDialog, showGroupAddRoomDialog } from '../../GroupAddressPicker'; import GroupStore from '../../stores/GroupStore'; import { RightPanelPhases, @@ -36,50 +37,70 @@ import RoomSummaryCard from "../views/right_panel/RoomSummaryCard"; import WidgetCard from "../views/right_panel/WidgetCard"; import {replaceableComponent} from "../../utils/replaceableComponent"; import SettingsStore from "../../settings/SettingsStore"; +import {ActionPayload} from "../../dispatcher/payloads"; +import MemberList from "../views/rooms/MemberList"; +import GroupMemberList from "../views/groups/GroupMemberList"; +import GroupRoomList from "../views/groups/GroupRoomList"; +import GroupRoomInfo from "../views/groups/GroupRoomInfo"; +import UserInfo from "../views/right_panel/UserInfo"; +import ThirdPartyMemberInfo from "../views/rooms/ThirdPartyMemberInfo"; +import FilePanel from "./FilePanel"; +import NotificationPanel from "./NotificationPanel"; +import ResizeNotifier from "../../utils/ResizeNotifier"; + +interface IProps { + room?: Room; // if showing panels for a given room, this is set + groupId?: string; // if showing panels for a given group, this is set + user?: User; // used if we know the user ahead of opening the panel + resizeNotifier: ResizeNotifier; +} + +interface IState { + phase: RightPanelPhases; + isUserPrivilegedInGroup?: boolean; + member?: RoomMember; + verificationRequest?: VerificationRequest; + verificationRequestPromise?: Promise; + space?: Room; + widgetId?: string; + groupRoomId?: string; + groupId?: string; + event: MatrixEvent; +} @replaceableComponent("structures.RightPanel") -export default class RightPanel extends React.Component { - static get propTypes() { - return { - room: PropTypes.instanceOf(Room), // if showing panels for a given room, this is set - groupId: PropTypes.string, // if showing panels for a given group, this is set - user: PropTypes.object, // used if we know the user ahead of opening the panel - }; - } - +export default class RightPanel extends React.Component { static contextType = MatrixClientContext; + private readonly delayedUpdate: RateLimitedFunc; + private dispatcherRef: string; + constructor(props, context) { super(props, context); this.state = { ...RightPanelStore.getSharedInstance().roomPanelPhaseParams, - phase: this._getPhaseFromProps(), + phase: this.getPhaseFromProps(), isUserPrivilegedInGroup: null, - member: this._getUserForPanel(), + member: this.getUserForPanel(), }; - this.onAction = this.onAction.bind(this); - this.onRoomStateMember = this.onRoomStateMember.bind(this); - this.onGroupStoreUpdated = this.onGroupStoreUpdated.bind(this); - this.onInviteToGroupButtonClick = this.onInviteToGroupButtonClick.bind(this); - this.onAddRoomToGroupButtonClick = this.onAddRoomToGroupButtonClick.bind(this); - this._delayedUpdate = new RateLimitedFunc(() => { + this.delayedUpdate = new RateLimitedFunc(() => { this.forceUpdate(); }, 500); } - // Helper function to split out the logic for _getPhaseFromProps() and the constructor + // Helper function to split out the logic for getPhaseFromProps() and the constructor // as both are called at the same time in the constructor. - _getUserForPanel() { + private getUserForPanel() { if (this.state && this.state.member) return this.state.member; const lastParams = RightPanelStore.getSharedInstance().roomPanelPhaseParams; return this.props.user || lastParams['member']; } // gets the current phase from the props and also maybe the store - _getPhaseFromProps() { + private getPhaseFromProps() { const rps = RightPanelStore.getSharedInstance(); - const userForPanel = this._getUserForPanel(); + const userForPanel = this.getUserForPanel(); if (this.props.groupId) { if (!RIGHT_PANEL_PHASES_NO_ARGS.includes(rps.groupPanelPhase)) { dis.dispatch({action: Action.SetRightPanelPhase, phase: RightPanelPhases.GroupMemberList}); @@ -118,7 +139,7 @@ export default class RightPanel extends React.Component { this.dispatcherRef = dis.register(this.onAction); const cli = this.context; cli.on("RoomState.members", this.onRoomStateMember); - this._initGroupStore(this.props.groupId); + this.initGroupStore(this.props.groupId); } componentWillUnmount() { @@ -126,61 +147,47 @@ export default class RightPanel extends React.Component { if (this.context) { this.context.removeListener("RoomState.members", this.onRoomStateMember); } - this._unregisterGroupStore(this.props.groupId); + this.unregisterGroupStore(); } // TODO: [REACT-WARNING] Replace with appropriate lifecycle event UNSAFE_componentWillReceiveProps(newProps) { // eslint-disable-line camelcase if (newProps.groupId !== this.props.groupId) { - this._unregisterGroupStore(this.props.groupId); - this._initGroupStore(newProps.groupId); + this.unregisterGroupStore(); + this.initGroupStore(newProps.groupId); } } - _initGroupStore(groupId) { + private initGroupStore(groupId: string) { if (!groupId) return; GroupStore.registerListener(groupId, this.onGroupStoreUpdated); } - _unregisterGroupStore() { + private unregisterGroupStore() { GroupStore.unregisterListener(this.onGroupStoreUpdated); } - onGroupStoreUpdated() { + private onGroupStoreUpdated = () => { this.setState({ isUserPrivilegedInGroup: GroupStore.isUserPrivileged(this.props.groupId), }); - } + }; - onInviteToGroupButtonClick() { - showGroupInviteDialog(this.props.groupId).then(() => { - this.setState({ - phase: RightPanelPhases.GroupMemberList, - }); - }); - } - - onAddRoomToGroupButtonClick() { - showGroupAddRoomDialog(this.props.groupId).then(() => { - this.forceUpdate(); - }); - } - - onRoomStateMember(ev, state, member) { + private onRoomStateMember = (ev: MatrixEvent, _, member: RoomMember) => { if (!this.props.room || member.roomId !== this.props.room.roomId) { return; } // redraw the badge on the membership list if (this.state.phase === RightPanelPhases.RoomMemberList && member.roomId === this.props.room.roomId) { - this._delayedUpdate(); + this.delayedUpdate(); } else if (this.state.phase === RightPanelPhases.RoomMemberInfo && member.roomId === this.props.room.roomId && member.userId === this.state.member.userId) { // refresh the member info (e.g. new power level) - this._delayedUpdate(); + this.delayedUpdate(); } - } + }; - onAction(payload) { + private onAction = (payload: ActionPayload) => { if (payload.action === Action.AfterRightPanelPhaseChange) { this.setState({ phase: payload.phase, @@ -194,9 +201,9 @@ export default class RightPanel extends React.Component { space: payload.space, }); } - } + }; - onClose = () => { + private onClose = () => { // XXX: There are three different ways of 'closing' this panel depending on what state // things are in... this knows far more than it should do about the state of the rest // of the app and is generally a bit silly. @@ -224,16 +231,6 @@ export default class RightPanel extends React.Component { }; render() { - const MemberList = sdk.getComponent('rooms.MemberList'); - const UserInfo = sdk.getComponent('right_panel.UserInfo'); - const ThirdPartyMemberInfo = sdk.getComponent('rooms.ThirdPartyMemberInfo'); - const NotificationPanel = sdk.getComponent('structures.NotificationPanel'); - const FilePanel = sdk.getComponent('structures.FilePanel'); - - const GroupMemberList = sdk.getComponent('groups.GroupMemberList'); - const GroupRoomList = sdk.getComponent('groups.GroupRoomList'); - const GroupRoomInfo = sdk.getComponent('groups.GroupRoomInfo'); - let panel =
      ; const roomId = this.props.room ? this.props.room.roomId : undefined; @@ -285,6 +282,7 @@ export default class RightPanel extends React.Component { user={this.state.member} groupId={this.props.groupId} key={this.state.member.userId} + phase={this.state.phase} onClose={this.onClose} />; break; diff --git a/src/components/views/right_panel/UserInfo.tsx b/src/components/views/right_panel/UserInfo.tsx index 9798b282f6..9ed2943bbb 100644 --- a/src/components/views/right_panel/UserInfo.tsx +++ b/src/components/views/right_panel/UserInfo.tsx @@ -25,6 +25,7 @@ import {User} from 'matrix-js-sdk/src/models/user'; import {Room} from 'matrix-js-sdk/src/models/room'; import {EventTimeline} from 'matrix-js-sdk/src/models/event-timeline'; 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 Modal from '../../../Modal'; @@ -1529,21 +1530,16 @@ interface IProps { user: Member; groupId?: string; room?: Room; - phase: RightPanelPhases.RoomMemberInfo | RightPanelPhases.GroupMemberInfo | RightPanelPhases.SpaceMemberInfo; + phase: RightPanelPhases.RoomMemberInfo + | RightPanelPhases.GroupMemberInfo + | RightPanelPhases.SpaceMemberInfo + | RightPanelPhases.EncryptionPanel; onClose(): void; + verificationRequest?: VerificationRequest; + verificationRequestPromise?: Promise; } -interface IPropsWithEncryptionPanel extends React.ComponentProps { - user: Member; - groupId: void; - room: Room; - phase: RightPanelPhases.EncryptionPanel; - onClose(): void; -} - -type Props = IProps | IPropsWithEncryptionPanel; - -const UserInfo: React.FC = ({ +const UserInfo: React.FC = ({ user, groupId, room, From 152c178ea9786fa071df680598600c4ba34fa18d Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 25 May 2021 12:15:37 +0100 Subject: [PATCH 224/999] Convert NotificationPanel to Typescript --- ...ficationPanel.js => NotificationPanel.tsx} | 27 ++++++++----------- 1 file changed, 11 insertions(+), 16 deletions(-) rename src/components/structures/{NotificationPanel.js => NotificationPanel.tsx} (76%) diff --git a/src/components/structures/NotificationPanel.js b/src/components/structures/NotificationPanel.tsx similarity index 76% rename from src/components/structures/NotificationPanel.js rename to src/components/structures/NotificationPanel.tsx index 41aafc8b13..7e1566521d 100644 --- a/src/components/structures/NotificationPanel.js +++ b/src/components/structures/NotificationPanel.tsx @@ -1,7 +1,5 @@ /* -Copyright 2016 OpenMarket Ltd -Copyright 2019 New Vector Ltd -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2016, 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,29 +14,25 @@ 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 from "react"; import { _t } from '../../languageHandler'; import {MatrixClientPeg} from "../../MatrixClientPeg"; -import * as sdk from "../../index"; import BaseCard from "../views/right_panel/BaseCard"; import {replaceableComponent} from "../../utils/replaceableComponent"; +import TimelinePanel from "./TimelinePanel"; +import Spinner from "../views/elements/Spinner"; + +interface IProps { + onClose(): void; +} /* * Component which shows the global notification list using a TimelinePanel */ @replaceableComponent("structures.NotificationPanel") -class NotificationPanel extends React.Component { - static propTypes = { - onClose: PropTypes.func.isRequired, - }; - +class NotificationPanel extends React.Component { render() { - // 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('You’re all caught up')}

      {_t('You have no visible notifications.')}

      @@ -47,6 +41,7 @@ class NotificationPanel extends React.Component { let content; const timelineSet = MatrixClientPeg.get().getNotifTimelineSet(); if (timelineSet) { + // wrap a TimelinePanel with the jump-to-event bits turned off. content = ( ; + content = ; } return From cb88f37bbd146c62c4ead8043b10b1c3496be2e7 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Tue, 25 May 2021 12:28:16 +0100 Subject: [PATCH 225/999] Remove outdated diagnostic log The cited issue (https://github.com/vector-im/element-web/issues/11120) has since been fixed, so this "temporary" (2 years ago) logging is no longer needed. --- src/components/views/rooms/EventTile.tsx | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 19c5a7acaa..67df5a84ba 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -790,13 +790,6 @@ export default class EventTile extends React.Component { return null; } const eventId = this.props.mxEvent.getId(); - if (!eventId) { - // XXX: Temporary diagnostic logging for https://github.com/vector-im/element-web/issues/11120 - console.error("EventTile attempted to get relations for an event without an ID"); - // Use event's special `toJSON` method to log key data. - console.log(JSON.stringify(this.props.mxEvent, null, 4)); - console.trace("Stacktrace for https://github.com/vector-im/element-web/issues/11120"); - } return this.props.getRelationsForEvent(eventId, "m.annotation", "m.reaction"); }; From 88af74e4a4b07325246ff6e8f9301f60c7955f97 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Tue, 25 May 2021 12:45:19 +0100 Subject: [PATCH 226/999] Improve addEventsToTimeline performance scoping WhoIsTypingTile::setState --- src/components/views/rooms/WhoIsTypingTile.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/WhoIsTypingTile.js b/src/components/views/rooms/WhoIsTypingTile.js index a25b43fc3a..e69406505e 100644 --- a/src/components/views/rooms/WhoIsTypingTile.js +++ b/src/components/views/rooms/WhoIsTypingTile.js @@ -87,7 +87,9 @@ export default class WhoIsTypingTile extends React.Component { const userId = event.getSender(); // remove user from usersTyping const usersTyping = this.state.usersTyping.filter((m) => m.userId !== userId); - this.setState({usersTyping}); + if (usersTyping.length !== this.state.usersTyping.length) { + this.setState({usersTyping}); + } // abort timer if any this._abortUserTimer(userId); } From 13427aaf0778a8a09d574d99ea33388586ee9a79 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 25 May 2021 13:02:09 +0100 Subject: [PATCH 227/999] Add a pulse animation to the pinned messages indicator and move it --- res/css/views/rooms/_RoomHeader.scss | 41 ++++++++++++++++++------ res/img/element-icons/room/pin.svg | 10 +++--- src/components/views/rooms/RoomHeader.js | 11 +------ 3 files changed, 37 insertions(+), 25 deletions(-) diff --git a/res/css/views/rooms/_RoomHeader.scss b/res/css/views/rooms/_RoomHeader.scss index 387d1588a3..a102dfcdd7 100644 --- a/res/css/views/rooms/_RoomHeader.scss +++ b/res/css/views/rooms/_RoomHeader.scss @@ -281,18 +281,39 @@ limitations under the License. mask-image: url('$(res)/img/element-icons/room/pin.svg'); } -.mx_RoomHeader_pinsIndicator { - position: absolute; - right: 0; - bottom: 4px; - width: 8px; - height: 8px; - border-radius: 8px; - background-color: $pinned-color; -} +$dot-size: 8px; +$pulse-color: $pinned-unread-color; .mx_RoomHeader_pinsIndicatorUnread { - background-color: $pinned-unread-color; + position: absolute; + right: 0; + top: 0; + margin: 4px; + width: $dot-size; + height: $dot-size; + border-radius: 50%; + transform: scale(1); + background: rgba($pulse-color, 1); + box-shadow: 0 0 0 0 rgba($pulse-color, 1); + animation: mx_RoomHeader_indicator_pulse 2s infinite; + animation-iteration-count: 1; +} + +@keyframes mx_RoomHeader_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); + } } @media only screen and (max-width: 480px) { diff --git a/res/img/element-icons/room/pin.svg b/res/img/element-icons/room/pin.svg index 16941b329b..2448fc61c5 100644 --- a/res/img/element-icons/room/pin.svg +++ b/res/img/element-icons/room/pin.svg @@ -1,7 +1,7 @@ - - - - - + + + + + diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js index 6d3b50c10d..7eeb5875ef 100644 --- a/src/components/views/rooms/RoomHeader.js +++ b/src/components/views/rooms/RoomHeader.js @@ -110,13 +110,6 @@ export default class RoomHeader extends React.Component { return true; } - _hasPins() { - const currentPinEvent = this.props.room.currentState.getStateEvents("m.room.pinned_events", ''); - if (!currentPinEvent) return false; - - return !(currentPinEvent.getContent().pinned && currentPinEvent.getContent().pinned.length <= 0); - } - render() { let searchStatus = null; let cancelButton = null; @@ -184,9 +177,7 @@ export default class RoomHeader extends React.Component { if (this.props.onPinnedClick && SettingsStore.getValue('feature_pinning')) { let pinsIndicator = null; if (this._hasUnreadPins()) { - pinsIndicator = (
      ); - } else if (this._hasPins()) { - pinsIndicator = (
      ); + pinsIndicator = (
      ); } pinnedEventsButton = From 96928e5d319653352ad8a5ae56840cfb5dae1e02 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 25 May 2021 13:17:14 +0100 Subject: [PATCH 228/999] Header Buttons switch to a fragment from an array of nodes --- .../views/right_panel/GroupHeaderButtons.tsx | 14 ++++++++------ src/components/views/right_panel/HeaderButtons.tsx | 2 +- .../views/right_panel/RoomHeaderButtons.tsx | 10 ++++------ 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/components/views/right_panel/GroupHeaderButtons.tsx b/src/components/views/right_panel/GroupHeaderButtons.tsx index f006975b08..5ae5709d44 100644 --- a/src/components/views/right_panel/GroupHeaderButtons.tsx +++ b/src/components/views/right_panel/GroupHeaderButtons.tsx @@ -84,19 +84,21 @@ export default class GroupHeaderButtons extends HeaderButtons { }; renderButtons() { - return [ - + , - + , - ]; + /> + ; } } diff --git a/src/components/views/right_panel/HeaderButtons.tsx b/src/components/views/right_panel/HeaderButtons.tsx index 2144292679..6b8445e9b9 100644 --- a/src/components/views/right_panel/HeaderButtons.tsx +++ b/src/components/views/right_panel/HeaderButtons.tsx @@ -95,7 +95,7 @@ export default abstract class HeaderButtons extends React.Component diff --git a/src/components/views/right_panel/RoomHeaderButtons.tsx b/src/components/views/right_panel/RoomHeaderButtons.tsx index 0571622e64..2a7a9e191b 100644 --- a/src/components/views/right_panel/RoomHeaderButtons.tsx +++ b/src/components/views/right_panel/RoomHeaderButtons.tsx @@ -81,23 +81,21 @@ export default class RoomHeaderButtons extends HeaderButtons { }; public renderButtons() { - return [ + return <> , + /> , - ]; + /> + ; } } From 7303166924b0c6ea492811f768354e27b35cd974 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Tue, 25 May 2021 13:53:20 +0100 Subject: [PATCH 229/999] fix sticky headers when results num get displayed --- res/css/views/rooms/_RoomSublist.scss | 2 +- src/components/structures/LeftPanel.tsx | 15 ++++++--------- src/components/views/rooms/RoomListNumResults.tsx | 14 ++++++++++++-- 3 files changed, 19 insertions(+), 12 deletions(-) diff --git a/res/css/views/rooms/_RoomSublist.scss b/res/css/views/rooms/_RoomSublist.scss index 1aafa8da0e..fa94425659 100644 --- a/res/css/views/rooms/_RoomSublist.scss +++ b/res/css/views/rooms/_RoomSublist.scss @@ -62,7 +62,7 @@ limitations under the License. position: fixed; height: 32px; // to match the header container // width set by JS - width: calc(100% - 22px); + width: calc(100% - 15px); } // We don't have a top style because the top is dependent on the room list header's diff --git a/src/components/structures/LeftPanel.tsx b/src/components/structures/LeftPanel.tsx index e929306940..4d7b80726f 100644 --- a/src/components/structures/LeftPanel.tsx +++ b/src/components/structures/LeftPanel.tsx @@ -110,6 +110,11 @@ export default class LeftPanel extends React.Component { dis.fire(Action.ViewRoomDirectory); }; + private refreshStickyHeaders = () => { + if (!this.listContainerRef.current) return; // ignore: no headers to sticky + this.handleStickyHeaders(this.listContainerRef.current); + } + private onBreadcrumbsUpdate = () => { const newVal = BreadcrumbsStore.instance.visible; if (newVal !== this.state.showBreadcrumbs) { @@ -243,18 +248,10 @@ export default class LeftPanel extends React.Component { if (!header.classList.contains("mx_RoomSublist_headerContainer_sticky")) { header.classList.add("mx_RoomSublist_headerContainer_sticky"); } - - const newWidth = `${headerStickyWidth}px`; - if (header.style.width !== newWidth) { - header.style.width = newWidth; - } } else if (!style.stickyTop && !style.stickyBottom) { if (header.classList.contains("mx_RoomSublist_headerContainer_sticky")) { header.classList.remove("mx_RoomSublist_headerContainer_sticky"); } - if (header.style.width) { - header.style.removeProperty('width'); - } } } @@ -432,7 +429,7 @@ export default class LeftPanel extends React.Component { {this.renderHeader()} {this.renderSearchExplore()} {this.renderBreadcrumbs()} - +
      { +interface IProps { + onVisibilityChange?: () => void +} + +const RoomListNumResults: React.FC = ({ onVisibilityChange }) => { const [count, setCount] = useState(null); useEventEmitter(RoomListStore.instance, LISTS_UPDATE_EVENT, () => { if (RoomListStore.instance.getFirstNameFilterCondition()) { @@ -32,6 +36,12 @@ const RoomListNumResults: React.FC = () => { } }); + useEffect(() => { + if (onVisibilityChange) { + onVisibilityChange(); + } + }, [count, onVisibilityChange]); + if (typeof count !== "number") return null; return
      From 02d11b8926494b16f897e49f97076af8461c1278 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 25 May 2021 13:53:46 +0100 Subject: [PATCH 230/999] Extend HeaderButton and HeaderButtons to be more generic --- .../views/right_panel/HeaderButton.tsx | 26 ++++++++----------- .../views/right_panel/HeaderButtons.tsx | 4 +-- 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/src/components/views/right_panel/HeaderButton.tsx b/src/components/views/right_panel/HeaderButton.tsx index 2bc360e380..8451f69fd3 100644 --- a/src/components/views/right_panel/HeaderButton.tsx +++ b/src/components/views/right_panel/HeaderButton.tsx @@ -29,8 +29,6 @@ interface IProps { isHighlighted: boolean; // click handler onClick: () => void; - // The badge to display above the icon - badge?: React.ReactNode; // The parameters to track the click event analytics: Parameters; @@ -40,31 +38,29 @@ interface IProps { title: string; } -// TODO: replace this, the composer buttons and the right panel buttons with a unified -// representation +// TODO: replace this, the composer buttons and the right panel buttons with a unified representation @replaceableComponent("views.right_panel.HeaderButton") export default class HeaderButton extends React.Component { - constructor(props: IProps) { - super(props); - this.onClick = this.onClick.bind(this); - } - - private onClick() { + private onClick = () => { Analytics.trackEvent(...this.props.analytics); this.props.onClick(); - } + }; public render() { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const {isHighlighted, onClick, analytics, name, title, ...props} = this.props; + const classes = classNames({ mx_RightPanel_headerButton: true, - mx_RightPanel_headerButton_highlight: this.props.isHighlighted, - [`mx_RightPanel_${this.props.name}`]: true, + mx_RightPanel_headerButton_highlight: isHighlighted, + [`mx_RightPanel_${name}`]: true, }); return ; diff --git a/src/components/views/right_panel/HeaderButtons.tsx b/src/components/views/right_panel/HeaderButtons.tsx index 6b8445e9b9..281d7edd8b 100644 --- a/src/components/views/right_panel/HeaderButtons.tsx +++ b/src/components/views/right_panel/HeaderButtons.tsx @@ -43,11 +43,11 @@ interface IState { interface IProps {} @replaceableComponent("views.right_panel.HeaderButtons") -export default abstract class HeaderButtons extends React.Component { +export default abstract class HeaderButtons

      extends React.Component { private storeToken: EventSubscription; private dispatcherRef: string; - constructor(props: IProps, kind: HeaderKind) { + constructor(props: IProps & P, kind: HeaderKind) { super(props); const rps = RightPanelStore.getSharedInstance(); From a803e33ffe3ae4b17548a69161ae9c0cb2c160f8 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Tue, 25 May 2021 14:10:16 +0100 Subject: [PATCH 231/999] Convert WhoIsTypingTile to TypeScript --- ...WhoIsTypingTile.js => WhoIsTypingTile.tsx} | 62 +++++++++++-------- 1 file changed, 35 insertions(+), 27 deletions(-) rename src/components/views/rooms/{WhoIsTypingTile.js => WhoIsTypingTile.tsx} (83%) diff --git a/src/components/views/rooms/WhoIsTypingTile.js b/src/components/views/rooms/WhoIsTypingTile.tsx similarity index 83% rename from src/components/views/rooms/WhoIsTypingTile.js rename to src/components/views/rooms/WhoIsTypingTile.tsx index e69406505e..eaade3016b 100644 --- a/src/components/views/rooms/WhoIsTypingTile.js +++ b/src/components/views/rooms/WhoIsTypingTile.tsx @@ -16,36 +16,44 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import * as WhoIsTyping from '../../../WhoIsTyping'; import Timer from '../../../utils/Timer'; -import {MatrixClientPeg} from '../../../MatrixClientPeg'; +import { MatrixClientPeg } from '../../../MatrixClientPeg'; import MemberAvatar from '../avatars/MemberAvatar'; -import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; + +import Room from "matrix-js-sdk/src/models/room"; +import { RoomMember } from "matrix-js-sdk/src/models/room-member"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; + +interface IProps { + // the room this statusbar is representing. + room: Room, + onShown?: () => void, + onHidden?: () => void, + // Number of names to display in typing indication. E.g. set to 3, will + // result in "X, Y, Z and 100 others are typing." + whoIsTypingLimit: number, +} + +interface IState { + usersTyping: RoomMember[], + // a map with userid => Timer to delay + // hiding the "x is typing" message for a + // user so hiding it can coincide + // with the sent message by the other side + // resulting in less timeline jumpiness + delayedStopTypingTimers: any +} @replaceableComponent("views.rooms.WhoIsTypingTile") -export default class WhoIsTypingTile extends React.Component { - static propTypes = { - // the room this statusbar is representing. - room: PropTypes.object.isRequired, - onShown: PropTypes.func, - onHidden: PropTypes.func, - // Number of names to display in typing indication. E.g. set to 3, will - // result in "X, Y, Z and 100 others are typing." - whoIsTypingLimit: PropTypes.number, - }; - +export default class WhoIsTypingTile extends React.Component { static defaultProps = { whoIsTypingLimit: 3, }; state = { usersTyping: WhoIsTyping.usersTypingApartFromMe(this.props.room), - // a map with userid => Timer to delay - // hiding the "x is typing" message for a - // user so hiding it can coincide - // with the sent message by the other side - // resulting in less timeline jumpiness delayedStopTypingTimers: {}, }; @@ -74,15 +82,15 @@ export default class WhoIsTypingTile extends React.Component { Object.values(this.state.delayedStopTypingTimers).forEach((t) => t.abort()); } - _isVisible(state) { + _isVisible(state: IState): boolean { return state.usersTyping.length !== 0 || Object.keys(state.delayedStopTypingTimers).length !== 0; } - isVisible = () => { + isVisible: () => boolean = () => { return this._isVisible(this.state); }; - onRoomTimeline = (event, room) => { + onRoomTimeline = (event: MatrixEvent, room: Room): void => { if (room?.roomId === this.props.room?.roomId) { const userId = event.getSender(); // remove user from usersTyping @@ -95,7 +103,7 @@ export default class WhoIsTypingTile extends React.Component { } }; - onRoomMemberTyping = (ev, member) => { + onRoomMemberTyping = (): void => { const usersTyping = WhoIsTyping.usersTypingApartFromMeAndIgnored(this.props.room); this.setState({ delayedStopTypingTimers: this._updateDelayedStopTypingTimers(usersTyping), @@ -103,7 +111,7 @@ export default class WhoIsTypingTile extends React.Component { }); }; - _updateDelayedStopTypingTimers(usersTyping) { + _updateDelayedStopTypingTimers(usersTyping: RoomMember[]): void { const usersThatStoppedTyping = this.state.usersTyping.filter((a) => { return !usersTyping.some((b) => a.userId === b.userId); }); @@ -141,7 +149,7 @@ export default class WhoIsTypingTile extends React.Component { return delayedStopTypingTimers; } - _abortUserTimer(userId) { + _abortUserTimer(userId: string): void { const timer = this.state.delayedStopTypingTimers[userId]; if (timer) { timer.abort(); @@ -149,7 +157,7 @@ export default class WhoIsTypingTile extends React.Component { } } - _removeUserTimer(userId) { + _removeUserTimer(userId: string): void { const timer = this.state.delayedStopTypingTimers[userId]; if (timer) { const delayedStopTypingTimers = Object.assign({}, this.state.delayedStopTypingTimers); @@ -158,7 +166,7 @@ export default class WhoIsTypingTile extends React.Component { } } - _renderTypingIndicatorAvatars(users, limit) { + _renderTypingIndicatorAvatars(users: RoomMember[], limit: number): void { let othersCount = 0; if (users.length > limit) { othersCount = users.length - limit + 1; From d6443384213fc13bafcaf0830ed1f832c1fe08ec Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Tue, 25 May 2021 14:34:19 +0100 Subject: [PATCH 232/999] WhoIsTypingTile TypeScript conversion --- .../views/rooms/WhoIsTypingTile.tsx | 48 +++++++++---------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/src/components/views/rooms/WhoIsTypingTile.tsx b/src/components/views/rooms/WhoIsTypingTile.tsx index eaade3016b..21afbc30f4 100644 --- a/src/components/views/rooms/WhoIsTypingTile.tsx +++ b/src/components/views/rooms/WhoIsTypingTile.tsx @@ -16,34 +16,34 @@ limitations under the License. */ import React from 'react'; +import Room from "matrix-js-sdk/src/models/room"; +import { RoomMember } from "matrix-js-sdk/src/models/room-member"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; + import * as WhoIsTyping from '../../../WhoIsTyping'; import Timer from '../../../utils/Timer'; import { MatrixClientPeg } from '../../../MatrixClientPeg'; import MemberAvatar from '../avatars/MemberAvatar'; import { replaceableComponent } from "../../../utils/replaceableComponent"; -import Room from "matrix-js-sdk/src/models/room"; -import { RoomMember } from "matrix-js-sdk/src/models/room-member"; -import { MatrixEvent } from "matrix-js-sdk/src/models/event"; - interface IProps { // the room this statusbar is representing. - room: Room, - onShown?: () => void, - onHidden?: () => void, + room: Room; + onShown?: () => void; + onHidden?: () => void; // Number of names to display in typing indication. E.g. set to 3, will // result in "X, Y, Z and 100 others are typing." - whoIsTypingLimit: number, + whoIsTypingLimit: number; } interface IState { - usersTyping: RoomMember[], + usersTyping: RoomMember[]; // a map with userid => Timer to delay // hiding the "x is typing" message for a // user so hiding it can coincide // with the sent message by the other side // resulting in less timeline jumpiness - delayedStopTypingTimers: any + delayedStopTypingTimers: Record; } @replaceableComponent("views.rooms.WhoIsTypingTile") @@ -79,18 +79,18 @@ export default class WhoIsTypingTile extends React.Component { client.removeListener("RoomMember.typing", this.onRoomMemberTyping); client.removeListener("Room.timeline", this.onRoomTimeline); } - Object.values(this.state.delayedStopTypingTimers).forEach((t) => t.abort()); + Object.values(this.state.delayedStopTypingTimers).forEach((t) => (t as Timer).abort()); } - _isVisible(state: IState): boolean { + private _isVisible(state: IState): boolean { return state.usersTyping.length !== 0 || Object.keys(state.delayedStopTypingTimers).length !== 0; } - isVisible: () => boolean = () => { + public isVisible = (): boolean => { return this._isVisible(this.state); }; - onRoomTimeline = (event: MatrixEvent, room: Room): void => { + private onRoomTimeline = (event: MatrixEvent, room: Room): void => { if (room?.roomId === this.props.room?.roomId) { const userId = event.getSender(); // remove user from usersTyping @@ -99,19 +99,19 @@ export default class WhoIsTypingTile extends React.Component { this.setState({usersTyping}); } // abort timer if any - this._abortUserTimer(userId); + this.abortUserTimer(userId); } }; - onRoomMemberTyping = (): void => { + private onRoomMemberTyping = (): void => { const usersTyping = WhoIsTyping.usersTypingApartFromMeAndIgnored(this.props.room); this.setState({ - delayedStopTypingTimers: this._updateDelayedStopTypingTimers(usersTyping), + delayedStopTypingTimers: this.updateDelayedStopTypingTimers(usersTyping), usersTyping, }); }; - _updateDelayedStopTypingTimers(usersTyping: RoomMember[]): void { + private updateDelayedStopTypingTimers(usersTyping: RoomMember[]): Record { const usersThatStoppedTyping = this.state.usersTyping.filter((a) => { return !usersTyping.some((b) => a.userId === b.userId); }); @@ -139,7 +139,7 @@ export default class WhoIsTypingTile extends React.Component { delayedStopTypingTimers[m.userId] = timer; timer.start(); timer.finished().then( - () => this._removeUserTimer(m.userId), // on elapsed + () => this.removeUserTimer(m.userId), // on elapsed () => {/* aborted */}, ); } @@ -149,15 +149,15 @@ export default class WhoIsTypingTile extends React.Component { return delayedStopTypingTimers; } - _abortUserTimer(userId: string): void { + private abortUserTimer(userId: string): void { const timer = this.state.delayedStopTypingTimers[userId]; if (timer) { timer.abort(); - this._removeUserTimer(userId); + this.removeUserTimer(userId); } } - _removeUserTimer(userId: string): void { + private removeUserTimer(userId: string): void { const timer = this.state.delayedStopTypingTimers[userId]; if (timer) { const delayedStopTypingTimers = Object.assign({}, this.state.delayedStopTypingTimers); @@ -166,7 +166,7 @@ export default class WhoIsTypingTile extends React.Component { } } - _renderTypingIndicatorAvatars(users: RoomMember[], limit: number): void { + private renderTypingIndicatorAvatars(users: RoomMember[], limit: number): JSX.Element[] { let othersCount = 0; if (users.length > limit) { othersCount = users.length - limit + 1; @@ -220,7 +220,7 @@ export default class WhoIsTypingTile extends React.Component { return (

    9. - { this._renderTypingIndicatorAvatars(usersTyping, this.props.whoIsTypingLimit) } + { this.renderTypingIndicatorAvatars(usersTyping, this.props.whoIsTypingLimit) }
      { typingString } From b09dd8f1f89046ac0c94c1eb71e6b4025f557623 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Tue, 25 May 2021 14:54:27 +0100 Subject: [PATCH 233/999] remove unused values --- src/components/structures/LeftPanel.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/components/structures/LeftPanel.tsx b/src/components/structures/LeftPanel.tsx index 4d7b80726f..22c60bff1e 100644 --- a/src/components/structures/LeftPanel.tsx +++ b/src/components/structures/LeftPanel.tsx @@ -157,9 +157,6 @@ export default class LeftPanel extends React.Component { const bottomEdge = list.offsetHeight + list.scrollTop; const sublists = list.querySelectorAll(".mx_RoomSublist:not(.mx_RoomSublist_hidden)"); - const headerRightMargin = 15; // calculated from margins and widths to align with non-sticky tiles - const headerStickyWidth = list.clientWidth - headerRightMargin; - // We track which styles we want on a target before making the changes to avoid // excessive layout updates. const targetStyles = new Map Date: Tue, 25 May 2021 14:57:07 +0100 Subject: [PATCH 234/999] remove CSS out of sync comment --- res/css/views/rooms/_RoomSublist.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/res/css/views/rooms/_RoomSublist.scss b/res/css/views/rooms/_RoomSublist.scss index fa94425659..b3e907af04 100644 --- a/res/css/views/rooms/_RoomSublist.scss +++ b/res/css/views/rooms/_RoomSublist.scss @@ -61,7 +61,6 @@ limitations under the License. &.mx_RoomSublist_headerContainer_sticky { position: fixed; height: 32px; // to match the header container - // width set by JS width: calc(100% - 15px); } From a6ca8f797dd851552608b5ea0c8355d34bb9d25c Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 25 May 2021 15:34:44 +0100 Subject: [PATCH 235/999] Fix useAsyncMemo out-of-order resolutions --- src/hooks/useAsyncMemo.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/hooks/useAsyncMemo.ts b/src/hooks/useAsyncMemo.ts index 38c70de259..eda44b1ae4 100644 --- a/src/hooks/useAsyncMemo.ts +++ b/src/hooks/useAsyncMemo.ts @@ -21,7 +21,15 @@ type Fn = () => Promise; export const useAsyncMemo = (fn: Fn, deps: DependencyList, initialValue?: T): T => { const [value, setValue] = useState(initialValue); useEffect(() => { - fn().then(setValue); + let discard = false; + fn().then(v => { + if (!discard) { + setValue(v); + } + }); + return () => { + discard = true; + }; }, deps); // eslint-disable-line react-hooks/exhaustive-deps return value; }; From 4fa6d3599b2b1902f5faa78cf9a5f130397de1ae Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 25 May 2021 15:44:39 +0100 Subject: [PATCH 236/999] Convert PinnedEventTile to Typescript --- ...PinnedEventTile.js => PinnedEventTile.tsx} | 47 +++++++++++++------ 1 file changed, 32 insertions(+), 15 deletions(-) rename src/components/views/rooms/{PinnedEventTile.js => PinnedEventTile.tsx} (76%) diff --git a/src/components/views/rooms/PinnedEventTile.js b/src/components/views/rooms/PinnedEventTile.tsx similarity index 76% rename from src/components/views/rooms/PinnedEventTile.js rename to src/components/views/rooms/PinnedEventTile.tsx index 78cf422cc6..482fd6ab17 100644 --- a/src/components/views/rooms/PinnedEventTile.js +++ b/src/components/views/rooms/PinnedEventTile.tsx @@ -1,5 +1,6 @@ /* Copyright 2017 Travis Ralston +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,7 +16,9 @@ limitations under the License. */ import React from "react"; -import PropTypes from 'prop-types'; +import { Room } from "matrix-js-sdk/src/models/room"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; + import {MatrixClientPeg} from "../../../MatrixClientPeg"; import dis from "../../../dispatcher/dispatcher"; import AccessibleButton from "../elements/AccessibleButton"; @@ -25,15 +28,15 @@ import { _t } from '../../../languageHandler'; import {formatFullDate} from '../../../DateUtils'; import {replaceableComponent} from "../../../utils/replaceableComponent"; -@replaceableComponent("views.rooms.PinnedEventTile") -export default class PinnedEventTile extends React.Component { - static propTypes = { - mxRoom: PropTypes.object.isRequired, - mxEvent: PropTypes.object.isRequired, - onUnpinned: PropTypes.func, - }; +interface IProps { + mxRoom: Room; + mxEvent: MatrixEvent; + onUnpinned?(): void; +} - onTileClicked = () => { +@replaceableComponent("views.rooms.PinnedEventTile") +export default class PinnedEventTile extends React.Component { + private onTileClicked = () => { dis.dispatch({ action: 'view_room', event_id: this.props.mxEvent.getId(), @@ -42,7 +45,7 @@ export default class PinnedEventTile extends React.Component { }); }; - onUnpinClicked = () => { + private onUnpinClicked = () => { const pinnedEvents = this.props.mxRoom.currentState.getStateEvents("m.room.pinned_events", ""); if (!pinnedEvents || !pinnedEvents.getContent().pinned) { // Nothing to do: already unpinned @@ -60,7 +63,7 @@ export default class PinnedEventTile extends React.Component { } }; - _canUnpin() { + private canUnpin() { return this.props.mxRoom.currentState.mayClientSendStateEvent('m.room.pinned_events', MatrixClientPeg.get()); } @@ -71,10 +74,16 @@ export default class PinnedEventTile extends React.Component { const avatarSize = 40; let unpinButton = null; - if (this._canUnpin()) { + if (this.canUnpin()) { unpinButton = ( - {_t('Unpin + {_t('Unpin ); } @@ -82,14 +91,22 @@ export default class PinnedEventTile extends React.Component { return (
      - + { _t("Jump to message") } { unpinButton }
      - + { senderProfile ? senderProfile.name : sender } From 59f4c728c9a9b2b6072ae26fb1e5f0953fe17fd0 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 25 May 2021 16:10:44 +0100 Subject: [PATCH 237/999] Initial cut of Pinned event card in the right panel --- res/css/_components.scss | 2 +- res/css/structures/_RightPanel.scss | 42 +++ .../right_panel/_PinnedMessagesCard.scss | 241 ++++++++++++++++++ res/css/views/rooms/_PinnedEventsPanel.scss | 37 --- res/css/views/rooms/_RoomHeader.scss | 39 --- src/components/structures/RightPanel.tsx | 11 +- src/components/structures/RoomView.tsx | 15 -- .../views/right_panel/PinnedMessagesCard.tsx | 145 +++++++++++ .../views/right_panel/RoomHeaderButtons.tsx | 46 +++- src/components/views/right_panel/UserInfo.tsx | 3 - src/components/views/rooms/RoomHeader.js | 49 +--- src/settings/Settings.tsx | 4 - src/stores/RightPanelStorePhases.ts | 1 + 13 files changed, 483 insertions(+), 152 deletions(-) create mode 100644 res/css/views/right_panel/_PinnedMessagesCard.scss delete mode 100644 res/css/views/rooms/_PinnedEventsPanel.scss create mode 100644 src/components/views/right_panel/PinnedMessagesCard.tsx diff --git a/res/css/_components.scss b/res/css/_components.scss index c8985cbb51..418b8f51c9 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -179,6 +179,7 @@ @import "./views/messages/_common_CryptoEvent.scss"; @import "./views/right_panel/_BaseCard.scss"; @import "./views/right_panel/_EncryptionInfo.scss"; +@import "./views/right_panel/_PinnedMessagesCard.scss"; @import "./views/right_panel/_RoomSummaryCard.scss"; @import "./views/right_panel/_UserInfo.scss"; @import "./views/right_panel/_VerificationPanel.scss"; @@ -203,7 +204,6 @@ @import "./views/rooms/_NewRoomIntro.scss"; @import "./views/rooms/_NotificationBadge.scss"; @import "./views/rooms/_PinnedEventTile.scss"; -@import "./views/rooms/_PinnedEventsPanel.scss"; @import "./views/rooms/_PresenceLabel.scss"; @import "./views/rooms/_ReplyPreview.scss"; @import "./views/rooms/_RoomBreadcrumbs.scss"; diff --git a/res/css/structures/_RightPanel.scss b/res/css/structures/_RightPanel.scss index 5515fe4060..e24100a9bb 100644 --- a/res/css/structures/_RightPanel.scss +++ b/res/css/structures/_RightPanel.scss @@ -98,6 +98,48 @@ limitations under the License. mask-position: center; } +$dot-size: 8px; +$pulse-color: $pinned-unread-color; + +.mx_RightPanel_pinnedMessagesButton { + &::before { + mask-image: url('$(res)/img/element-icons/room/pin.svg'); + mask-position: center; + } + + .mx_RightPanel_pinnedMessagesButton_unreadIndicator { + position: absolute; + right: 0; + top: 0; + margin: 4px; + width: $dot-size; + height: $dot-size; + border-radius: 50%; + transform: scale(1); + background: rgba($pulse-color, 1); + box-shadow: 0 0 0 0 rgba($pulse-color, 1); + animation: mx_RightPanel_indicator_pulse 2s infinite; + animation-iteration-count: 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); + } +} + .mx_RightPanel_headerButton_highlight { &::before { background-color: $accent-color !important; diff --git a/res/css/views/right_panel/_PinnedMessagesCard.scss b/res/css/views/right_panel/_PinnedMessagesCard.scss new file mode 100644 index 0000000000..4cbc8b9abc --- /dev/null +++ b/res/css/views/right_panel/_PinnedMessagesCard.scss @@ -0,0 +1,241 @@ +/* +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_PinnedMessagesCard { + .mx_BaseCard_header { + text-align: center; + margin-top: 20px; + + h2 { + font-weight: $font-semi-bold; + font-size: $font-18px; + margin: 12px 0 4px; + } + + .mx_RoomSummaryCard_alias { + font-size: $font-13px; + color: $secondary-fg-color; + } + + h2, .mx_RoomSummaryCard_alias { + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + text-overflow: ellipsis; + white-space: pre-wrap; + } + + .mx_RoomSummaryCard_avatar { + display: inline-flex; + + .mx_RoomSummaryCard_e2ee { + display: inline-block; + position: relative; + width: 54px; + height: 54px; + border-radius: 50%; + background-color: #737d8c; + margin-top: -3px; // alignment + margin-left: -10px; // overlap + border: 3px solid $dark-panel-bg-color; + + &::before { + content: ''; + position: absolute; + top: 13px; + left: 13px; + height: 28px; + width: 28px; + mask-size: cover; + mask-repeat: no-repeat; + mask-position: center; + mask-image: url('$(res)/img/e2e/disabled.svg'); + background-color: #ffffff; + } + } + + .mx_RoomSummaryCard_e2ee_normal { + background-color: #424446; + &::before { + mask-image: url('$(res)/img/e2e/normal.svg'); + } + } + + .mx_RoomSummaryCard_e2ee_verified { + background-color: #0dbd8b; + &::before { + mask-image: url('$(res)/img/e2e/verified.svg'); + } + } + + .mx_RoomSummaryCard_e2ee_warning { + background-color: #ff4b55; + &::before { + mask-image: url('$(res)/img/e2e/warning.svg'); + } + } + } + } + + .mx_RoomSummaryCard_aboutGroup { + .mx_RoomSummaryCard_Button { + padding-left: 44px; + + &::before { + content: ''; + position: absolute; + top: 8px; + left: 10px; + height: 24px; + width: 24px; + mask-repeat: no-repeat; + mask-position: center; + background-color: $icon-button-color; + } + } + } + + .mx_RoomSummaryCard_appsGroup { + .mx_RoomSummaryCard_Button { + // this button is special so we have to override some of the original styling + // as we will be applying it in its children + padding: 0; + height: auto; + color: $tertiary-fg-color; + + .mx_RoomSummaryCard_icon_app { + padding: 10px 48px 10px 12px; // based on typical mx_RoomSummaryCard_Button padding + text-overflow: ellipsis; + overflow: hidden; + + .mx_BaseAvatar_image { + vertical-align: top; + margin-right: 12px; + } + + span { + color: $primary-fg-color; + } + } + + .mx_RoomSummaryCard_app_pinToggle, + .mx_RoomSummaryCard_app_options { + position: absolute; + top: 0; + height: 100%; // to give bigger interactive zone + width: 24px; + padding: 12px 4px; + box-sizing: border-box; + min-width: 24px; // prevent flexbox crushing + + &:hover { + &::after { + content: ''; + position: absolute; + height: 24px; + width: 24px; + top: 8px; // equal to padding-top of parent + left: 0; + border-radius: 12px; + background-color: rgba(141, 151, 165, 0.1); + } + } + + &::before { + content: ''; + position: absolute; + height: 16px; + width: 16px; + mask-repeat: no-repeat; + mask-position: center; + mask-size: 16px; + background-color: $icon-button-color; + } + } + + .mx_RoomSummaryCard_app_pinToggle { + right: 24px; + + &::before { + mask-image: url('$(res)/img/element-icons/room/pin-upright.svg'); + } + } + + .mx_RoomSummaryCard_app_options { + right: 48px; + display: none; + + &::before { + mask-image: url('$(res)/img/element-icons/room/ellipsis.svg'); + } + } + + &.mx_RoomSummaryCard_Button_pinned { + &::after { + opacity: 0.2; + } + + .mx_RoomSummaryCard_app_pinToggle::before { + background-color: $accent-color; + } + } + + &:hover { + .mx_RoomSummaryCard_icon_app { + padding-right: 72px; + } + + .mx_RoomSummaryCard_app_options { + display: unset; + } + } + + &::before { + content: unset; + } + + &::after { + top: 8px; // re-align based on the height change + pointer-events: none; // pass through to the real button + } + } + } + + .mx_AccessibleButton_kind_link { + padding: 0; + margin-top: 12px; + margin-bottom: 12px; + font-size: $font-13px; + font-weight: $font-semi-bold; + } +} + +.mx_RoomSummaryCard_icon_people::before { + mask-image: url("$(res)/img/element-icons/room/members.svg"); +} + +.mx_RoomSummaryCard_icon_files::before { + mask-image: url('$(res)/img/element-icons/room/files.svg'); +} + +.mx_RoomSummaryCard_icon_share::before { + mask-image: url('$(res)/img/element-icons/room/share.svg'); +} + +.mx_RoomSummaryCard_icon_settings::before { + mask-image: url('$(res)/img/element-icons/settings.svg'); +} diff --git a/res/css/views/rooms/_PinnedEventsPanel.scss b/res/css/views/rooms/_PinnedEventsPanel.scss deleted file mode 100644 index 663d5bdf6e..0000000000 --- a/res/css/views/rooms/_PinnedEventsPanel.scss +++ /dev/null @@ -1,37 +0,0 @@ -/* -Copyright 2017 Travis Ralston - -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_PinnedEventsPanel { - border-top: 1px solid $primary-hairline-color; -} - -.mx_PinnedEventsPanel_body { - max-height: 300px; - overflow-y: auto; - padding-bottom: 15px; -} - -.mx_PinnedEventsPanel_header { - margin: 0; - padding-top: 8px; - padding-bottom: 15px; -} - -.mx_PinnedEventsPanel_cancel { - margin: 12px; - float: right; - display: inline-block; -} diff --git a/res/css/views/rooms/_RoomHeader.scss b/res/css/views/rooms/_RoomHeader.scss index a102dfcdd7..4142b0a2ef 100644 --- a/res/css/views/rooms/_RoomHeader.scss +++ b/res/css/views/rooms/_RoomHeader.scss @@ -277,45 +277,6 @@ limitations under the License. margin-top: 18px; } -.mx_RoomHeader_pinnedButton::before { - mask-image: url('$(res)/img/element-icons/room/pin.svg'); -} - -$dot-size: 8px; -$pulse-color: $pinned-unread-color; - -.mx_RoomHeader_pinsIndicatorUnread { - position: absolute; - right: 0; - top: 0; - margin: 4px; - width: $dot-size; - height: $dot-size; - border-radius: 50%; - transform: scale(1); - background: rgba($pulse-color, 1); - box-shadow: 0 0 0 0 rgba($pulse-color, 1); - animation: mx_RoomHeader_indicator_pulse 2s infinite; - animation-iteration-count: 1; -} - -@keyframes mx_RoomHeader_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); - } -} - @media only screen and (max-width: 480px) { .mx_RoomHeader_wrapper { padding: 0; diff --git a/src/components/structures/RightPanel.tsx b/src/components/structures/RightPanel.tsx index 15fa94e035..e3237d98a6 100644 --- a/src/components/structures/RightPanel.tsx +++ b/src/components/structures/RightPanel.tsx @@ -26,9 +26,9 @@ import dis from '../../dispatcher/dispatcher'; import RateLimitedFunc from '../../ratelimitedfunc'; import GroupStore from '../../stores/GroupStore'; import { - RightPanelPhases, RIGHT_PANEL_PHASES_NO_ARGS, RIGHT_PANEL_SPACE_PHASES, + RightPanelPhases, } from "../../stores/RightPanelStorePhases"; import RightPanelStore from "../../stores/RightPanelStore"; import MatrixClientContext from "../../contexts/MatrixClientContext"; @@ -47,6 +47,7 @@ import ThirdPartyMemberInfo from "../views/rooms/ThirdPartyMemberInfo"; import FilePanel from "./FilePanel"; import NotificationPanel from "./NotificationPanel"; import ResizeNotifier from "../../utils/ResizeNotifier"; +import PinnedMessagesCard from "../views/right_panel/PinnedMessagesCard"; interface IProps { room?: Room; // if showing panels for a given room, this is set @@ -294,7 +295,13 @@ export default class RightPanel extends React.Component { break; case RightPanelPhases.NotificationPanel: - panel = ; + if (SettingsStore.getValue("feature_pinning")) { + panel = ; + } + break; + + case RightPanelPhases.PinnedMessages: + panel = ; break; case RightPanelPhases.FilePanel: diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index d822b6a839..e8ad1d7e45 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -155,7 +155,6 @@ export interface IState { canPeek: boolean; showApps: boolean; isPeeking: boolean; - showingPinned: boolean; showReadReceipts: boolean; showRightPanel: boolean; // error object, as from the matrix client/server API @@ -232,7 +231,6 @@ export default class RoomView extends React.Component { canPeek: false, showApps: false, isPeeking: false, - showingPinned: false, showReadReceipts: true, showRightPanel: RightPanelStore.getSharedInstance().isOpenForRoom, joining: false, @@ -327,7 +325,6 @@ export default class RoomView extends React.Component { forwardingEvent: RoomViewStore.getForwardingEvent(), // we should only peek once we have a ready client shouldPeek: this.state.matrixClientIsReady && RoomViewStore.shouldPeek(), - showingPinned: SettingsStore.getValue("PinnedEvents.isOpen", roomId), showReadReceipts: SettingsStore.getValue("showReadReceipts", roomId), wasContextSwitch: RoomViewStore.getWasContextSwitch(), }; @@ -1375,13 +1372,6 @@ export default class RoomView extends React.Component { return ret; } - private onPinnedClick = () => { - const nowShowingPinned = !this.state.showingPinned; - const roomId = this.state.room.roomId; - this.setState({showingPinned: nowShowingPinned, searching: false}); - SettingsStore.setValue("PinnedEvents.isOpen", roomId, SettingLevel.ROOM_DEVICE, nowShowingPinned); - }; - private onCallPlaced = (type: PlaceCallType) => { dis.dispatch({ action: 'place_call', @@ -1498,7 +1488,6 @@ export default class RoomView extends React.Component { private onSearchClick = () => { this.setState({ searching: !this.state.searching, - showingPinned: false, }); }; @@ -1825,9 +1814,6 @@ export default class RoomView extends React.Component { } else if (showRoomUpgradeBar) { aux = ; hideCancel = true; - } else if (this.state.showingPinned) { - hideCancel = true; // has own cancel - aux = ; } else if (myMembership !== "join") { // We do have a room object for this room, but we're not currently in it. // We may have a 3rd party invite to it. @@ -2045,7 +2031,6 @@ export default class RoomView extends React.Component { inRoom={myMembership === 'join'} onSearchClick={this.onSearchClick} onSettingsClick={this.onSettingsClick} - onPinnedClick={this.onPinnedClick} onCancelClick={(aux && !hideCancel) ? this.onCancelClick : null} onForgetClick={(myMembership === "leave") ? this.onForgetClick : null} onLeaveClick={(myMembership === "join") ? this.onLeaveClick : null} diff --git a/src/components/views/right_panel/PinnedMessagesCard.tsx b/src/components/views/right_panel/PinnedMessagesCard.tsx new file mode 100644 index 0000000000..46625b6e07 --- /dev/null +++ b/src/components/views/right_panel/PinnedMessagesCard.tsx @@ -0,0 +1,145 @@ +/* +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, {useCallback, useContext, useEffect, useState} from "react"; +import { Room } from "matrix-js-sdk/src/models/room"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { EventType } from 'matrix-js-sdk/src/@types/event'; + +import { _t } from "../../../languageHandler"; +import BaseCard from "./BaseCard"; +import Spinner from "../elements/Spinner"; +import MatrixClientContext from "../../../contexts/MatrixClientContext"; +import { useEventEmitter } from "../../../hooks/useEventEmitter"; +import PinningUtils from "../../../utils/PinningUtils"; +import { useAsyncMemo } from "../../../hooks/useAsyncMemo"; +import PinnedEventTile from "../rooms/PinnedEventTile"; + +interface IProps { + room: Room; + onClose(): void; +} + +export const usePinnedEvents = (room: Room): string[] => { + const [pinnedEvents, setPinnedEvents] = useState([]); + + const update = useCallback((ev?: MatrixEvent) => { + if (!room) return; + if (ev && ev.getType() !== EventType.RoomPinnedEvents) return; + setPinnedEvents(room.currentState.getStateEvents(EventType.RoomPinnedEvents, "")?.getContent()?.pinned || []); + }, [room]); + + useEventEmitter(room.currentState, "RoomState.events", update); + useEffect(() => { + update(); + return () => { + setPinnedEvents([]); + }; + }, [update]); + return pinnedEvents; +}; + +const ReadPinsEventId = "im.vector.room.read_pins"; +const ReadPinsNumIds = 10; + +export const useReadPinnedEvents = (room: Room): Set => { + const [readPinnedEvents, setReadPinnedEvents] = useState>(new Set()); + + const update = useCallback((ev?: MatrixEvent) => { + if (!room) return; + if (ev && ev.getType() !== ReadPinsEventId) return; + const readPins = room.getAccountData(ReadPinsEventId)?.getContent()?.event_ids; + setReadPinnedEvents(new Set(readPins || [])); + }, [room]); + + useEventEmitter(room, "Room.accountData", update); + useEffect(() => { + update(); + return () => { + setReadPinnedEvents(new Set()); + }; + }, [update]); + return readPinnedEvents; +}; + +const PinnedMessagesCard = ({ room, onClose }: IProps) => { + const cli = useContext(MatrixClientContext); + const pinnedEventIds = usePinnedEvents(room); + const readPinnedEvents = useReadPinnedEvents(room); + + useEffect(() => { + const newlyRead = pinnedEventIds.filter(id => !readPinnedEvents.has(id)); + if (newlyRead.length > 0) { + // Only keep the last N event IDs to avoid infinite growth + cli.setRoomAccountData(room.roomId, ReadPinsEventId, { + event_ids: [ + ...newlyRead.reverse(), + ...readPinnedEvents, + ].splice(0, ReadPinsNumIds), + }); + } + }, [cli, room.roomId, pinnedEventIds, readPinnedEvents]); + + const pinnedEvents = useAsyncMemo(() => { + const promises = pinnedEventIds.map(async eventId => { + const timelineSet = room.getUnfilteredTimelineSet(); + const localEvent = timelineSet?.getTimelineForEvent(eventId)?.getEvents().find(e => e.getId() === eventId); + if (localEvent) return localEvent; + + try { + const evJson = await cli.fetchRoomEvent(room.roomId, eventId); + const event = new MatrixEvent(evJson); + if (event.isEncrypted()) { + await cli.decryptEventIfNeeded(event); // TODO await? + } + if (event && PinningUtils.isPinnable(event)) { + return event; + } + } catch (err) { + console.error("Error looking up pinned event " + eventId + " in room " + room.roomId); + console.error(err); + } + return null; + }); + + return Promise.all(promises); + }, [cli, room, pinnedEventIds], null); + + let content; + if (!pinnedEvents) { + content = ; + } else if (pinnedEvents.length > 0) { + content = pinnedEvents.filter(Boolean).map(ev => ( + {}} + /> + )); + } else { + content =
      +

      {_t("You’re all caught up")}

      +

      {_t("You have no visible notifications.")}

      +
      ; + } + + return + { content } + ; +}; + +export default PinnedMessagesCard; diff --git a/src/components/views/right_panel/RoomHeaderButtons.tsx b/src/components/views/right_panel/RoomHeaderButtons.tsx index 2a7a9e191b..3d3f33be00 100644 --- a/src/components/views/right_panel/RoomHeaderButtons.tsx +++ b/src/components/views/right_panel/RoomHeaderButtons.tsx @@ -18,7 +18,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; +import React from "react"; +import { Room } from "matrix-js-sdk/src/models/room"; + import {_t} from '../../../languageHandler'; import HeaderButton from './HeaderButton'; import HeaderButtons, {HeaderKind} from './HeaderButtons'; @@ -27,6 +29,8 @@ import {Action} from "../../../dispatcher/actions"; import {ActionPayload} from "../../../dispatcher/payloads"; import RightPanelStore from "../../../stores/RightPanelStore"; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import {useSettingValue} from "../../../hooks/useSettings"; +import { useReadPinnedEvents, usePinnedEvents } from './PinnedMessagesCard'; const ROOM_INFO_PHASES = [ RightPanelPhases.RoomSummary, @@ -38,9 +42,35 @@ const ROOM_INFO_PHASES = [ RightPanelPhases.Room3pidMemberInfo, ]; +const PinnedMessagesHeaderButton = ({ room, isHighlighted, onClick }) => { + const pinningEnabled = useSettingValue("feature_pinning"); + const pinnedEvents = usePinnedEvents(pinningEnabled && room); + const readPinnedEvents = useReadPinnedEvents(pinningEnabled && room); + if (!pinningEnabled) return null; + + let unreadIndicator; + if (pinnedEvents.some(id => !readPinnedEvents.has(id))) { + unreadIndicator =
      ; + } + + return + { unreadIndicator } + ; +}; + +interface IProps { + room?: Room; +} + @replaceableComponent("views.right_panel.RoomHeaderButtons") -export default class RoomHeaderButtons extends HeaderButtons { - constructor(props) { +export default class RoomHeaderButtons extends HeaderButtons { + constructor(props: IProps) { super(props, HeaderKind.Room); } @@ -80,8 +110,18 @@ export default class RoomHeaderButtons extends HeaderButtons { this.setPhase(RightPanelPhases.NotificationPanel); }; + private onPinnedMessagesClicked = () => { + // This toggles for us, if needed + this.setPhase(RightPanelPhases.PinnedMessages); + }; + public renderButtons() { return <> + { } else { setPowerLevels({}); } - return () => { - setPowerLevels({}); - }; }, [room]); useEventEmitter(cli, "RoomState.events", update); diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js index 7eeb5875ef..217b3ea5c2 100644 --- a/src/components/views/rooms/RoomHeader.js +++ b/src/components/views/rooms/RoomHeader.js @@ -40,7 +40,6 @@ export default class RoomHeader extends React.Component { oobData: PropTypes.object, inRoom: PropTypes.bool, onSettingsClick: PropTypes.func, - onPinnedClick: PropTypes.func, onSearchClick: PropTypes.func, onLeaveClick: PropTypes.func, onCancelClick: PropTypes.func, @@ -59,14 +58,12 @@ export default class RoomHeader extends React.Component { componentDidMount() { const cli = MatrixClientPeg.get(); cli.on("RoomState.events", this._onRoomStateEvents); - cli.on("Room.accountData", this._onRoomAccountData); } componentWillUnmount() { const cli = MatrixClientPeg.get(); if (cli) { cli.removeListener("RoomState.events", this._onRoomStateEvents); - cli.removeListener("Room.accountData", this._onRoomAccountData); } } @@ -79,41 +76,14 @@ export default class RoomHeader extends React.Component { this._rateLimitedUpdate(); }; - _onRoomAccountData = (event, room) => { - if (!this.props.room || room.roomId !== this.props.room.roomId) return; - if (event.getType() !== "im.vector.room.read_pins") return; - - this._rateLimitedUpdate(); - }; - _rateLimitedUpdate = new RateLimitedFunc(function() { /* eslint-disable babel/no-invalid-this */ this.forceUpdate(); }, 500); - _hasUnreadPins() { - const currentPinEvent = this.props.room.currentState.getStateEvents("m.room.pinned_events", ''); - if (!currentPinEvent) return false; - if (currentPinEvent.getContent().pinned && currentPinEvent.getContent().pinned.length <= 0) { - return false; // no pins == nothing to read - } - - const readPinsEvent = this.props.room.getAccountData("im.vector.room.read_pins"); - if (readPinsEvent && readPinsEvent.getContent()) { - const readStateEvents = readPinsEvent.getContent().event_ids || []; - if (readStateEvents) { - return !readStateEvents.includes(currentPinEvent.getId()); - } - } - - // There's pins, and we haven't read any of them - return true; - } - render() { let searchStatus = null; let cancelButton = null; - let pinnedEventsButton = null; if (this.props.onCancelClick) { cancelButton = ; @@ -174,22 +144,6 @@ export default class RoomHeader extends React.Component { />; } - if (this.props.onPinnedClick && SettingsStore.getValue('feature_pinning')) { - let pinsIndicator = null; - if (this._hasUnreadPins()) { - pinsIndicator = (
      ); - } - - pinnedEventsButton = - - { pinsIndicator } - ; - } - let forgetButton; if (this.props.onForgetClick) { forgetButton = @@ -239,7 +193,6 @@ export default class RoomHeader extends React.Component {
      { videoCallButton } { voiceCallButton } - { pinnedEventsButton } { forgetButton } { appsButton } { searchButton } @@ -256,7 +209,7 @@ export default class RoomHeader extends React.Component { { topicElement } { cancelButton } { rightRow } - +
      ); diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 6ff14c16b5..155d039572 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -601,10 +601,6 @@ export const SETTINGS: {[setting: string]: ISetting} = { displayName: _td('Enable widget screenshots on supported widgets'), default: false, }, - "PinnedEvents.isOpen": { - supportedLevels: [SettingLevel.ROOM_DEVICE], - default: false, - }, "promptBeforeInviteUnknownUsers": { supportedLevels: LEVELS_ACCOUNT_SETTINGS, displayName: _td('Prompt before sending invites to potentially invalid matrix IDs'), diff --git a/src/stores/RightPanelStorePhases.ts b/src/stores/RightPanelStorePhases.ts index aea78c7460..0d96f92945 100644 --- a/src/stores/RightPanelStorePhases.ts +++ b/src/stores/RightPanelStorePhases.ts @@ -24,6 +24,7 @@ export enum RightPanelPhases { EncryptionPanel = 'EncryptionPanel', RoomSummary = 'RoomSummary', Widget = 'Widget', + PinnedMessages = "PinnedMessages", Room3pidMemberInfo = 'Room3pidMemberInfo', // Group stuff From 17bbbff4797fac7b796f9d1b773b6cf0cefc4783 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 25 May 2021 16:12:34 +0100 Subject: [PATCH 238/999] Remove Promise allSettled polyfill as its widespread enough now and js-sdk uses it directly --- src/GroupAddressPicker.js | 3 +-- src/components/structures/GroupView.js | 6 +++--- .../views/dialogs/SpaceSettingsDialog.tsx | 3 +-- src/utils/promise.ts | 18 ------------------ 4 files changed, 5 insertions(+), 25 deletions(-) diff --git a/src/GroupAddressPicker.js b/src/GroupAddressPicker.js index d956189f0d..9497d9de4c 100644 --- a/src/GroupAddressPicker.js +++ b/src/GroupAddressPicker.js @@ -21,7 +21,6 @@ import MultiInviter from './utils/MultiInviter'; import { _t } from './languageHandler'; import {MatrixClientPeg} from './MatrixClientPeg'; import GroupStore from './stores/GroupStore'; -import {allSettled} from "./utils/promise"; import StyledCheckbox from './components/views/elements/StyledCheckbox'; export function showGroupInviteDialog(groupId) { @@ -120,7 +119,7 @@ function _onGroupInviteFinished(groupId, addrs) { function _onGroupAddRoomFinished(groupId, addrs, addRoomsPublicly) { const matrixClient = MatrixClientPeg.get(); const errorList = []; - return allSettled(addrs.map((addr) => { + return Promise.allSettled(addrs.map((addr) => { return GroupStore .addRoomToGroup(groupId, addr.address, addRoomsPublicly) .catch(() => { errorList.push(addr.address); }) diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index 3ab009d7b8..3a2c611cc9 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 {allSettled, sleep} from "../../utils/promise"; +import {sleep} from "../../utils/promise"; import RightPanelStore from "../../stores/RightPanelStore"; import AutoHideScrollbar from "./AutoHideScrollbar"; import {mediaFromMxc} from "../../customisations/Media"; @@ -99,7 +99,7 @@ class CategoryRoomList extends React.Component { onFinished: (success, addrs) => { if (!success) return; const errorList = []; - allSettled(addrs.map((addr) => { + Promise.allSettled(addrs.map((addr) => { return GroupStore .addRoomToGroupSummary(this.props.groupId, addr.address) .catch(() => { errorList.push(addr.address); }); @@ -274,7 +274,7 @@ class RoleUserList extends React.Component { onFinished: (success, addrs) => { if (!success) return; const errorList = []; - allSettled(addrs.map((addr) => { + Promise.allSettled(addrs.map((addr) => { return GroupStore .addUserToGroupSummary(addr.address) .catch(() => { errorList.push(addr.address); }); diff --git a/src/components/views/dialogs/SpaceSettingsDialog.tsx b/src/components/views/dialogs/SpaceSettingsDialog.tsx index dc6052650a..7453ff1d8b 100644 --- a/src/components/views/dialogs/SpaceSettingsDialog.tsx +++ b/src/components/views/dialogs/SpaceSettingsDialog.tsx @@ -30,7 +30,6 @@ import ToggleSwitch from "../elements/ToggleSwitch"; import AccessibleButton from "../elements/AccessibleButton"; import Modal from "../../../Modal"; import defaultDispatcher from "../../../dispatcher/dispatcher"; -import {allSettled} from "../../../utils/promise"; import {useDispatcher} from "../../../hooks/useDispatcher"; import {SpaceFeedbackPrompt} from "../../structures/SpaceRoomView"; @@ -91,7 +90,7 @@ const SpaceSettingsDialog: React.FC = ({ matrixClient: cli, space, onFin promises.push(cli.sendStateEvent(space.roomId, EventType.RoomJoinRules, { join_rule: joinRule }, "")); } - const results = await allSettled(promises); + const results = await Promise.allSettled(promises); setBusy(false); const failures = results.filter(r => r.status === "rejected"); if (failures.length > 0) { diff --git a/src/utils/promise.ts b/src/utils/promise.ts index f828ddfdaf..4ebbb27141 100644 --- a/src/utils/promise.ts +++ b/src/utils/promise.ts @@ -51,24 +51,6 @@ export function defer(): IDeferred { return {resolve, reject, promise}; } -// Promise.allSettled polyfill until browser support is stable in Firefox -export function allSettled(promises: Promise[]): Promise | ISettledRejected>> { - if (Promise.allSettled) { - return Promise.allSettled(promises); - } - - // @ts-ignore - typescript isn't smart enough to see the disjoint here - return Promise.all(promises.map((promise) => { - return promise.then(value => ({ - status: "fulfilled", - value, - })).catch(reason => ({ - status: "rejected", - reason, - })); - })); -} - // 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 c1f397dcf7bfaad0e894b4d949b94094108813b6 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 25 May 2021 16:20:23 +0100 Subject: [PATCH 239/999] delint --- src/components/structures/RoomView.tsx | 2 -- src/components/views/right_panel/HeaderButtons.tsx | 2 +- src/contexts/RoomContext.ts | 1 - 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index e8ad1d7e45..482f4a45a7 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -54,7 +54,6 @@ import RoomContext from "../../contexts/RoomContext"; import MatrixClientContext from "../../contexts/MatrixClientContext"; import { E2EStatus, shieldStatusForRoom } from '../../utils/ShieldUtils'; import { Action } from "../../dispatcher/actions"; -import { SettingLevel } from "../../settings/SettingLevel"; import { IMatrixClientCreds } from "../../MatrixClientPeg"; import ScrollPanel from "./ScrollPanel"; import TimelinePanel from "./TimelinePanel"; @@ -63,7 +62,6 @@ import RoomPreviewBar from "../views/rooms/RoomPreviewBar"; import ForwardMessage from "../views/rooms/ForwardMessage"; import SearchBar from "../views/rooms/SearchBar"; import RoomUpgradeWarningBar from "../views/rooms/RoomUpgradeWarningBar"; -import PinnedEventsPanel from "../views/rooms/PinnedEventsPanel"; import AuxPanel from "../views/rooms/AuxPanel"; import RoomHeader from "../views/rooms/RoomHeader"; import { XOR } from "../../@types/common"; diff --git a/src/components/views/right_panel/HeaderButtons.tsx b/src/components/views/right_panel/HeaderButtons.tsx index 281d7edd8b..0c7d9ee299 100644 --- a/src/components/views/right_panel/HeaderButtons.tsx +++ b/src/components/views/right_panel/HeaderButtons.tsx @@ -43,7 +43,7 @@ interface IState { interface IProps {} @replaceableComponent("views.right_panel.HeaderButtons") -export default abstract class HeaderButtons

      extends React.Component { +export default abstract class HeaderButtons

      extends React.Component { private storeToken: EventSubscription; private dispatcherRef: string; diff --git a/src/contexts/RoomContext.ts b/src/contexts/RoomContext.ts index 30ff74b071..7f2c0bb157 100644 --- a/src/contexts/RoomContext.ts +++ b/src/contexts/RoomContext.ts @@ -31,7 +31,6 @@ const RoomContext = createContext({ canPeek: false, showApps: false, isPeeking: false, - showingPinned: false, showReadReceipts: true, showRightPanel: true, joining: false, From e934f81521dbe50dbdfca807fce003fb1f816573 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Tue, 25 May 2021 16:34:52 +0100 Subject: [PATCH 240/999] Skip generatePreview if event is not part of the live timeline --- src/stores/room-list/MessagePreviewStore.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stores/room-list/MessagePreviewStore.ts b/src/stores/room-list/MessagePreviewStore.ts index 10e5cf554e..4de612c7bd 100644 --- a/src/stores/room-list/MessagePreviewStore.ts +++ b/src/stores/room-list/MessagePreviewStore.ts @@ -176,7 +176,7 @@ export class MessagePreviewStore extends AsyncStoreWithClient { if (payload.action === 'MatrixActions.Room.timeline' || payload.action === 'MatrixActions.Event.decrypted') { const event = payload.event; // TODO: Type out the dispatcher - if (!this.previews.has(event.getRoomId())) return; // not important + if (!this.previews.has(event.getRoomId()) || !event.isLiveEvent) return; // not important await this.generatePreview(this.matrixClient.getRoom(event.getRoomId()), TAG_ANY); } } From 80bd1304213b6b9818a1ad4a5bf6cc855422114b Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Tue, 25 May 2021 16:58:23 +0100 Subject: [PATCH 241/999] Prevent DecoratedRoomAvatar to update its state for the same value --- src/components/views/avatars/DecoratedRoomAvatar.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/views/avatars/DecoratedRoomAvatar.tsx b/src/components/views/avatars/DecoratedRoomAvatar.tsx index f15538eabf..42aef24086 100644 --- a/src/components/views/avatars/DecoratedRoomAvatar.tsx +++ b/src/components/views/avatars/DecoratedRoomAvatar.tsx @@ -119,7 +119,10 @@ export default class DecoratedRoomAvatar extends React.PureComponent Date: Tue, 25 May 2021 17:24:43 +0100 Subject: [PATCH 242/999] add header --- .../views/right_panel/PinnedMessagesCard.tsx | 6 +- .../views/rooms/PinnedEventsPanel.js | 145 ------------------ src/i18n/strings/en_EN.json | 10 +- 3 files changed, 10 insertions(+), 151 deletions(-) delete mode 100644 src/components/views/rooms/PinnedEventsPanel.js diff --git a/src/components/views/right_panel/PinnedMessagesCard.tsx b/src/components/views/right_panel/PinnedMessagesCard.tsx index 46625b6e07..579b12c3cf 100644 --- a/src/components/views/right_panel/PinnedMessagesCard.tsx +++ b/src/components/views/right_panel/PinnedMessagesCard.tsx @@ -137,7 +137,11 @@ const PinnedMessagesCard = ({ room, onClose }: IProps) => {

      ; } - return + return { _t("Pinned") }} + className="mx_NotificationPanel" + onClose={onClose} + > { content } ; }; diff --git a/src/components/views/rooms/PinnedEventsPanel.js b/src/components/views/rooms/PinnedEventsPanel.js deleted file mode 100644 index 4b310dbbca..0000000000 --- a/src/components/views/rooms/PinnedEventsPanel.js +++ /dev/null @@ -1,145 +0,0 @@ -/* -Copyright 2017 Travis Ralston -Copyright 2019 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 {MatrixClientPeg} from "../../../MatrixClientPeg"; -import AccessibleButton from "../elements/AccessibleButton"; -import PinnedEventTile from "./PinnedEventTile"; -import { _t } from '../../../languageHandler'; -import PinningUtils from "../../../utils/PinningUtils"; -import {replaceableComponent} from "../../../utils/replaceableComponent"; - -@replaceableComponent("views.rooms.PinnedEventsPanel") -export default class PinnedEventsPanel extends React.Component { - static propTypes = { - // The Room from the js-sdk we're going to show pinned events for - room: PropTypes.object.isRequired, - - onCancelClick: PropTypes.func, - }; - - state = { - loading: true, - }; - - componentDidMount() { - this._updatePinnedMessages(); - MatrixClientPeg.get().on("RoomState.events", this._onStateEvent); - } - - componentWillUnmount() { - if (MatrixClientPeg.get()) { - MatrixClientPeg.get().removeListener("RoomState.events", this._onStateEvent); - } - } - - _onStateEvent = ev => { - if (ev.getRoomId() === this.props.room.roomId && ev.getType() === "m.room.pinned_events") { - this._updatePinnedMessages(); - } - }; - - _updatePinnedMessages = () => { - const pinnedEvents = this.props.room.currentState.getStateEvents("m.room.pinned_events", ""); - if (!pinnedEvents || !pinnedEvents.getContent().pinned) { - this.setState({ loading: false, pinned: [] }); - } else { - const promises = []; - const cli = MatrixClientPeg.get(); - - pinnedEvents.getContent().pinned.map((eventId) => { - promises.push(cli.getEventTimeline(this.props.room.getUnfilteredTimelineSet(), eventId, 0).then( - (timeline) => { - const event = timeline.getEvents().find((e) => e.getId() === eventId); - return {eventId, timeline, event}; - }).catch((err) => { - console.error("Error looking up pinned event " + eventId + " in room " + this.props.room.roomId); - console.error(err); - return null; // return lack of context to avoid unhandled errors - })); - }); - - Promise.all(promises).then((contexts) => { - // Filter out the messages before we try to render them - const pinned = contexts.filter((context) => PinningUtils.isPinnable(context.event)); - - this.setState({ loading: false, pinned }); - }); - } - - this._updateReadState(); - }; - - _updateReadState() { - const pinnedEvents = this.props.room.currentState.getStateEvents("m.room.pinned_events", ""); - if (!pinnedEvents) return; // nothing to read - - let readStateEvents = []; - const readPinsEvent = this.props.room.getAccountData("im.vector.room.read_pins"); - if (readPinsEvent && readPinsEvent.getContent()) { - readStateEvents = readPinsEvent.getContent().event_ids || []; - } - - if (!readStateEvents.includes(pinnedEvents.getId())) { - readStateEvents.push(pinnedEvents.getId()); - - // Only keep the last 10 event IDs to avoid infinite growth - readStateEvents = readStateEvents.reverse().splice(0, 10).reverse(); - - MatrixClientPeg.get().setRoomAccountData(this.props.room.roomId, "im.vector.room.read_pins", { - event_ids: readStateEvents, - }); - } - } - - _getPinnedTiles() { - if (this.state.pinned.length === 0) { - return (
      { _t("No pinned messages.") }
      ); - } - - return this.state.pinned.map((context) => { - return ( - - ); - }); - } - - render() { - let tiles =
      { _t("Loading...") }
      ; - if (this.state && !this.state.loading) { - tiles = this._getPinnedTiles(); - } - - return ( -
      -
      - - - -

      { _t("Pinned Messages") }

      - { tiles } -
      -
      - ); - } -} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 7ceb039822..0aebfc48ce 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1508,9 +1508,6 @@ "Invite to just this room": "Invite to just this room", "Add a photo, so people can easily spot your room.": "Add a photo, so people can easily spot your room.", "This is the start of .": "This is the start of .", - "No pinned messages.": "No pinned messages.", - "Loading...": "Loading...", - "Pinned Messages": "Pinned Messages", "Unpin Message": "Unpin Message", "Jump to message": "Jump to message", "%(duration)ss": "%(duration)ss", @@ -1718,6 +1715,10 @@ "The homeserver the user you’re verifying is connected to": "The homeserver the user you’re verifying is connected to", "Yours, or the other users’ internet connection": "Yours, or the other users’ internet connection", "Yours, or the other users’ session": "Yours, or the other users’ session", + "You’re all caught up": "You’re all caught up", + "You have no visible notifications.": "You have no visible notifications.", + "Pinned": "Pinned", + "Pinned Messages": "Pinned Messages", "Room Info": "Room Info", "You can only pin up to %(count)s widgets|other": "You can only pin up to %(count)s widgets", "Unpin": "Unpin", @@ -1896,6 +1897,7 @@ "Add rooms to this community": "Add rooms to this community", "Filter community rooms": "Filter community rooms", "Something went wrong when trying to get your communities.": "Something went wrong when trying to get your communities.", + "Loading...": "Loading...", "Display your community flair in rooms configured to show it.": "Display your community flair in rooms configured to show it.", "You're not currently a member of any communities.": "You're not currently a member of any communities.", "Frequently Used": "Frequently Used", @@ -2626,8 +2628,6 @@ "Create a new community": "Create a new community", "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.": "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.", "Communities are changing to Spaces": "Communities are changing to Spaces", - "You’re all caught up": "You’re all caught up", - "You have no visible notifications.": "You have no visible notifications.", "%(brand)s failed to get the protocol list from the homeserver. The homeserver may be too old to support third party networks.": "%(brand)s failed to get the protocol list from the homeserver. The homeserver may be too old to support third party networks.", "%(brand)s failed to get the public room list.": "%(brand)s failed to get the public room list.", "The homeserver may be unavailable or overloaded.": "The homeserver may be unavailable or overloaded.", From 231e39a965f0573d6def947d5f85ac8f93295076 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 25 May 2021 17:26:43 +0100 Subject: [PATCH 243/999] Fix accessing currentState on an invalid joinedRoom --- src/components/structures/SpaceRoomDirectory.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/structures/SpaceRoomDirectory.tsx b/src/components/structures/SpaceRoomDirectory.tsx index 3f1679c97e..3985553b20 100644 --- a/src/components/structures/SpaceRoomDirectory.tsx +++ b/src/components/structures/SpaceRoomDirectory.tsx @@ -102,7 +102,7 @@ const Tile: React.FC = ({ children, }) => { const cli = MatrixClientPeg.get(); - const joinedRoom = cli.getRoom(room.room_id)?.getMyMembership() === "join" && cli.getRoom(room.room_id); + const joinedRoom = cli.getRoom(room.room_id)?.getMyMembership() === "join" ? cli.getRoom(room.room_id) : null; const name = joinedRoom?.name || room.name || room.canonical_alias || room.aliases?.[0] || (room.room_type === RoomType.Space ? _t("Unnamed Space") : _t("Unnamed Room")); @@ -162,7 +162,7 @@ const Tile: React.FC = ({ description += " · " + _t("%(count)s rooms", { count: numChildRooms }); } - const topic = joinedRoom?.currentState.getStateEvents(EventType.RoomTopic, "")?.getContent()?.topic || room.topic; + const topic = joinedRoom?.currentState?.getStateEvents(EventType.RoomTopic, "")?.getContent()?.topic || room.topic; if (topic) { description += " · " + topic; } From a4907f8061a1d0e2ec84c6edf5f9669fa7bbc821 Mon Sep 17 00:00:00 2001 From: Jaiwanth Date: Wed, 26 May 2021 12:57:39 +0530 Subject: [PATCH 244/999] Destroy playback instance on unmount --- src/components/views/messages/MVoiceMessageBody.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/views/messages/MVoiceMessageBody.tsx b/src/components/views/messages/MVoiceMessageBody.tsx index 4a2a83465d..a6bd30ac6e 100644 --- a/src/components/views/messages/MVoiceMessageBody.tsx +++ b/src/components/views/messages/MVoiceMessageBody.tsx @@ -71,10 +71,16 @@ export default class MVoiceMessageBody extends React.PureComponent Date: Wed, 26 May 2021 13:07:57 +0530 Subject: [PATCH 245/999] Update src/components/views/messages/MVoiceMessageBody.tsx Co-authored-by: Michael Telatynski <7t3chguy@googlemail.com> --- src/components/views/messages/MVoiceMessageBody.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/views/messages/MVoiceMessageBody.tsx b/src/components/views/messages/MVoiceMessageBody.tsx index a6bd30ac6e..d65de7697a 100644 --- a/src/components/views/messages/MVoiceMessageBody.tsx +++ b/src/components/views/messages/MVoiceMessageBody.tsx @@ -76,9 +76,7 @@ export default class MVoiceMessageBody extends React.PureComponent Date: Wed, 26 May 2021 10:15:31 +0100 Subject: [PATCH 246/999] Fix preview generate check --- src/stores/room-list/MessagePreviewStore.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/stores/room-list/MessagePreviewStore.ts b/src/stores/room-list/MessagePreviewStore.ts index 4de612c7bd..f5b9d9bc6a 100644 --- a/src/stores/room-list/MessagePreviewStore.ts +++ b/src/stores/room-list/MessagePreviewStore.ts @@ -176,7 +176,8 @@ export class MessagePreviewStore extends AsyncStoreWithClient { if (payload.action === 'MatrixActions.Room.timeline' || payload.action === 'MatrixActions.Event.decrypted') { const event = payload.event; // TODO: Type out the dispatcher - if (!this.previews.has(event.getRoomId()) || !event.isLiveEvent) return; // not important + const isHistoricalEvent = payload.hasOwnProperty("isLiveEvent") && !payload.isLiveEvent + if (!this.previews.has(event.getRoomId()) || isHistoricalEvent) return; // not important await this.generatePreview(this.matrixClient.getRoom(event.getRoomId()), TAG_ANY); } } From 27ad90760d117e6c8c203d04669c715d564a01e5 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 26 May 2021 13:51:17 +0100 Subject: [PATCH 247/999] Iterate pinned messages --- .../right_panel/_PinnedMessagesCard.scss | 222 +----------------- res/css/views/rooms/_PinnedEventTile.scss | 122 ++++++---- .../views/right_panel/PinnedMessagesCard.tsx | 61 +++-- .../views/right_panel/RoomHeaderButtons.tsx | 2 +- .../views/rooms/PinnedEventTile.tsx | 132 +++++------ src/i18n/strings/en_EN.json | 6 +- src/stores/RightPanelStorePhases.ts | 1 + 7 files changed, 185 insertions(+), 361 deletions(-) diff --git a/res/css/views/right_panel/_PinnedMessagesCard.scss b/res/css/views/right_panel/_PinnedMessagesCard.scss index 4cbc8b9abc..b6b8238bed 100644 --- a/res/css/views/right_panel/_PinnedMessagesCard.scss +++ b/res/css/views/right_panel/_PinnedMessagesCard.scss @@ -15,227 +15,21 @@ limitations under the License. */ .mx_PinnedMessagesCard { + padding-top: 0; + .mx_BaseCard_header { text-align: center; - margin-top: 20px; + margin-top: 0; + border-bottom: 1px solid $menu-border-color; - h2 { + > h2 { font-weight: $font-semi-bold; font-size: $font-18px; - margin: 12px 0 4px; + margin: 8px 0; } - .mx_RoomSummaryCard_alias { - font-size: $font-13px; - color: $secondary-fg-color; - } - - h2, .mx_RoomSummaryCard_alias { - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - overflow: hidden; - text-overflow: ellipsis; - white-space: pre-wrap; - } - - .mx_RoomSummaryCard_avatar { - display: inline-flex; - - .mx_RoomSummaryCard_e2ee { - display: inline-block; - position: relative; - width: 54px; - height: 54px; - border-radius: 50%; - background-color: #737d8c; - margin-top: -3px; // alignment - margin-left: -10px; // overlap - border: 3px solid $dark-panel-bg-color; - - &::before { - content: ''; - position: absolute; - top: 13px; - left: 13px; - height: 28px; - width: 28px; - mask-size: cover; - mask-repeat: no-repeat; - mask-position: center; - mask-image: url('$(res)/img/e2e/disabled.svg'); - background-color: #ffffff; - } - } - - .mx_RoomSummaryCard_e2ee_normal { - background-color: #424446; - &::before { - mask-image: url('$(res)/img/e2e/normal.svg'); - } - } - - .mx_RoomSummaryCard_e2ee_verified { - background-color: #0dbd8b; - &::before { - mask-image: url('$(res)/img/e2e/verified.svg'); - } - } - - .mx_RoomSummaryCard_e2ee_warning { - background-color: #ff4b55; - &::before { - mask-image: url('$(res)/img/e2e/warning.svg'); - } - } + .mx_BaseCard_close { + margin-right: 6px; } } - - .mx_RoomSummaryCard_aboutGroup { - .mx_RoomSummaryCard_Button { - padding-left: 44px; - - &::before { - content: ''; - position: absolute; - top: 8px; - left: 10px; - height: 24px; - width: 24px; - mask-repeat: no-repeat; - mask-position: center; - background-color: $icon-button-color; - } - } - } - - .mx_RoomSummaryCard_appsGroup { - .mx_RoomSummaryCard_Button { - // this button is special so we have to override some of the original styling - // as we will be applying it in its children - padding: 0; - height: auto; - color: $tertiary-fg-color; - - .mx_RoomSummaryCard_icon_app { - padding: 10px 48px 10px 12px; // based on typical mx_RoomSummaryCard_Button padding - text-overflow: ellipsis; - overflow: hidden; - - .mx_BaseAvatar_image { - vertical-align: top; - margin-right: 12px; - } - - span { - color: $primary-fg-color; - } - } - - .mx_RoomSummaryCard_app_pinToggle, - .mx_RoomSummaryCard_app_options { - position: absolute; - top: 0; - height: 100%; // to give bigger interactive zone - width: 24px; - padding: 12px 4px; - box-sizing: border-box; - min-width: 24px; // prevent flexbox crushing - - &:hover { - &::after { - content: ''; - position: absolute; - height: 24px; - width: 24px; - top: 8px; // equal to padding-top of parent - left: 0; - border-radius: 12px; - background-color: rgba(141, 151, 165, 0.1); - } - } - - &::before { - content: ''; - position: absolute; - height: 16px; - width: 16px; - mask-repeat: no-repeat; - mask-position: center; - mask-size: 16px; - background-color: $icon-button-color; - } - } - - .mx_RoomSummaryCard_app_pinToggle { - right: 24px; - - &::before { - mask-image: url('$(res)/img/element-icons/room/pin-upright.svg'); - } - } - - .mx_RoomSummaryCard_app_options { - right: 48px; - display: none; - - &::before { - mask-image: url('$(res)/img/element-icons/room/ellipsis.svg'); - } - } - - &.mx_RoomSummaryCard_Button_pinned { - &::after { - opacity: 0.2; - } - - .mx_RoomSummaryCard_app_pinToggle::before { - background-color: $accent-color; - } - } - - &:hover { - .mx_RoomSummaryCard_icon_app { - padding-right: 72px; - } - - .mx_RoomSummaryCard_app_options { - display: unset; - } - } - - &::before { - content: unset; - } - - &::after { - top: 8px; // re-align based on the height change - pointer-events: none; // pass through to the real button - } - } - } - - .mx_AccessibleButton_kind_link { - padding: 0; - margin-top: 12px; - margin-bottom: 12px; - font-size: $font-13px; - font-weight: $font-semi-bold; - } -} - -.mx_RoomSummaryCard_icon_people::before { - mask-image: url("$(res)/img/element-icons/room/members.svg"); -} - -.mx_RoomSummaryCard_icon_files::before { - mask-image: url('$(res)/img/element-icons/room/files.svg'); -} - -.mx_RoomSummaryCard_icon_share::before { - mask-image: url('$(res)/img/element-icons/room/share.svg'); -} - -.mx_RoomSummaryCard_icon_settings::before { - mask-image: url('$(res)/img/element-icons/settings.svg'); } diff --git a/res/css/views/rooms/_PinnedEventTile.scss b/res/css/views/rooms/_PinnedEventTile.scss index 030a76674a..8e96f3caeb 100644 --- a/res/css/views/rooms/_PinnedEventTile.scss +++ b/res/css/views/rooms/_PinnedEventTile.scss @@ -16,62 +16,90 @@ limitations under the License. .mx_PinnedEventTile { min-height: 40px; - margin-bottom: 5px; width: 100%; - border-radius: 5px; // for the hover -} + padding: 0 4px 12px; -.mx_PinnedEventTile:hover { - background-color: $event-selected-color; -} + display: grid; + grid-template-areas: "avatar name remove" + "content content content" + "footer footer footer"; + grid-template-rows: max-content auto max-content; + grid-template-columns: 24px auto 24px; + grid-row-gap: 12px; + grid-column-gap: 8px; -.mx_PinnedEventTile .mx_PinnedEventTile_sender, -.mx_PinnedEventTile .mx_PinnedEventTile_timestamp { - color: #868686; - font-size: 0.8em; - vertical-align: top; - display: inline-block; - padding-bottom: 3px; -} + & + .mx_PinnedEventTile { + padding: 12px 4px; + border-top: 1px solid $menu-border-color; + } -.mx_PinnedEventTile .mx_PinnedEventTile_timestamp { - padding-left: 15px; - display: none; -} + .mx_PinnedEventTile_senderAvatar { + grid-area: avatar; + } -.mx_PinnedEventTile .mx_PinnedEventTile_senderAvatar .mx_BaseAvatar { - float: left; - margin-right: 10px; -} + .mx_PinnedEventTile_sender { + grid-area: name; + font-weight: $font-semi-bold; + font-size: $font-15px; + line-height: $font-24px; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } -.mx_PinnedEventTile_actions { - float: right; - margin-right: 10px; - display: none; -} + .mx_PinnedEventTile_unpinButton { + visibility: hidden; + grid-area: remove; + position: relative; + width: 24px; + height: 24px; + border-radius: 8px; -.mx_PinnedEventTile:hover .mx_PinnedEventTile_timestamp { - display: inline-block; -} + &:hover { + background-color: $roomheader-addroom-bg-color; + } -.mx_PinnedEventTile:hover .mx_PinnedEventTile_actions { - display: block; -} + &::before { + content: ""; + position: absolute; + //top: 0; + //left: 0; + height: inherit; + width: inherit; + background: $secondary-fg-color; + mask-position: center; + mask-size: 8px; + mask-repeat: no-repeat; + mask-image: url('$(res)/img/image-view/close.svg'); + } + } -.mx_PinnedEventTile_unpinButton { - display: inline-block; - cursor: pointer; - margin-left: 10px; -} + .mx_PinnedEventTile_message { + grid-area: content; + } -.mx_PinnedEventTile_gotoButton { - display: inline-block; - font-size: 0.7em; // Smaller text to avoid conflicting with the layout -} + .mx_PinnedEventTile_footer { + grid-area: footer; + font-size: 10px; + line-height: 12px; -.mx_PinnedEventTile_message { - margin-left: 50px; - position: relative; - top: 0; - left: 0; + .mx_PinnedEventTile_timestamp { + font-size: inherit; + line-height: inherit; + color: $secondary-fg-color; + } + + .mx_AccessibleButton_kind_link { + padding: 0; + margin-left: 12px; + font-size: inherit; + line-height: inherit; + } + } + + &:hover { + .mx_PinnedEventTile_unpinButton { + visibility: visible; + } + } } diff --git a/src/components/views/right_panel/PinnedMessagesCard.tsx b/src/components/views/right_panel/PinnedMessagesCard.tsx index 579b12c3cf..48be8cd706 100644 --- a/src/components/views/right_panel/PinnedMessagesCard.tsx +++ b/src/components/views/right_panel/PinnedMessagesCard.tsx @@ -16,6 +16,7 @@ limitations under the License. import React, {useCallback, useContext, useEffect, useState} from "react"; import { Room } from "matrix-js-sdk/src/models/room"; +import { RoomState } from "matrix-js-sdk/src/models/room-state"; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { EventType } from 'matrix-js-sdk/src/@types/event'; @@ -42,7 +43,7 @@ export const usePinnedEvents = (room: Room): string[] => { setPinnedEvents(room.currentState.getStateEvents(EventType.RoomPinnedEvents, "")?.getContent()?.pinned || []); }, [room]); - useEventEmitter(room.currentState, "RoomState.events", update); + useEventEmitter(room?.currentState, "RoomState.events", update); useEffect(() => { update(); return () => { @@ -53,7 +54,6 @@ export const usePinnedEvents = (room: Room): string[] => { }; const ReadPinsEventId = "im.vector.room.read_pins"; -const ReadPinsNumIds = 10; export const useReadPinnedEvents = (room: Room): Set => { const [readPinnedEvents, setReadPinnedEvents] = useState>(new Set()); @@ -75,20 +75,36 @@ export const useReadPinnedEvents = (room: Room): Set => { return readPinnedEvents; }; +const useRoomState = (room: Room, mapper: (state: RoomState) => T): T => { + const [value, setValue] = useState(room ? mapper(room.currentState) : undefined); + + const update = useCallback(() => { + if (!room) return; + setValue(mapper(room.currentState)); + }, [room, mapper]); + + useEventEmitter(room?.currentState, "RoomState.events", update); + useEffect(() => { + update(); + return () => { + setValue(undefined); + }; + }, [update]); + return value; +}; + const PinnedMessagesCard = ({ room, onClose }: IProps) => { const cli = useContext(MatrixClientContext); + const canUnpin = useRoomState(room, state => state.mayClientSendStateEvent(EventType.RoomPinnedEvents, cli)); const pinnedEventIds = usePinnedEvents(room); const readPinnedEvents = useReadPinnedEvents(room); useEffect(() => { const newlyRead = pinnedEventIds.filter(id => !readPinnedEvents.has(id)); if (newlyRead.length > 0) { - // Only keep the last N event IDs to avoid infinite growth + // clear out any read pinned events which no longer are pinned cli.setRoomAccountData(room.roomId, ReadPinsEventId, { - event_ids: [ - ...newlyRead.reverse(), - ...readPinnedEvents, - ].splice(0, ReadPinsNumIds), + event_ids: pinnedEventIds, }); } }, [cli, room.roomId, pinnedEventIds, readPinnedEvents]); @@ -122,24 +138,35 @@ const PinnedMessagesCard = ({ room, onClose }: IProps) => { if (!pinnedEvents) { content = ; } else if (pinnedEvents.length > 0) { - content = pinnedEvents.filter(Boolean).map(ev => ( - {}} - /> + let onUnpinClicked; + if (canUnpin) { + onUnpinClicked = async (event: MatrixEvent) => { + const pinnedEvents = room.currentState.getStateEvents(EventType.RoomPinnedEvents, ""); + if (pinnedEvents?.getContent()?.pinned) { + const pinned = pinnedEvents.getContent().pinned; + const index = pinned.indexOf(event.getId()); + if (index !== -1) { + pinned.splice(index, 1); + await cli.sendStateEvent(room.roomId, EventType.RoomPinnedEvents, { pinned }, ""); + } + } + }; + } + + // show them in reverse, with latest pinned at the top + content = pinnedEvents.filter(Boolean).reverse().map(ev => ( + )); } else { - content =
      + content =

      {_t("You’re all caught up")}

      {_t("You have no visible notifications.")}

      ; } return { _t("Pinned") }} - className="mx_NotificationPanel" + header={

      { _t("Pinned messages") }

      } + className="mx_PinnedMessagesCard" onClose={onClose} > { content } diff --git a/src/components/views/right_panel/RoomHeaderButtons.tsx b/src/components/views/right_panel/RoomHeaderButtons.tsx index 3d3f33be00..7ff7207fb6 100644 --- a/src/components/views/right_panel/RoomHeaderButtons.tsx +++ b/src/components/views/right_panel/RoomHeaderButtons.tsx @@ -55,7 +55,7 @@ const PinnedMessagesHeaderButton = ({ room, isHighlighted, onClick }) => { return { + static contextType = MatrixClientContext; + private onTileClicked = () => { dis.dispatch({ action: 'view_room', - event_id: this.props.mxEvent.getId(), + event_id: this.props.event.getId(), highlighted: true, - room_id: this.props.mxEvent.getRoomId(), + room_id: this.props.event.getRoomId(), }); }; - private onUnpinClicked = () => { - const pinnedEvents = this.props.mxRoom.currentState.getStateEvents("m.room.pinned_events", ""); - if (!pinnedEvents || !pinnedEvents.getContent().pinned) { - // Nothing to do: already unpinned - if (this.props.onUnpinned) this.props.onUnpinned(); - } else { - const pinned = pinnedEvents.getContent().pinned; - const index = pinned.indexOf(this.props.mxEvent.getId()); - if (index !== -1) { - pinned.splice(index, 1); - MatrixClientPeg.get().sendStateEvent(this.props.mxRoom.roomId, 'm.room.pinned_events', {pinned}, '') - .then(() => { - if (this.props.onUnpinned) this.props.onUnpinned(); - }); - } else if (this.props.onUnpinned) this.props.onUnpinned(); - } - }; - - private canUnpin() { - return this.props.mxRoom.currentState.mayClientSendStateEvent('m.room.pinned_events', MatrixClientPeg.get()); - } - render() { - const sender = this.props.mxEvent.getSender(); - // Get the latest sender profile rather than historical - const senderProfile = this.props.mxRoom.getMember(sender); - const avatarSize = 40; + const sender = this.props.event.getSender(); + const senderProfile = this.props.room.getMember(sender); let unpinButton = null; - if (this.canUnpin()) { + if (this.props.onUnpinClicked) { unpinButton = ( - - {_t('Unpin - + ); } - return ( -
      -
      - - { _t("Jump to message") } - - { unpinButton } -
      + return
      + - - - - - { senderProfile ? senderProfile.name : sender } - - - { formatFullDate(new Date(this.props.mxEvent.getTs())) } - -
      - {}} // we need to give this, apparently - /> -
      + + { senderProfile?.name || sender } + + + { unpinButton } + +
      + {}} // we need to give this, apparently + />
      - ); + +
      + + { formatDate(new Date(this.props.event.getTs())) } + + + + { _t("View message") } + +
      +
      ; } } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 0aebfc48ce..d5b37a7d2d 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1509,7 +1509,7 @@ "Add a photo, so people can easily spot your room.": "Add a photo, so people can easily spot your room.", "This is the start of .": "This is the start of .", "Unpin Message": "Unpin Message", - "Jump to message": "Jump to message", + "View message": "View message", "%(duration)ss": "%(duration)ss", "%(duration)sm": "%(duration)sm", "%(duration)sh": "%(duration)sh", @@ -1717,8 +1717,7 @@ "Yours, or the other users’ session": "Yours, or the other users’ session", "You’re all caught up": "You’re all caught up", "You have no visible notifications.": "You have no visible notifications.", - "Pinned": "Pinned", - "Pinned Messages": "Pinned Messages", + "Pinned messages": "Pinned messages", "Room Info": "Room Info", "You can only pin up to %(count)s widgets|other": "You can only pin up to %(count)s widgets", "Unpin": "Unpin", @@ -1952,7 +1951,6 @@ "Rotate Right": "Rotate Right", "Download": "Download", "Information": "Information", - "View message": "View message", "Language Dropdown": "Language Dropdown", "%(nameList)s %(transitionList)s": "%(nameList)s %(transitionList)s", "%(severalUsers)sjoined %(count)s times|other": "%(severalUsers)sjoined %(count)s times", diff --git a/src/stores/RightPanelStorePhases.ts b/src/stores/RightPanelStorePhases.ts index 0d96f92945..d62f6c6110 100644 --- a/src/stores/RightPanelStorePhases.ts +++ b/src/stores/RightPanelStorePhases.ts @@ -44,6 +44,7 @@ export enum RightPanelPhases { export const RIGHT_PANEL_PHASES_NO_ARGS = [ RightPanelPhases.RoomSummary, RightPanelPhases.NotificationPanel, + RightPanelPhases.PinnedMessages, RightPanelPhases.FilePanel, RightPanelPhases.RoomMemberList, RightPanelPhases.GroupMemberList, From 54d8953024b1026fd636f2bed4e45cacda12a360 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 26 May 2021 14:00:53 +0100 Subject: [PATCH 248/999] delint --- res/css/views/rooms/_PinnedEventTile.scss | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/res/css/views/rooms/_PinnedEventTile.scss b/res/css/views/rooms/_PinnedEventTile.scss index 8e96f3caeb..15b3c16faa 100644 --- a/res/css/views/rooms/_PinnedEventTile.scss +++ b/res/css/views/rooms/_PinnedEventTile.scss @@ -20,9 +20,10 @@ limitations under the License. padding: 0 4px 12px; display: grid; - grid-template-areas: "avatar name remove" - "content content content" - "footer footer footer"; + grid-template-areas: + "avatar name remove" + "content content content" + "footer footer footer"; grid-template-rows: max-content auto max-content; grid-template-columns: 24px auto 24px; grid-row-gap: 12px; From 0758c09d9e9760d3a2bae3b562016ff73af82ec9 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 26 May 2021 14:06:12 +0100 Subject: [PATCH 249/999] i18n --- src/i18n/strings/en_EN.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index d5b37a7d2d..7c24364725 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1508,7 +1508,7 @@ "Invite to just this room": "Invite to just this room", "Add a photo, so people can easily spot your room.": "Add a photo, so people can easily spot your room.", "This is the start of .": "This is the start of .", - "Unpin Message": "Unpin Message", + "Unpin": "Unpin", "View message": "View message", "%(duration)ss": "%(duration)ss", "%(duration)sm": "%(duration)sm", @@ -1720,7 +1720,6 @@ "Pinned messages": "Pinned messages", "Room Info": "Room Info", "You can only pin up to %(count)s widgets|other": "You can only pin up to %(count)s widgets", - "Unpin": "Unpin", "Unpin a widget to view it in this panel": "Unpin a widget to view it in this panel", "Options": "Options", "Set my room layout for everyone": "Set my room layout for everyone", @@ -2464,6 +2463,7 @@ "Unable to reject invite": "Unable to reject invite", "Resend %(unsentCount)s reaction(s)": "Resend %(unsentCount)s reaction(s)", "Forward Message": "Forward Message", + "Unpin Message": "Unpin Message", "Pin Message": "Pin Message", "Unhide Preview": "Unhide Preview", "Share Permalink": "Share Permalink", From 1ffbaee560a94b6a310993d4a6ddc3e19ce05d90 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 26 May 2021 14:14:55 +0100 Subject: [PATCH 250/999] update style of imports in all modified files --- src/components/structures/TimelinePanel.js | 20 +++++----- .../views/context_menus/MessageContextMenu.js | 14 +++---- src/components/views/messages/TextualBody.js | 20 +++++----- src/components/views/right_panel/UserInfo.tsx | 38 +++++++++---------- .../views/rooms/BasicMessageComposer.tsx | 30 +++++++-------- .../views/rooms/EditMessageComposer.js | 22 +++++------ .../views/rooms/MessageComposer.tsx | 36 +++++++++--------- .../views/rooms/SendMessageComposer.js | 20 +++++----- src/editor/model.ts | 10 ++--- 9 files changed, 105 insertions(+), 105 deletions(-) diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index d6d29e1cde..c815b69abc 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -18,14 +18,14 @@ limitations under the License. */ import SettingsStore from "../../settings/SettingsStore"; -import {LayoutPropType} from "../../settings/Layout"; -import React, {createRef} from 'react'; +import { LayoutPropType } from "../../settings/Layout"; +import React, { createRef } from 'react'; import ReactDOM from "react-dom"; import PropTypes from 'prop-types'; -import {EventTimeline} from "matrix-js-sdk/src/models/event-timeline"; -import {TimelineWindow} from "matrix-js-sdk/src/timeline-window"; +import { EventTimeline } from "matrix-js-sdk/src/models/event-timeline"; +import { TimelineWindow } from "matrix-js-sdk/src/timeline-window"; import { _t } from '../../languageHandler'; -import {MatrixClientPeg} from "../../MatrixClientPeg"; +import { MatrixClientPeg } from "../../MatrixClientPeg"; import UserActivity from "../../UserActivity"; import Modal from "../../Modal"; import dis from "../../dispatcher/dispatcher"; @@ -34,12 +34,12 @@ import { Key } from '../../Keyboard'; import Timer from '../../utils/Timer'; import shouldHideEvent from '../../shouldHideEvent'; import EditorStateTransfer from '../../utils/EditorStateTransfer'; -import {haveTileForEvent} from "../views/rooms/EventTile"; -import {UIFeature} from "../../settings/UIFeature"; -import {objectHasDiff} from "../../utils/objects"; -import {replaceableComponent} from "../../utils/replaceableComponent"; +import { haveTileForEvent } from "../views/rooms/EventTile"; +import { UIFeature } from "../../settings/UIFeature"; +import { objectHasDiff } from "../../utils/objects"; +import { replaceableComponent } from "../../utils/replaceableComponent"; import { arrayFastClone } from "../../utils/arrays"; -import {Action} from "../../dispatcher/actions"; +import { Action } from "../../dispatcher/actions"; const PAGINATE_SIZE = 20; const INITIAL_SIZE = 20; diff --git a/src/components/views/context_menus/MessageContextMenu.js b/src/components/views/context_menus/MessageContextMenu.js index cb4632b2ac..9576d71e16 100644 --- a/src/components/views/context_menus/MessageContextMenu.js +++ b/src/components/views/context_menus/MessageContextMenu.js @@ -17,9 +17,9 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; -import {EventStatus} from 'matrix-js-sdk/src/models/event'; +import { EventStatus } from 'matrix-js-sdk/src/models/event'; -import {MatrixClientPeg} from '../../../MatrixClientPeg'; +import { MatrixClientPeg } from '../../../MatrixClientPeg'; import dis from '../../../dispatcher/dispatcher'; import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; @@ -28,11 +28,11 @@ import Resend from '../../../Resend'; import SettingsStore from '../../../settings/SettingsStore'; import { isUrlPermitted } from '../../../HtmlUtils'; import { isContentActionable } from '../../../utils/EventUtils'; -import {MenuItem} from "../../structures/ContextMenu"; -import {EventType} from "matrix-js-sdk/src/@types/event"; -import {replaceableComponent} from "../../../utils/replaceableComponent"; -import {ComposerInsertPayload} from "../../../dispatcher/payloads/ComposerInsertPayload"; -import {Action} from "../../../dispatcher/actions"; +import { MenuItem } from "../../structures/ContextMenu"; +import { EventType } from "matrix-js-sdk/src/@types/event"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload"; +import { Action } from "../../../dispatcher/actions"; export function canCancel(eventStatus) { return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT; diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js index 1c1f64ff37..eb4516b37a 100644 --- a/src/components/views/messages/TextualBody.js +++ b/src/components/views/messages/TextualBody.js @@ -16,12 +16,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, {createRef} from 'react'; +import React, { createRef } from 'react'; import ReactDOM from 'react-dom'; import PropTypes from 'prop-types'; import highlight from 'highlight.js'; import * as HtmlUtils from '../../../HtmlUtils'; -import {formatDate} from '../../../DateUtils'; +import { formatDate } from '../../../DateUtils'; import * as sdk from '../../../index'; import Modal from '../../../Modal'; import dis from '../../../dispatcher/dispatcher'; @@ -29,16 +29,16 @@ import { _t } from '../../../languageHandler'; import * as ContextMenu 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 { 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 { replaceableComponent } from "../../../utils/replaceableComponent"; import UIStore from "../../../stores/UIStore"; -import {ComposerInsertPayload} from "../../../dispatcher/payloads/ComposerInsertPayload"; -import {Action} from "../../../dispatcher/actions"; +import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload"; +import { Action } from "../../../dispatcher/actions"; @replaceableComponent("views.messages.TextualBody") export default class TextualBody extends React.Component { diff --git a/src/components/views/right_panel/UserInfo.tsx b/src/components/views/right_panel/UserInfo.tsx index 06f0fbff6a..1a9f95d4c4 100644 --- a/src/components/views/right_panel/UserInfo.tsx +++ b/src/components/views/right_panel/UserInfo.tsx @@ -17,18 +17,18 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, {useCallback, useContext, useEffect, useMemo, useState} from 'react'; +import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react'; import classNames from 'classnames'; -import {MatrixClient} from 'matrix-js-sdk/src/client'; -import {RoomMember} from 'matrix-js-sdk/src/models/room-member'; -import {User} from 'matrix-js-sdk/src/models/user'; -import {Room} from 'matrix-js-sdk/src/models/room'; -import {EventTimeline} from 'matrix-js-sdk/src/models/event-timeline'; -import {MatrixEvent} from 'matrix-js-sdk/src/models/event'; +import { MatrixClient } from 'matrix-js-sdk/src/client'; +import { RoomMember } from 'matrix-js-sdk/src/models/room-member'; +import { User } from 'matrix-js-sdk/src/models/user'; +import { Room } from 'matrix-js-sdk/src/models/room'; +import { EventTimeline } from 'matrix-js-sdk/src/models/event-timeline'; +import { MatrixEvent } from 'matrix-js-sdk/src/models/event'; import dis from '../../../dispatcher/dispatcher'; import Modal from '../../../Modal'; -import {_t} from '../../../languageHandler'; +import { _t } from '../../../languageHandler'; import createRoom, { findDMForUser, privateShouldBeEncrypted } from '../../../createRoom'; import DMRoomMap from '../../../utils/DMRoomMap'; import AccessibleButton from '../elements/AccessibleButton'; @@ -37,20 +37,20 @@ import SettingsStore from "../../../settings/SettingsStore"; import RoomViewStore from "../../../stores/RoomViewStore"; import MultiInviter from "../../../utils/MultiInviter"; import GroupStore from "../../../stores/GroupStore"; -import {MatrixClientPeg} from "../../../MatrixClientPeg"; +import { MatrixClientPeg } from "../../../MatrixClientPeg"; import E2EIcon from "../rooms/E2EIcon"; -import {useEventEmitter} from "../../../hooks/useEventEmitter"; -import {textualPowerLevel} from '../../../Roles'; +import { useEventEmitter } from "../../../hooks/useEventEmitter"; +import { textualPowerLevel } from '../../../Roles'; import MatrixClientContext from "../../../contexts/MatrixClientContext"; -import {RightPanelPhases} from "../../../stores/RightPanelStorePhases"; +import { RightPanelPhases } from "../../../stores/RightPanelStorePhases"; import EncryptionPanel from "./EncryptionPanel"; -import {useAsyncMemo} from '../../../hooks/useAsyncMemo'; -import {legacyVerifyUser, verifyDevice, verifyUser} from '../../../verification'; -import {Action} from "../../../dispatcher/actions"; +import { useAsyncMemo } from '../../../hooks/useAsyncMemo'; +import { legacyVerifyUser, verifyDevice, verifyUser } from '../../../verification'; +import { Action } from "../../../dispatcher/actions"; import { USER_SECURITY_TAB } from "../dialogs/UserSettingsDialog"; -import {useIsEncrypted} from "../../../hooks/useIsEncrypted"; +import { useIsEncrypted } from "../../../hooks/useIsEncrypted"; import BaseCard from "./BaseCard"; -import {E2EStatus} from "../../../utils/ShieldUtils"; +import { E2EStatus } from "../../../utils/ShieldUtils"; import ImageView from "../elements/ImageView"; import Spinner from "../elements/Spinner"; import PowerSelector from "../elements/PowerSelector"; @@ -65,9 +65,9 @@ import { EventType } from "matrix-js-sdk/src/@types/event"; import { SetRightPanelPhasePayload } from "../../../dispatcher/payloads/SetRightPanelPhasePayload"; import RoomAvatar from "../avatars/RoomAvatar"; import RoomName from "../elements/RoomName"; -import {mediaFromMxc} from "../../../customisations/Media"; +import { mediaFromMxc } from "../../../customisations/Media"; import UIStore from "../../../stores/UIStore"; -import {ComposerInsertPayload} from "../../../dispatcher/payloads/ComposerInsertPayload"; +import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload"; export interface IDevice { deviceId: string; diff --git a/src/components/views/rooms/BasicMessageComposer.tsx b/src/components/views/rooms/BasicMessageComposer.tsx index 31651d807d..3ab3865fb1 100644 --- a/src/components/views/rooms/BasicMessageComposer.tsx +++ b/src/components/views/rooms/BasicMessageComposer.tsx @@ -16,39 +16,39 @@ limitations under the License. */ import classNames from 'classnames'; -import React, {createRef, ClipboardEvent} from 'react'; -import {Room} from 'matrix-js-sdk/src/models/room'; -import {MatrixEvent} from 'matrix-js-sdk/src/models/event'; +import React, { createRef, ClipboardEvent } from 'react'; +import { Room } from 'matrix-js-sdk/src/models/room'; +import { MatrixEvent } from 'matrix-js-sdk/src/models/event'; import EMOTICON_REGEX from 'emojibase-regex/emoticon'; import EditorModel from '../../../editor/model'; import HistoryManager from '../../../editor/history'; -import {Caret, setSelection} from '../../../editor/caret'; +import { Caret, setSelection } from '../../../editor/caret'; import { formatRangeAsQuote, formatRangeAsCode, toggleInlineFormat, replaceRangeAndMoveCaret, } from '../../../editor/operations'; -import {getCaretOffsetAndText, getRangeForSelection} from '../../../editor/dom'; -import Autocomplete, {generateCompletionDomId} from '../rooms/Autocomplete'; -import {getAutoCompleteCreator} from '../../../editor/parts'; -import {parseEvent, parsePlainTextMessage} from '../../../editor/deserialize'; -import {renderModel} from '../../../editor/render'; +import { getCaretOffsetAndText, getRangeForSelection } from '../../../editor/dom'; +import Autocomplete, { generateCompletionDomId } from '../rooms/Autocomplete'; +import { getAutoCompleteCreator } from '../../../editor/parts'; +import { parseEvent, parsePlainTextMessage } from '../../../editor/deserialize'; +import { renderModel } from '../../../editor/render'; import TypingStore from "../../../stores/TypingStore"; import SettingsStore from "../../../settings/SettingsStore"; -import {Key} from "../../../Keyboard"; -import {EMOTICON_TO_EMOJI} from "../../../emoji"; -import {CommandCategories, CommandMap, parseCommandString} from "../../../SlashCommands"; +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 DocumentOffset from "../../../editor/offset"; -import {IDiff} from "../../../editor/diff"; +import { IDiff } from "../../../editor/diff"; import AutocompleteWrapperModel from "../../../editor/autocomplete"; import DocumentPosition from "../../../editor/position"; -import {ICompletion} from "../../../autocomplete/Autocompleter"; +import { ICompletion } from "../../../autocomplete/Autocompleter"; import { AutocompleteAction, getKeyBindingsManager, MessageComposerAction } from '../../../KeyBindingsManager'; -import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { replaceableComponent} from "../../../utils/replaceableComponent"; // matches emoticons which follow the start of a line or whitespace const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')\\s$'); diff --git a/src/components/views/rooms/EditMessageComposer.js b/src/components/views/rooms/EditMessageComposer.js index 7128085c1c..e11b3836d9 100644 --- a/src/components/views/rooms/EditMessageComposer.js +++ b/src/components/views/rooms/EditMessageComposer.js @@ -16,25 +16,25 @@ limitations under the License. */ import React from 'react'; import * as sdk from '../../../index'; -import {_t, _td} from '../../../languageHandler'; +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 { 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 EditorStateTransfer from '../../../utils/EditorStateTransfer'; import classNames from 'classnames'; -import {EventStatus} from 'matrix-js-sdk/src/models/event'; +import { EventStatus } from 'matrix-js-sdk/src/models/event'; import BasicMessageComposer from "./BasicMessageComposer"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; -import {CommandCategories, getCommand} from '../../../SlashCommands'; -import {Action} from "../../../dispatcher/actions"; +import { CommandCategories, getCommand } from '../../../SlashCommands'; +import { Action } from "../../../dispatcher/actions"; import CountlyAnalytics from "../../../CountlyAnalytics"; -import {getKeyBindingsManager, MessageComposerAction} from '../../../KeyBindingsManager'; -import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { getKeyBindingsManager, MessageComposerAction } from '../../../KeyBindingsManager'; +import { replaceableComponent } from "../../../utils/replaceableComponent"; import SendHistoryManager from '../../../SendHistoryManager'; import Modal from '../../../Modal'; diff --git a/src/components/views/rooms/MessageComposer.tsx b/src/components/views/rooms/MessageComposer.tsx index dc3e2658c0..f7d562fca0 100644 --- a/src/components/views/rooms/MessageComposer.tsx +++ b/src/components/views/rooms/MessageComposer.tsx @@ -15,34 +15,34 @@ limitations under the License. */ import React from 'react'; import classNames from 'classnames'; -import {_t} from '../../../languageHandler'; -import {MatrixClientPeg} from '../../../MatrixClientPeg'; +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"; +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"; import dis from '../../../dispatcher/dispatcher'; -import {ActionPayload} from "../../../dispatcher/payloads"; +import { ActionPayload } from "../../../dispatcher/payloads"; import Stickerpicker from './Stickerpicker'; -import {makeRoomPermalink, RoomPermalinkCreator} from '../../../utils/permalinks/Permalinks'; +import { makeRoomPermalink, RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks'; import ContentMessages from '../../../ContentMessages'; import E2EIcon from './E2EIcon'; import SettingsStore from "../../../settings/SettingsStore"; -import {aboveLeftOf, ContextMenu, ContextMenuTooltipButton, useContextMenu} from "../../structures/ContextMenu"; +import { aboveLeftOf, ContextMenu, ContextMenuTooltipButton, useContextMenu } from "../../structures/ContextMenu"; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import ReplyPreview from "./ReplyPreview"; -import {UIFeature} from "../../../settings/UIFeature"; -import {UPDATE_EVENT} from "../../../stores/AsyncStore"; -import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { UIFeature } from "../../../settings/UIFeature"; +import { UPDATE_EVENT } from "../../../stores/AsyncStore"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; import VoiceRecordComposerTile from "./VoiceRecordComposerTile"; -import {VoiceRecordingStore} from "../../../stores/VoiceRecordingStore"; -import {RecordingState} from "../../../voice/VoiceRecording"; -import Tooltip, {Alignment} from "../elements/Tooltip"; +import { VoiceRecordingStore } from "../../../stores/VoiceRecordingStore"; +import { RecordingState } from "../../../voice/VoiceRecording"; +import Tooltip, { Alignment } from "../elements/Tooltip"; import ResizeNotifier from "../../../utils/ResizeNotifier"; -import {E2EStatus} from '../../../utils/ShieldUtils'; +import { E2EStatus } from '../../../utils/ShieldUtils'; import SendMessageComposer from "./SendMessageComposer"; -import {ComposerInsertPayload} from "../../../dispatcher/payloads/ComposerInsertPayload"; -import {Action} from "../../../dispatcher/actions"; +import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload"; +import { Action } from "../../../dispatcher/actions"; interface IComposerAvatarProps { me: object; @@ -318,7 +318,7 @@ export default class MessageComposer extends React.Component { } } - addEmoji(emoji) { + addEmoji(emoji: string) { dis.dispatch({ action: Action.ComposerInsert, text: emoji, diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js index 44cd94460e..10ef91c689 100644 --- a/src/components/views/rooms/SendMessageComposer.js +++ b/src/components/views/rooms/SendMessageComposer.js @@ -27,26 +27,26 @@ import { startsWith, stripPrefix, } from '../../../editor/serialize'; -import {CommandPartCreator} from '../../../editor/parts'; +import { CommandPartCreator } from '../../../editor/parts'; import BasicMessageComposer from "./BasicMessageComposer"; import ReplyThread from "../elements/ReplyThread"; -import {findEditableEvent} from '../../../utils/EventUtils'; +import { findEditableEvent } from '../../../utils/EventUtils'; import SendHistoryManager from "../../../SendHistoryManager"; -import {CommandCategories, getCommand} from '../../../SlashCommands'; +import { CommandCategories, getCommand } from '../../../SlashCommands'; import * as sdk from '../../../index'; import Modal from '../../../Modal'; -import {_t, _td} from '../../../languageHandler'; +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'; +import { Action } from "../../../dispatcher/actions"; +import { containsEmoji } from "../../../effects/utils"; +import { CHAT_EFFECTS } from '../../../effects'; import CountlyAnalytics from "../../../CountlyAnalytics"; -import {MatrixClientPeg} from "../../../MatrixClientPeg"; +import { MatrixClientPeg } from "../../../MatrixClientPeg"; import EMOJI_REGEX from 'emojibase-regex'; -import {getKeyBindingsManager, MessageComposerAction} from '../../../KeyBindingsManager'; -import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { getKeyBindingsManager, MessageComposerAction } from '../../../KeyBindingsManager'; +import { replaceableComponent } from "../../../utils/replaceableComponent"; import SettingsStore from '../../../settings/SettingsStore'; function addReplyToMessageContent(content, repliedToEvent, permalinkCreator) { diff --git a/src/editor/model.ts b/src/editor/model.ts index c57e06c657..7925a43164 100644 --- a/src/editor/model.ts +++ b/src/editor/model.ts @@ -15,13 +15,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {diffAtCaret, diffDeletion, IDiff} from "./diff"; -import DocumentPosition, {IPosition} from "./position"; +import { diffAtCaret, diffDeletion, IDiff } from "./diff"; +import DocumentPosition, { IPosition } from "./position"; import Range from "./range"; -import {SerializedPart, Part, PartCreator} from "./parts"; -import AutocompleteWrapperModel, {ICallback} from "./autocomplete"; +import { SerializedPart, Part, PartCreator } from "./parts"; +import AutocompleteWrapperModel, { ICallback } from "./autocomplete"; import DocumentOffset from "./offset"; -import {Caret} from "./caret"; +import { Caret } from "./caret"; /** * @callback ModelCallback From 264ab925cd5da1abee7e5bde9b5be0611b3d75a1 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 26 May 2021 14:21:28 +0100 Subject: [PATCH 251/999] delint --- src/components/views/rooms/BasicMessageComposer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/BasicMessageComposer.tsx b/src/components/views/rooms/BasicMessageComposer.tsx index 3ab3865fb1..981ff1b4ae 100644 --- a/src/components/views/rooms/BasicMessageComposer.tsx +++ b/src/components/views/rooms/BasicMessageComposer.tsx @@ -48,7 +48,7 @@ import AutocompleteWrapperModel from "../../../editor/autocomplete"; import DocumentPosition from "../../../editor/position"; import { ICompletion } from "../../../autocomplete/Autocompleter"; import { AutocompleteAction, getKeyBindingsManager, MessageComposerAction } from '../../../KeyBindingsManager'; -import { replaceableComponent} from "../../../utils/replaceableComponent"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; // matches emoticons which follow the start of a line or whitespace const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')\\s$'); From 1ff870927a425145d8676e0e87a97feadc313efa Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 26 May 2021 15:40:24 +0100 Subject: [PATCH 252/999] When pinning a message automatically mark it as read --- .../views/context_menus/MessageContextMenu.js | 37 +++++++++---------- .../views/right_panel/PinnedMessagesCard.tsx | 2 +- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/src/components/views/context_menus/MessageContextMenu.js b/src/components/views/context_menus/MessageContextMenu.js index 365f2ab1de..d9f21906fe 100644 --- a/src/components/views/context_menus/MessageContextMenu.js +++ b/src/components/views/context_menus/MessageContextMenu.js @@ -31,6 +31,7 @@ import { isContentActionable } from '../../../utils/EventUtils'; import {MenuItem} from "../../structures/ContextMenu"; import {EventType} from "matrix-js-sdk/src/@types/event"; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { ReadPinsEventId } from "../right_panel/PinnedMessagesCard"; export function canCancel(eventStatus) { return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT; @@ -82,7 +83,7 @@ export default class MessageContextMenu extends React.Component { const canRedact = room.currentState.maySendRedactionForEvent(this.props.mxEvent, cli.credentials.userId) && this.props.mxEvent.getType() !== EventType.RoomServerAcl && this.props.mxEvent.getType() !== EventType.RoomEncryption; - let canPin = room.currentState.mayClientSendStateEvent('m.room.pinned_events', cli); + let canPin = room.currentState.mayClientSendStateEvent(EventType.RoomPinnedEvents, cli); // HACK: Intentionally say we can't pin if the user doesn't want to use the functionality if (!SettingsStore.getValue("feature_pinning")) canPin = false; @@ -92,7 +93,7 @@ export default class MessageContextMenu extends React.Component { _isPinned() { const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId()); - const pinnedEvent = room.currentState.getStateEvents('m.room.pinned_events', ''); + const pinnedEvent = room.currentState.getStateEvents(EventType.RoomPinnedEvents, ''); if (!pinnedEvent) return false; const content = pinnedEvent.getContent(); return content.pinned && Array.isArray(content.pinned) && content.pinned.includes(this.props.mxEvent.getId()); @@ -165,25 +166,23 @@ export default class MessageContextMenu extends React.Component { }; onPinClick = () => { - MatrixClientPeg.get().getStateEvent(this.props.mxEvent.getRoomId(), 'm.room.pinned_events', '') - .catch((e) => { - // Intercept the Event Not Found error and fall through the promise chain with no event. - if (e.errcode === "M_NOT_FOUND") return null; - throw e; - }) - .then((event) => { - const eventIds = (event ? event.pinned : []) || []; - if (!eventIds.includes(this.props.mxEvent.getId())) { - // Not pinned - add - eventIds.push(this.props.mxEvent.getId()); - } else { - // Pinned - remove - eventIds.splice(eventIds.indexOf(this.props.mxEvent.getId()), 1); - } + const cli = MatrixClientPeg.get(); + const room = cli.getRoom(this.props.mxEvent.getRoomId()); + const eventId = this.props.mxEvent.getId(); - const cli = MatrixClientPeg.get(); - cli.sendStateEvent(this.props.mxEvent.getRoomId(), 'm.room.pinned_events', {pinned: eventIds}, ''); + const pinnedIds = room?.currentState?.getStateEvents(EventType.RoomPinnedEvents, "")?.pinned || []; + if (pinnedIds.includes(eventId)) { + pinnedIds.splice(pinnedIds.indexOf(eventId), 1); + } else { + pinnedIds.push(eventId); + cli.setRoomAccountData(room.roomId, ReadPinsEventId, { + event_ids: [ + ...room.getAccountData(ReadPinsEventId)?.getContent()?.event_ids, + eventId, + ], }); + } + cli.sendStateEvent(this.props.mxEvent.getRoomId(), EventType.RoomPinnedEvents, { pinned: pinnedIds }, ""); this.closeMenu(); }; diff --git a/src/components/views/right_panel/PinnedMessagesCard.tsx b/src/components/views/right_panel/PinnedMessagesCard.tsx index 48be8cd706..a3f1f2d9df 100644 --- a/src/components/views/right_panel/PinnedMessagesCard.tsx +++ b/src/components/views/right_panel/PinnedMessagesCard.tsx @@ -53,7 +53,7 @@ export const usePinnedEvents = (room: Room): string[] => { return pinnedEvents; }; -const ReadPinsEventId = "im.vector.room.read_pins"; +export const ReadPinsEventId = "im.vector.room.read_pins"; export const useReadPinnedEvents = (room: Room): Set => { const [readPinnedEvents, setReadPinnedEvents] = useState>(new Set()); From 3f10279e152c20b05569e53a9e60d7b65d62871d Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 26 May 2021 16:38:02 +0100 Subject: [PATCH 253/999] Invite Dialog don't show warning modals after unmount, it is jarring --- src/components/views/dialogs/InviteDialog.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/components/views/dialogs/InviteDialog.tsx b/src/components/views/dialogs/InviteDialog.tsx index ec9c71ccbe..868d89bfa4 100644 --- a/src/components/views/dialogs/InviteDialog.tsx +++ b/src/components/views/dialogs/InviteDialog.tsx @@ -351,6 +351,7 @@ export default class InviteDialog extends React.PureComponent { this.setState({consultFirst: ev.target.checked}); } @@ -1027,6 +1032,7 @@ export default class InviteDialog extends React.PureComponent 0) { const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog'); From d4ca1babbe5228d028ebcc9ca1e89f66f1955337 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Wed, 26 May 2021 16:47:21 +0100 Subject: [PATCH 254/999] Update reactions row on event decryption This fixes a race (perhaps revealed by the recent lazy decryption work) where the reactions row have reactions to show, but the event would not be decrypted, so they wouldn't render. Adding a decryption listener gets things moving again. Fixes https://github.com/vector-im/element-web/issues/17461 --- .../views/messages/ReactionsRow.tsx | 50 +++++++++++-------- 1 file changed, 29 insertions(+), 21 deletions(-) diff --git a/src/components/views/messages/ReactionsRow.tsx b/src/components/views/messages/ReactionsRow.tsx index f5910473ff..797f997d27 100644 --- a/src/components/views/messages/ReactionsRow.tsx +++ b/src/components/views/messages/ReactionsRow.tsx @@ -81,18 +81,38 @@ export default class ReactionsRow extends React.PureComponent { constructor(props, context) { super(props, context); - if (props.reactions) { - props.reactions.on("Relations.add", this.onReactionsChange); - props.reactions.on("Relations.remove", this.onReactionsChange); - props.reactions.on("Relations.redaction", this.onReactionsChange); - } - this.state = { myReactions: this.getMyReactions(), showAll: false, }; } + componentDidMount() { + const { mxEvent, reactions } = this.props; + + if (mxEvent.isBeingDecrypted() || mxEvent.shouldAttemptDecryption()) { + mxEvent.once("Event.decrypted", this.onDecrypted); + } + + if (reactions) { + reactions.on("Relations.add", this.onReactionsChange); + reactions.on("Relations.remove", this.onReactionsChange); + reactions.on("Relations.redaction", this.onReactionsChange); + } + } + + componentWillUnmount() { + const { mxEvent, reactions } = this.props; + + mxEvent.off("Event.decrypted", this.onDecrypted); + + if (reactions) { + reactions.off("Relations.add", this.onReactionsChange); + reactions.off("Relations.remove", this.onReactionsChange); + reactions.off("Relations.redaction", this.onReactionsChange); + } + } + componentDidUpdate(prevProps) { if (prevProps.reactions !== this.props.reactions) { this.props.reactions.on("Relations.add", this.onReactionsChange); @@ -102,21 +122,9 @@ export default class ReactionsRow extends React.PureComponent { } } - componentWillUnmount() { - if (this.props.reactions) { - this.props.reactions.removeListener( - "Relations.add", - this.onReactionsChange, - ); - this.props.reactions.removeListener( - "Relations.remove", - this.onReactionsChange, - ); - this.props.reactions.removeListener( - "Relations.redaction", - this.onReactionsChange, - ); - } + private onDecrypted = () => { + // Decryption changes whether the event is actionable + this.forceUpdate(); } onReactionsChange = () => { From 60d161caf58e63f41650af9286bb03d08440ba65 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 26 May 2021 16:47:46 +0100 Subject: [PATCH 255/999] Apply some actual typescripting to this file --- src/components/views/dialogs/InviteDialog.tsx | 195 +++++++++--------- 1 file changed, 96 insertions(+), 99 deletions(-) diff --git a/src/components/views/dialogs/InviteDialog.tsx b/src/components/views/dialogs/InviteDialog.tsx index 868d89bfa4..b9a5a68f83 100644 --- a/src/components/views/dialogs/InviteDialog.tsx +++ b/src/components/views/dialogs/InviteDialog.tsx @@ -47,6 +47,8 @@ import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call'; import {replaceableComponent} from "../../../utils/replaceableComponent"; import {mediaFromMxc} from "../../../customisations/Media"; import {getAddressType} from "../../../UserAddress"; +import BaseAvatar from '../avatars/BaseAvatar'; +import AccessibleButton from '../elements/AccessibleButton'; // we have a number of types defined from the Matrix spec which can't reasonably be altered here. /* eslint-disable camelcase */ @@ -61,43 +63,41 @@ const INCREMENT_ROOMS_SHOWN = 5; // Number of rooms to add when 'show more' is c // This is the interface that is expected by various components in this file. It is a bit // awkward because it also matches the RoomMember class from the js-sdk with some extra support // for 3PIDs/email addresses. -// -// XXX: We should use TypeScript interfaces instead of this weird "abstract" class. -class Member { +abstract class Member { /** * The display name of this Member. For users this should be their profile's display * name or user ID if none set. For 3PIDs this should be the 3PID address (email). */ - get name(): string { throw new Error("Member class not implemented"); } + public abstract get name(): string; /** * The ID of this Member. For users this should be their user ID. For 3PIDs this should * be the 3PID address (email). */ - get userId(): string { throw new Error("Member class not implemented"); } + public abstract get userId(): string; /** * Gets the MXC URL of this Member's avatar. For users this should be their profile's * avatar MXC URL or null if none set. For 3PIDs this should always be null. */ - getMxcAvatarUrl(): string { throw new Error("Member class not implemented"); } + public abstract getMxcAvatarUrl(): string; } class DirectoryMember extends Member { - _userId: string; - _displayName: string; - _avatarUrl: string; + private readonly _userId: string; + private readonly displayName: string; + private readonly avatarUrl: string; constructor(userDirResult: {user_id: string, display_name: string, avatar_url: string}) { super(); this._userId = userDirResult.user_id; - this._displayName = userDirResult.display_name; - this._avatarUrl = userDirResult.avatar_url; + this.displayName = userDirResult.display_name; + this.avatarUrl = userDirResult.avatar_url; } // These next class members are for the Member interface get name(): string { - return this._displayName || this._userId; + return this.displayName || this._userId; } get userId(): string { @@ -105,32 +105,32 @@ class DirectoryMember extends Member { } getMxcAvatarUrl(): string { - return this._avatarUrl; + return this.avatarUrl; } } class ThreepidMember extends Member { - _id: string; + private readonly id: string; constructor(id: string) { super(); - this._id = id; + this.id = id; } // This is a getter that would be falsey on all other implementations. Until we have // better type support in the react-sdk we can use this trick to determine the kind // of 3PID we're dealing with, if any. get isEmail(): boolean { - return this._id.includes('@'); + return this.id.includes('@'); } // These next class members are for the Member interface get name(): string { - return this._id; + return this.id; } get userId(): string { - return this._id; + return this.id; } getMxcAvatarUrl(): string { @@ -140,11 +140,11 @@ class ThreepidMember extends Member { interface IDMUserTileProps { member: RoomMember; - onRemove: (RoomMember) => any; + onRemove(member: RoomMember): void; } class DMUserTile extends React.PureComponent { - _onRemove = (e) => { + private onRemove = (e) => { // Stop the browser from highlighting text e.preventDefault(); e.stopPropagation(); @@ -153,9 +153,6 @@ class DMUserTile extends React.PureComponent { }; render() { - const BaseAvatar = sdk.getComponent("views.avatars.BaseAvatar"); - const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); - const avatarSize = 20; const avatar = this.props.member.isEmail ? { closeButton = ( {_t('Remove')} { interface IDMRoomTileProps { member: RoomMember; lastActiveTs: number; - onToggle: (RoomMember) => any; + onToggle(member: RoomMember): void; highlightWord: string; isSelected: boolean; } class DMRoomTile extends React.PureComponent { - _onClick = (e) => { + private onClick = (e) => { // Stop the browser from highlighting text e.preventDefault(); e.stopPropagation(); @@ -215,7 +212,7 @@ class DMRoomTile extends React.PureComponent { this.props.onToggle(this.props.member); }; - _highlightName(str: string) { + private highlightName(str: string) { if (!this.props.highlightWord) return str; // We convert things to lowercase for index searching, but pull substrings from @@ -252,8 +249,6 @@ class DMRoomTile extends React.PureComponent { } render() { - const BaseAvatar = sdk.getComponent("views.avatars.BaseAvatar"); - let timestamp = null; if (this.props.lastActiveTs) { const humanTs = humanizeTime(this.props.lastActiveTs); @@ -291,13 +286,13 @@ class DMRoomTile extends React.PureComponent { const caption = this.props.member.isEmail ? _t("Invite by email") - : this._highlightName(this.props.member.userId); + : this.highlightName(this.props.member.userId); return ( -
      +
      {stackedAvatar} -
      {this._highlightName(this.props.member.name)}
      +
      {this.highlightName(this.props.member.name)}
      {caption}
      {timestamp} @@ -308,7 +303,7 @@ class DMRoomTile extends React.PureComponent { interface IInviteDialogProps { // Takes an array of user IDs/emails to invite. - onFinished: (toInvite?: string[]) => any; + onFinished: (toInvite?: string[]) => void; // The kind of invite being performed. Assumed to be KIND_DM if // not provided. @@ -349,8 +344,8 @@ export default class InviteDialog extends React.PureComponent(); private unmounted = false; constructor(props) { @@ -379,7 +374,7 @@ export default class InviteDialog extends React.PureComponent): {userId: string, user: RoomMember, lastActive: number}[] { + public static buildRecents(excludedTargetIds: Set): { + userId: string, + user: RoomMember, + lastActive: number, + }[] { const rooms = DMRoomMap.shared().getUniqueRoomsWithIndividuals(); // map of userId => js-sdk Room // Also pull in all the rooms tagged as DefaultTagID.DM so we don't miss anything. Sometimes the @@ -472,7 +469,7 @@ export default class InviteDialog extends React.PureComponent): {userId: string, user: RoomMember}[] { + private buildSuggestions(excludedTargetIds: Set): {userId: string, user: RoomMember}[] { const maxConsideredMembers = 200; const joinedRooms = MatrixClientPeg.get().getRooms() .filter(r => r.getMyMembership() === 'join' && r.getJoinedMemberCount() <= maxConsideredMembers); @@ -590,7 +587,7 @@ export default class InviteDialog extends React.PureComponent ({userId: m.member.userId, user: m.member})); } - _shouldAbortAfterInviteError(result): boolean { + private shouldAbortAfterInviteError(result): boolean { const failedUsers = Object.keys(result.states).filter(a => result.states[a] === 'error'); if (failedUsers.length > 0) { console.log("Failed to invite users: ", result); @@ -605,7 +602,7 @@ export default class InviteDialog extends React.PureComponent { + private startDm = async () => { this.setState({busy: true}); const client = MatrixClientPeg.get(); - const targets = this._convertFilter(); + const targets = this.convertFilter(); const targetIds = targets.map(t => t.userId); // Check if there is already a DM with these people and reuse it if possible. @@ -699,11 +696,11 @@ export default class InviteDialog extends React.PureComponent { + private inviteUsers = async () => { const startTime = CountlyAnalytics.getTimestamp(); this.setState({busy: true}); - this._convertFilter(); - const targets = this._convertFilter(); + this.convertFilter(); + const targets = this.convertFilter(); const targetIds = targets.map(t => t.userId); const cli = MatrixClientPeg.get(); @@ -720,7 +717,7 @@ export default class InviteDialog extends React.PureComponent { - this._convertFilter(); - const targets = this._convertFilter(); + private transferCall = async () => { + this.convertFilter(); + const targets = this.convertFilter(); const targetIds = targets.map(t => t.userId); if (targetIds.length > 1) { this.setState({ @@ -795,26 +792,26 @@ export default class InviteDialog extends React.PureComponent { + private onKeyDown = (e) => { if (this.state.busy) return; const value = e.target.value.trim(); const hasModifiers = e.ctrlKey || e.shiftKey || e.metaKey; if (!value && this.state.targets.length > 0 && e.key === Key.BACKSPACE && !hasModifiers) { // when the field is empty and the user hits backspace remove the right-most target e.preventDefault(); - this._removeMember(this.state.targets[this.state.targets.length - 1]); + this.removeMember(this.state.targets[this.state.targets.length - 1]); } else if (value && e.key === Key.ENTER && !hasModifiers) { // when the user hits enter with something in their field try to convert it e.preventDefault(); - this._convertFilter(); + this.convertFilter(); } else if (value && e.key === Key.SPACE && !hasModifiers && value.includes("@") && !value.includes(" ")) { // when the user hits space and their input looks like an e-mail/MXID then try to convert it e.preventDefault(); - this._convertFilter(); + this.convertFilter(); } }; - _updateSuggestions = async (term) => { + private updateSuggestions = async (term) => { MatrixClientPeg.get().searchUserDirectory({term}).then(async r => { if (term !== this.state.filterText) { // Discard the results - we were probably too slow on the server-side to make @@ -923,30 +920,30 @@ export default class InviteDialog extends React.PureComponent { + private updateFilter = (e) => { const term = e.target.value; this.setState({filterText: term}); // Debounce server lookups to reduce spam. We don't clear the existing server // results because they might still be vaguely accurate, likewise for races which // could happen here. - if (this._debounceTimer) { - clearTimeout(this._debounceTimer); + if (this.debounceTimer) { + clearTimeout(this.debounceTimer); } - this._debounceTimer = setTimeout(() => { - this._updateSuggestions(term); + this.debounceTimer = setTimeout(() => { + this.updateSuggestions(term); }, 150); // 150ms debounce (human reaction time + some) }; - _showMoreRecents = () => { + private showMoreRecents = () => { this.setState({numRecentsShown: this.state.numRecentsShown + INCREMENT_ROOMS_SHOWN}); }; - _showMoreSuggestions = () => { + private showMoreSuggestions = () => { this.setState({numSuggestionsShown: this.state.numSuggestionsShown + INCREMENT_ROOMS_SHOWN}); }; - _toggleMember = (member: Member) => { + private toggleMember = (member: Member) => { if (!this.state.busy) { let filterText = this.state.filterText; const targets = this.state.targets.map(t => t); // cheap clone for mutation @@ -959,13 +956,13 @@ export default class InviteDialog extends React.PureComponent { + private removeMember = (member: Member) => { const targets = this.state.targets.map(t => t); // cheap clone for mutation const idx = targets.indexOf(member); if (idx >= 0) { @@ -973,12 +970,12 @@ export default class InviteDialog extends React.PureComponent { + private onPaste = async (e) => { if (this.state.filterText) { // if the user has already typed something, just let them // paste normally. @@ -1049,17 +1046,17 @@ export default class InviteDialog extends React.PureComponent { + private onClickInputArea = (e) => { // Stop the browser from highlighting text e.preventDefault(); e.stopPropagation(); - if (this._editorRef && this._editorRef.current) { - this._editorRef.current.focus(); + if (this.editorRef && this.editorRef.current) { + this.editorRef.current.focus(); } }; - _onUseDefaultIdentityServerClick = (e) => { + private onUseDefaultIdentityServerClick = (e) => { e.preventDefault(); // Update the IS in account data. Actually using it may trigger terms. @@ -1068,21 +1065,21 @@ export default class InviteDialog extends React.PureComponent { + private onManageSettingsClick = (e) => { e.preventDefault(); dis.fire(Action.ViewUserSettings); this.props.onFinished(); }; - _onCommunityInviteClick = (e) => { + private onCommunityInviteClick = (e) => { this.props.onFinished(); showCommunityInviteDialog(CommunityPrototypeStore.instance.getSelectedCommunityId()); }; - _renderSection(kind: "recents"|"suggestions") { + private renderSection(kind: "recents"|"suggestions") { let sourceMembers = kind === 'recents' ? this.state.recents : this.state.suggestions; let showNum = kind === 'recents' ? this.state.numRecentsShown : this.state.numSuggestionsShown; - const showMoreFn = kind === 'recents' ? this._showMoreRecents.bind(this) : this._showMoreSuggestions.bind(this); + const showMoreFn = kind === 'recents' ? this.showMoreRecents.bind(this) : this.showMoreSuggestions.bind(this); const lastActive = (m) => kind === 'recents' ? m.lastActive : null; let sectionName = kind === 'recents' ? _t("Recent Conversations") : _t("Suggestions"); let sectionSubname = null; @@ -1162,7 +1159,7 @@ export default class InviteDialog extends React.PureComponent t.userId === r.userId)} /> @@ -1177,32 +1174,32 @@ export default class InviteDialog extends React.PureComponent ( - + )); const input = ( ); return ( -
      +
      {targets} {input}
      ); } - _renderIdentityServerWarning() { + private renderIdentityServerWarning() { if (!this.state.tryingIdentityServer || this.state.canUseIdentityServer || !SettingsStore.getValue(UIFeature.IdentityServer) ) { @@ -1220,8 +1217,8 @@ export default class InviteDialog extends React.PureComponent {sub}, - settings: sub => {sub}, + default: sub => {sub}, + settings: sub => {sub}, }, )}
      ); @@ -1231,7 +1228,7 @@ export default class InviteDialog extends React.PureComponentSettings.", {}, { - settings: sub => {sub}, + settings: sub => {sub}, }, )}
      ); @@ -1304,7 +1301,7 @@ export default class InviteDialog extends React.PureComponent{sub} ); }, @@ -1315,7 +1312,7 @@ export default class InviteDialog extends React.PureComponent; } buttonText = _t("Go"); - goButtonFn = this._startDm; + goButtonFn = this.startDm; } else if (this.props.kind === KIND_INVITE) { const room = MatrixClientPeg.get()?.getRoom(this.props.roomId); const isSpace = SettingsStore.getValue("feature_spaces") && room?.isSpaceRoom(); @@ -1354,7 +1351,7 @@ export default class InviteDialog extends React.PureComponent
      { { - setPanelCollapsed(!isPanelCollapsed); - if (menuDisplayed) closeMenu(); - }} + onClick={() => setPanelCollapsed(!isPanelCollapsed)} title={expandCollapseButtonTitle} /> { contextMenu } From be22a325f6d42b5d52b666462449b6aae30f8b2e Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Thu, 27 May 2021 08:57:27 +0100 Subject: [PATCH 258/999] Prevent having duplicates in pending room state --- src/components/structures/UserMenu.tsx | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx index c05f74a436..32fd3aa471 100644 --- a/src/components/structures/UserMenu.tsx +++ b/src/components/structures/UserMenu.tsx @@ -69,7 +69,7 @@ interface IState { contextMenuPosition: PartialDOMRect; isDarkTheme: boolean; selectedSpace?: Room; - pendingRoomJoin: string[] + pendingRoomJoin: Set } @replaceableComponent("structures.UserMenu") @@ -86,7 +86,7 @@ export default class UserMenu extends React.Component { this.state = { contextMenuPosition: null, isDarkTheme: this.isUserOnDarkTheme(), - pendingRoomJoin: [], + pendingRoomJoin: new Set(), }; OwnProfileStore.instance.on(UPDATE_EVENT, this.onProfileUpdate); @@ -168,28 +168,24 @@ export default class UserMenu extends React.Component { } }; - private addPendingJoinRoom(roomId) { + private addPendingJoinRoom(roomId: string): void { this.setState({ - pendingRoomJoin: [ - ...this.state.pendingRoomJoin, - roomId, - ], + pendingRoomJoin: new Set(this.state.pendingRoomJoin) + .add(roomId), }); } - private removePendingJoinRoom(roomId) { - const newPendingRoomJoin = this.state.pendingRoomJoin.filter(pendingJoinRoomId => { - return pendingJoinRoomId !== roomId; - }); - if (newPendingRoomJoin.length !== this.state.pendingRoomJoin.length) { + private removePendingJoinRoom(roomId: string): void { + if (this.state.pendingRoomJoin.has(roomId)) { + this.state.pendingRoomJoin.delete(roomId); this.setState({ - pendingRoomJoin: newPendingRoomJoin, + pendingRoomJoin: new Set(this.state.pendingRoomJoin), }) } } get hasPendingActions(): boolean { - return this.state.pendingRoomJoin.length > 0; + return Array.from(this.state.pendingRoomJoin).length > 0; } private onOpenMenuClick = (ev: React.MouseEvent) => { From 9007afabfa7b12c9b1358c9f9c4584b49b71c3f1 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Thu, 27 May 2021 08:57:48 +0100 Subject: [PATCH 259/999] Fix JoinRoomError action name typo --- 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 0c9a4160b5..9fc0b54eea 100644 --- a/src/dispatcher/actions.ts +++ b/src/dispatcher/actions.ts @@ -152,5 +152,5 @@ export enum Action { /** * Fired when joining a room failed */ - JoinRoomError = "join_room", + JoinRoomError = "join_room_error", } From 2d15d66df81de03f502d1c32d83c623f38384041 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Thu, 27 May 2021 08:58:11 +0100 Subject: [PATCH 260/999] Listen to home server sync update to remove pending rooms --- src/components/structures/UserMenu.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx index 32fd3aa471..7543dff953 100644 --- a/src/components/structures/UserMenu.tsx +++ b/src/components/structures/UserMenu.tsx @@ -106,6 +106,7 @@ export default class UserMenu extends React.Component { this.dispatcherRef = defaultDispatcher.register(this.onAction); this.themeWatcherRef = SettingsStore.watchSetting("theme", null, this.onThemeChanged); this.tagStoreRef = GroupFilterOrderStore.addListener(this.onTagStoreUpdate); + MatrixClientPeg.get().on("Room", this.onRoom); } public componentWillUnmount() { @@ -117,6 +118,11 @@ export default class UserMenu extends React.Component { if (SettingsStore.getValue("feature_spaces")) { SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdate); } + MatrixClientPeg.get().removeListener("Room", this.onRoom); + } + + private onRoom = (room: Room): void => { + this.removePendingJoinRoom(room.roomId); } private onTagStoreUpdate = () => { From fbb6a42d86b3f84fbe248752c80cf2129f6f2e28 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Thu, 27 May 2021 09:05:51 +0100 Subject: [PATCH 261/999] fix reading Set length --- src/components/structures/UserMenu.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx index 7543dff953..cc50db02ee 100644 --- a/src/components/structures/UserMenu.tsx +++ b/src/components/structures/UserMenu.tsx @@ -190,8 +190,8 @@ export default class UserMenu extends React.Component { } } - get hasPendingActions(): boolean { - return Array.from(this.state.pendingRoomJoin).length > 0; + get pendingActionsCount(): number { + return Array.from(this.state.pendingRoomJoin).length; } private onOpenMenuClick = (ev: React.MouseEvent) => { @@ -655,11 +655,11 @@ export default class UserMenu extends React.Component { /> {name} - {this.hasPendingActions && ( + {this.pendingActionsCount > 0 && ( )} From bd653ac5a89b1a47b7182c73bc5464f835645f07 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 27 May 2021 09:11:43 +0100 Subject: [PATCH 262/999] fix edge cases around space panel auto collapsing/closing menu --- src/components/views/spaces/SpacePanel.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/views/spaces/SpacePanel.tsx b/src/components/views/spaces/SpacePanel.tsx index aac1f609d5..eb63b21f0e 100644 --- a/src/components/views/spaces/SpacePanel.tsx +++ b/src/components/views/spaces/SpacePanel.tsx @@ -128,7 +128,9 @@ const SpacePanel = () => { const [isPanelCollapsed, setPanelCollapsed] = useState(true); useEffect(() => { - closeMenu(); + if (!isPanelCollapsed && menuDisplayed) { + closeMenu(); + } }, [isPanelCollapsed]); // eslint-disable-line react-hooks/exhaustive-deps const newClasses = classNames("mx_SpaceButton_new", { @@ -239,8 +241,8 @@ const SpacePanel = () => { className={newClasses} tooltip={menuDisplayed ? _t("Cancel") : _t("Create a space")} onClick={menuDisplayed ? closeMenu : () => { - openMenu(); if (!isPanelCollapsed) setPanelCollapsed(true); + openMenu(); }} isNarrow={isPanelCollapsed} /> From b8a7d5d730094f5442d063ebfdb9bff6df38dcb3 Mon Sep 17 00:00:00 2001 From: Germain Date: Thu, 27 May 2021 09:23:56 +0100 Subject: [PATCH 263/999] Better Set handling Co-authored-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/UserMenu.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx index cc50db02ee..ff00905a59 100644 --- a/src/components/structures/UserMenu.tsx +++ b/src/components/structures/UserMenu.tsx @@ -69,7 +69,7 @@ interface IState { contextMenuPosition: PartialDOMRect; isDarkTheme: boolean; selectedSpace?: Room; - pendingRoomJoin: Set + pendingRoomJoin: Set; } @replaceableComponent("structures.UserMenu") @@ -182,8 +182,7 @@ export default class UserMenu extends React.Component { } private removePendingJoinRoom(roomId: string): void { - if (this.state.pendingRoomJoin.has(roomId)) { - this.state.pendingRoomJoin.delete(roomId); + if (this.state.pendingRoomJoin.delete(roomId)) { this.setState({ pendingRoomJoin: new Set(this.state.pendingRoomJoin), }) From f31ec343f4b587d9fb928653418ccbb03e00158c Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Thu, 27 May 2021 09:26:26 +0100 Subject: [PATCH 264/999] Use Set::size instead of Array.from()::length --- src/components/structures/UserMenu.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx index ff00905a59..fb4829f879 100644 --- a/src/components/structures/UserMenu.tsx +++ b/src/components/structures/UserMenu.tsx @@ -189,10 +189,6 @@ export default class UserMenu extends React.Component { } } - get pendingActionsCount(): number { - return Array.from(this.state.pendingRoomJoin).length; - } - private onOpenMenuClick = (ev: React.MouseEvent) => { ev.preventDefault(); ev.stopPropagation(); @@ -654,11 +650,11 @@ export default class UserMenu extends React.Component { /> {name} - {this.pendingActionsCount > 0 && ( + {this.state.pendingRoomJoin.size > 0 && ( )} From d6d09227530da472ba5ecd9de99c32891ed9e82c Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 27 May 2021 10:11:28 +0100 Subject: [PATCH 265/999] Fix misleading child counts in spaces --- src/components/structures/SpaceRoomDirectory.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/structures/SpaceRoomDirectory.tsx b/src/components/structures/SpaceRoomDirectory.tsx index 3985553b20..8d59fe6c68 100644 --- a/src/components/structures/SpaceRoomDirectory.tsx +++ b/src/components/structures/SpaceRoomDirectory.tsx @@ -319,7 +319,7 @@ export const HierarchyLevel = ({ key={roomId} room={rooms.get(roomId)} numChildRooms={Array.from(relations.get(roomId)?.values() || []) - .filter(ev => rooms.get(ev.state_key)?.room_type !== RoomType.Space).length} + .filter(ev => rooms.has(ev.state_key) && !rooms.get(ev.state_key).room_type).length} suggested={relations.get(spaceId)?.get(roomId)?.content.suggested} selected={selectedMap?.get(spaceId)?.has(roomId)} onViewRoomClick={(autoJoin) => { @@ -437,7 +437,7 @@ export const SpaceHierarchy: React.FC = ({ let content; if (roomsMap) { - const numRooms = Array.from(roomsMap.values()).filter(r => r.room_type !== RoomType.Space).length; + const numRooms = Array.from(roomsMap.values()).filter(r => !r.room_type).length; const numSpaces = roomsMap.size - numRooms - 1; // -1 at the end to exclude the space we are looking at let countsStr; From fcae19f8318a3bf0bd8908162e56bd0b3d2f3884 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Thu, 27 May 2021 12:36:16 +0100 Subject: [PATCH 266/999] Track left panel width using ResizeObserver --- src/@types/global.d.ts | 2 + src/components/structures/LeftPanel.tsx | 8 +++- src/stores/UIStore.ts | 53 +++++++++++++++++++++---- 3 files changed, 55 insertions(+), 8 deletions(-) diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index 63966d96fa..22280b8a28 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -43,6 +43,7 @@ import TypingStore from "../stores/TypingStore"; import { EventIndexPeg } from "../indexing/EventIndexPeg"; import {VoiceRecordingStore} from "../stores/VoiceRecordingStore"; import PerformanceMonitor from "../performance"; +import UIStore from "../stores/UIStore"; declare global { interface Window { @@ -82,6 +83,7 @@ declare global { mxEventIndexPeg: EventIndexPeg; mxPerformanceMonitor: PerformanceMonitor; mxPerformanceEntryNames: any; + mxUIStore: UIStore; } interface Document { diff --git a/src/components/structures/LeftPanel.tsx b/src/components/structures/LeftPanel.tsx index 22c60bff1e..80cd9bc465 100644 --- a/src/components/structures/LeftPanel.tsx +++ b/src/components/structures/LeftPanel.tsx @@ -67,6 +67,7 @@ const cssClasses = [ @replaceableComponent("structures.LeftPanel") export default class LeftPanel extends React.Component { + private ref: React.RefObject = createRef(); private listContainerRef: React.RefObject = createRef(); private groupFilterPanelWatcherRef: string; private bgImageWatcherRef: string; @@ -93,6 +94,10 @@ export default class LeftPanel extends React.Component { }); } + public componentDidMount() { + UIStore.instance.trackElementDimensions("LeftPanel", this.ref.current); + } + public componentWillUnmount() { SettingsStore.unwatchSetting(this.groupFilterPanelWatcherRef); SettingsStore.unwatchSetting(this.bgImageWatcherRef); @@ -100,6 +105,7 @@ export default class LeftPanel extends React.Component { RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.onBreadcrumbsUpdate); OwnProfileStore.instance.off(UPDATE_EVENT, this.onBackgroundImageUpdate); SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.updateActiveSpace); + UIStore.instance.stopTrackingElementDimensions("LeftPanel"); } private updateActiveSpace = (activeSpace: Room) => { @@ -420,7 +426,7 @@ export default class LeftPanel extends React.Component { ); return ( -
      +
      {leftLeftPanel}
      ; + const link = makeUserPermalink(MatrixClientPeg.get().getUserId()); + footer =
      +

      { _t("Or send invite link") }

      + +
      } else if (this.props.kind === KIND_INVITE) { const room = MatrixClientPeg.get()?.getRoom(this.props.roomId); const isSpace = SettingsStore.getValue("feature_spaces") && room?.isSpaceRoom(); @@ -1371,7 +1420,7 @@ export default class InviteDialog extends React.PureComponent + footer =
      - {consultSection} + {footer}
      ); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 1b04ae3b89..9767d7ac76 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2246,6 +2246,9 @@ "Start a conversation with someone using their name or username (like ).": "Start a conversation with someone using their name or username (like ).", "This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click here": "This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click here", "Go": "Go", + "Some results may be hidden for privacy.": "Some results may be hidden for privacy.", + "If you can’t see who you’re looking for, send them your invite link below.": "If you can’t see who you’re looking for, send them your invite link below.", + "Or send invite link": "Or send invite link", "Unnamed Space": "Unnamed Space", "Invite to %(roomName)s": "Invite to %(roomName)s", "Invite someone using their name, email address, username (like ) or share this space.": "Invite someone using their name, email address, username (like ) or share this space.", From eef15394f2e13f048186ef7fe0ee910852a7a7dc Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 27 May 2021 17:00:48 +0100 Subject: [PATCH 269/999] extract buildRecents return type into an interface --- src/components/views/dialogs/InviteDialog.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/components/views/dialogs/InviteDialog.tsx b/src/components/views/dialogs/InviteDialog.tsx index b9a5a68f83..b00b45bf60 100644 --- a/src/components/views/dialogs/InviteDialog.tsx +++ b/src/components/views/dialogs/InviteDialog.tsx @@ -53,6 +53,12 @@ import AccessibleButton from '../elements/AccessibleButton'; // we have a number of types defined from the Matrix spec which can't reasonably be altered here. /* eslint-disable camelcase */ +interface IRecentUser { + userId: string, + user: RoomMember, + lastActive: number, +} + export const KIND_DM = "dm"; export const KIND_INVITE = "invite"; export const KIND_CALL_TRANSFER = "call_transfer"; @@ -402,11 +408,7 @@ export default class InviteDialog extends React.PureComponent): { - userId: string, - user: RoomMember, - lastActive: number, - }[] { + public static buildRecents(excludedTargetIds: Set): IRecentUser[] { const rooms = DMRoomMap.shared().getUniqueRoomsWithIndividuals(); // map of userId => js-sdk Room // Also pull in all the rooms tagged as DefaultTagID.DM so we don't miss anything. Sometimes the From c4a26893a0f9bc7a4f18ec2c729c51aa02a10c8c Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 27 May 2021 18:57:22 +0100 Subject: [PATCH 270/999] Handle user_busy in voip calls Newly added to MSC2746 --- src/CallHandler.tsx | 3 +++ src/i18n/strings/en_EN.json | 2 ++ 2 files changed, 5 insertions(+) diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx index 0268ebfe46..90a631ab7f 100644 --- a/src/CallHandler.tsx +++ b/src/CallHandler.tsx @@ -462,6 +462,9 @@ export default class CallHandler extends EventEmitter { if (call.hangupReason === CallErrorCode.UserHangup) { title = _t("Call Declined"); description = _t("The other party declined the call."); + } else if (call.hangupReason === CallErrorCode.UserBusy) { + title = _t("User Busy"); + description = _t("The user you called is busy."); } else if (call.hangupReason === CallErrorCode.InviteTimeout) { title = _t("Call Failed"); // XXX: full stop appended as some relic here, but these diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 1b04ae3b89..3d6fcb8643 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -37,6 +37,8 @@ "Call Failed": "Call Failed", "Call Declined": "Call Declined", "The other party declined the call.": "The other party declined the call.", + "User Busy": "User Busy", + "The user you called is busy.": "The user you called is busy.", "The remote side failed to pick up": "The remote side failed to pick up", "The call could not be established": "The call could not be established", "Answered Elsewhere": "Answered Elsewhere", From d743b10b3fdc6b6afa18c15b2cc710a16e01beb8 Mon Sep 17 00:00:00 2001 From: iaiz Date: Tue, 25 May 2021 16:42:46 +0000 Subject: [PATCH 271/999] Translated using Weblate (Spanish) Currently translated at 99.8% (2969 of 2974 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/es/ --- src/i18n/strings/es.json | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/i18n/strings/es.json b/src/i18n/strings/es.json index 53f6d80c5a..8db9e9a6e0 100644 --- a/src/i18n/strings/es.json +++ b/src/i18n/strings/es.json @@ -192,7 +192,7 @@ "Add a topic": "Añadir un tema", "No media permissions": "Sin permisos para el medio", "You may need to manually permit %(brand)s to access your microphone/webcam": "Probablemente necesites dar permisos manualmente a %(brand)s para tu micrófono/cámara", - "Are you sure you want to leave the room '%(roomName)s'?": "¿Salir de la sala «%(roomName)s?", + "Are you sure you want to leave the room '%(roomName)s'?": "¿Salir de la sala «%(roomName)s»?", "Can't connect to homeserver - please check your connectivity, ensure your homeserver's SSL certificate is trusted, and that a browser extension is not blocking requests.": "No se puede conectar al servidor base. Por favor, comprueba tu conexión, asegúrate de que el certificado SSL del servidor es de confiaza, y comprueba que no haya extensiones de navegador bloqueando las peticiones.", "%(senderDisplayName)s removed the room name.": "%(senderDisplayName)s eliminó el nombre de la sala.", "Drop File Here": "Deje el fichero aquí", @@ -1544,7 +1544,7 @@ "Theme added!": "¡Se añadió el tema!", "Custom theme URL": "URL de tema personalizado", "Add theme": "Añadir tema", - "To report a Matrix-related security issue, please read the Matrix.org Security Disclosure Policy.": "Para informar de un problema de seguridad relacionado con Matrix, por favor lea Security Disclosure Policy de Matrix.or.", + "To report a Matrix-related security issue, please read the Matrix.org Security Disclosure Policy.": "Para informar de un problema de seguridad relacionado con Matrix, lee la Política de divulgación de seguridad de Matrix.org.", "Keyboard Shortcuts": "Atajos de teclado", "Customise your experience with experimental labs features. Learn more.": "Personaliza tu experiencia con funciones experimentales. Más información.", "Something went wrong. Please try again or view your console for hints.": "Algo salió mal. Por favor, inténtalo de nuevo o mira tu consola para encontrar pistas.", @@ -2279,8 +2279,8 @@ "Create community": "Crear comunidad", "Failed to find the general chat for this community": "No se pudo encontrar el chat general de esta comunidad", "Security & privacy": "Seguridad y privacidad", - "All settings": "Todos los ajustes", - "Feedback": "Realimentación", + "All settings": "Ajustes", + "Feedback": "Danos tu opinión", "Community settings": "Configuración de la comunidad", "User settings": "Ajustes de usuario", "Switch to light mode": "Cambiar al tema claro", @@ -3307,5 +3307,8 @@ "Your feedback will help make spaces better. The more detail you can go into, the better.": "Tus comentarios ayudarán a mejorar los espacios. Cuanto más detalle incluyas, mejor.", "Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.": "Beta disponible para la versión web, de escritorio o Android. Puede que algunas funcionalidades no estén disponibles en tu servidor base.", "Space Autocomplete": "Autocompletar espacios", - "Go to my space": "Ir a mi espacio" + "Go to my space": "Ir a mi espacio", + "sends space invaders": "enviar space invaders", + "Sends the given message with a space themed effect": "Envía un mensaje con efectos espaciales", + "If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "Si sales, %(brand)s volverá a cargarse con los espacios desactivados. Las comunidades y las etiquetas personalizadas serán visibles de nuevo." } From 045ae490ee075593ceddafeac8bd83e4bad39a67 Mon Sep 17 00:00:00 2001 From: Thibault Martin Date: Sun, 23 May 2021 21:40:07 +0000 Subject: [PATCH 272/999] Translated using Weblate (French) Currently translated at 100.0% (2974 of 2974 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/fr/ --- src/i18n/strings/fr.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index 222bd85a4e..6f7221dbe7 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -3346,5 +3346,11 @@ "Add reaction": "Ajouter une réaction", "Send and receive voice messages": "Envoyer et recevoir des messages vocaux", "See when people join, leave, or are invited to this room": "Voir quand une personne rejoint, quitte ou est invitée sur ce salon", - "Kick, ban, or invite people to this room, and make you leave": "Exclure, bannir ou inviter une personne dans ce salon et vous permettre de partir" + "Kick, ban, or invite people to this room, and make you leave": "Exclure, bannir ou inviter une personne dans ce salon et vous permettre de partir", + "Space Autocomplete": "Autocomplétion d’espace", + "Go to my space": "Aller à mon espace", + "sends space invaders": "Envoie les Space Invaders", + "Sends the given message with a space themed effect": "Envoyer le message avec un effet lié au thème de l’espace", + "See when people join, leave, or are invited to your active room": "Afficher quand des personnes rejoignent, partent, ou sont invités dans votre salon actif", + "Kick, ban, or invite people to your active room, and make you leave": "Expulser, bannir ou inviter des personnes dans votre salon actif et en partir" } From 355246c88f98defc1df06f1f0f8a75588bdb2ea0 Mon Sep 17 00:00:00 2001 From: Tirifto Date: Wed, 26 May 2021 22:17:22 +0000 Subject: [PATCH 273/999] Translated using Weblate (Esperanto) Currently translated at 97.9% (2914 of 2974 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/eo/ --- src/i18n/strings/eo.json | 47 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/eo.json b/src/i18n/strings/eo.json index f4d30b40b7..a1979c6699 100644 --- a/src/i18n/strings/eo.json +++ b/src/i18n/strings/eo.json @@ -3218,5 +3218,50 @@ "Show options to enable 'Do not disturb' mode": "Montri elekteblojn por ŝalti sendistran reĝimon", "%(deviceId)s from %(ip)s": "%(deviceId)s de %(ip)s", "Review to ensure your account is safe": "Kontrolu por certigi sekurecon de via konto", - "Sends the given message as a spoiler": "Sendas la donitan mesaĝon kiel malkaŝon de intrigo" + "Sends the given message as a spoiler": "Sendas la donitan mesaĝon kiel malkaŝon de intrigo", + "Are you sure you wish to abort creation of the host? The process cannot be continued.": "Ĉu vi certe volas nuligi kreadon de la gastiganto? Ĉi tiu procedo ne estos daŭrigebla.", + "Confirm abort of host creation": "Konfirmu nuligon de kreado de gastiganto", + "Feeling experimental? Labs are the best way to get things early, test out new features and help shape them before they actually launch. Learn more.": "Ĉu vi eksperimentemas? Laboratorioj estas la plej bona maniero frue akiri kaj testi novajn funkciojn, kaj helpi ilin formi antaŭ ilia plena ekuzo. Eksciu plion.", + "Your access token gives full access to your account. Do not share it with anyone.": "Via alirpeco donas plenan aliron al via konto. Donu ĝin al neniu.", + "We couldn't create your DM.": "Ni ne povis krei vian rektan ĉambron.", + "You may contact me if you have any follow up questions": "Vi povas min kontakti okaze de pliaj demandoj", + "To leave the beta, visit your settings.": "Por foriri de la prova versio, iru al viaj agordoj.", + "Your platform and username will be noted to help us use your feedback as much as we can.": "Via platformo kaj uzantonomo helpos al ni pli bone uzi viajn prikomentojn.", + "Your feedback will help make spaces better. The more detail you can go into, the better.": "Viaj prikomentoj helpos plibonigi arojn. Kiom pli detale vi skribos, tiom pli bonos.", + "%(featureName)s beta feedback": "Komentoj pri la prova versio de %(featureName)s", + "Thank you for your feedback, we really appreciate it.": "Dankon pro viaj prikomentoj, ni vere ilin ŝatas.", + "Beta feedback": "Komentoj pri la prova versio", + "Want to add a new room instead?": "Ĉu vi volas anstataŭe aldoni novan ĉambron?", + "You can add existing spaces to a space.": "Vi povas arigi arojn.", + "Feeling experimental?": "Ĉu vi eksperimentemas?", + "Adding rooms... (%(progress)s out of %(count)s)|one": "Aldonante ĉambron…", + "Adding rooms... (%(progress)s out of %(count)s)|other": "Aldonante ĉambrojn… (%(progress)s el %(count)s)", + "Not all selected were added": "Ne ĉiuj elektitoj aldoniĝis", + "You are not allowed to view this server's rooms list": "Vi ne rajtas vidi liston de ĉambroj de tu ĉi servilo", + "Add reaction": "Aldoni reagon", + "Error processing voice message": "Eraris traktado de voĉmesaĝo", + "Delete recording": "Forigi registraĵon", + "Stop the recording": "Ĉesigi la registradon", + "We didn't find a microphone on your device. Please check your settings and try again.": "Ni ne trovis mikrofonon en via aparato. Bonvolu kontroli viajn agordojn kaj reprovi.", + "No microphone found": "Neniu mikrofono troviĝis", + "We were unable to access your microphone. Please check your browser settings and try again.": "Ni ne povis aliri vian mikrofonon. Bonvolu kontroli la agordojn de via foliumilo kaj reprovi.", + "Unable to access your microphone": "Ne povas aliri vian mikrofonon", + "%(count)s results in all spaces|one": "%(count)s rezulto en ĉiuj aroj", + "%(count)s results in all spaces|other": "%(count)s rezultoj en ĉiuj aroj", + "You have no ignored users.": "Vi malatentas neniujn uzantojn.", + "Spaces are a new way to group rooms and people. To join an existing space you'll need an invite.": "Aroj prezentas novan manieron grupigi ĉambrojn kaj personojn. Por aliĝi al jama spaco, vi bezonos inviton.", + "Please enter a name for the space": "Bonvolu enigi nomon por la aro", + "Play": "Ludi", + "Pause": "Paŭzigi", + "Connecting": "Konektante", + "Sends the given message with a space themed effect": "Sendas mesaĝon kun la efekto de kosmo", + "Allow Peer-to-Peer for 1:1 calls (if you enable this, the other party might be able to see your IP address)": "Permesi samtavolajn individuajn vokojn (kaj do videbligi vian IP-adreson al la alia vokanto)", + "Send and receive voice messages": "Sendi kaj ricevi voĉmesaĝojn", + "Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.": "Prova versio disponeblas por reto, labortablo, kaj Androido. Iuj funkcioj eble ne disponeblas per via hejmservilo.", + "You can leave the beta any time from settings or tapping on a beta badge, like the one above.": "Vi povas forlasi la provan version iam ajn per la agordoj, aŭ per tuŝeto al la prova insigno, kiel tiu ĉi-supre.", + "%(brand)s will reload with Spaces enabled. Communities and custom tags will be hidden.": "%(brand)s estos enlegita kun subetno de Aroj. Komunumoj kaj propraj etikedoj iĝos kaŝitaj.", + "If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "Se vi foriros, %(brand)s estos enlegita sen subteno de Aroj. Komunumoj kaj propraj etikedoj ree estos videblaj.", + "Spaces are a new way to group rooms and people.": "Aroj prezentas novan manieron grupigi ĉambrojn kaj homojn.", + "See when people join, leave, or are invited to your active room": "Vidu kiam oni aliĝas, foriras, aŭ invitiĝas al via aktiva ĉambro", + "See when people join, leave, or are invited to this room": "Vidu kiam oni aliĝas, foriras, aŭ invitiĝas al la ĉambro" } From 1bd9968c3e18b3a5229d5397276a7b3f66f53b16 Mon Sep 17 00:00:00 2001 From: Hivaa Date: Mon, 24 May 2021 07:20:34 +0000 Subject: [PATCH 274/999] Translated using Weblate (Persian) Currently translated at 26.6% (793 of 2974 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/fa/ --- src/i18n/strings/fa.json | 83 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 82 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/fa.json b/src/i18n/strings/fa.json index e99a1218bb..c6331c4d78 100644 --- a/src/i18n/strings/fa.json +++ b/src/i18n/strings/fa.json @@ -745,5 +745,86 @@ "(connection failed)": "(ارتباط ناموفق بود)", "(could not connect media)": "(امکان اتصال رسانه وجود ندارد)", "(not supported by this browser)": "(توسط این مرورگر پشتیبانی نمی شود)", - "Someone": "کسی" + "Someone": "کسی", + "Your %(brand)s is misconfigured": "%(brand)s‌ی شما به درستی پیکربندی نشده‌است", + "Ensure you have a stable internet connection, or get in touch with the server admin": "از اتصال اینترنت پایدار اطمینان حاصل‌کرده و سپس با مدیر سرور ارتباط بگیرید", + "Cannot reach homeserver": "دسترسی به سرور میسر نیست", + "See %(msgtype)s messages posted to your active room": "پیام های %(msgtype)s ارسال شده به اتاق فعال خودتان را مشاهده کنید", + "See %(msgtype)s messages posted to this room": "پیام های %(msgtype)s ارسال شده به این اتاق را مشاهده کنید", + "Send %(msgtype)s messages as you in your active room": "همانطور که در اتاق فعال خودتان هستید پیام های %(msgtype)s را ارسال کنید", + "Send %(msgtype)s messages as you in this room": "همانطور که در این اتاق هستید پیام های %(msgtype)s را ارسال کنید", + "See general files posted to your active room": "فایل‌های ارسال شده در اتاق فعال خودتان را مشاهده کنید", + "See general files posted to this room": "فایل‌های ارسال شده در این اتاق را مشاهده کنید", + "Send general files as you in your active room": "همانطور که در اتاق فعال خود هستید فایل ارسال کنید", + "Send general files as you in this room": "همانطور که در این اتاق هستید فایل ارسال کنید", + "See videos posted to your active room": "فیلم های ارسال شده در اتاق فعال خودتان را مشاهده کنید", + "See videos posted to this room": "فیلم های ارسال شده در این اتاق را مشاهده کنید", + "Send videos as you in your active room": "همانطور که در اتاق فعال خود هستید فیلم ارسال کنید", + "Send videos as you in this room": "همانطور که در این اتاق هستید فیلم ارسال کنید", + "See images posted to your active room": "تصاویری را که در اتاق فعال خودتان ارسال شده‌اند، مشاهده کنید", + "See images posted to this room": "تصاویری را که در این اتاق ارسال شده‌اند، مشاهده کنید", + "Send images as you in your active room": "همانطور که در اتاق فعال خود هستید تصاویر را ارسال کنید", + "Send images as you in this room": "همانطور که در این اتاق هستید تصاویر را ارسال کنید", + "Send emotes as you in your active room": "همانطور که در اتاق فعال خود هستید شکلک‌های خود را ارسال کنید", + "Send emotes as you in this room": "همانطور که در این اتاق هستید شکلک‌های خود را ارسال کنید", + "Send text messages as you in your active room": "همانطور که در اتاق فعال خود هستید پیام های متنی ارسال کنید", + "Send text messages as you in this room": "همانطور که در این اتاق هستید پیام های متنی ارسال کنید", + "Send messages as you in your active room": "همانطور که در اتاق فعال خود هستید پیام ارسال کنید", + "Send messages as you in this room": "همانطور که در این اتاق هستید پیام ارسال کنید", + "See emotes posted to your active room": "شکلک‌های ارسال‌شده در اتاق فعال خودتان را مشاهده کنید", + "See emotes posted to this room": "شکلک‌های ارسال‌شده در این اتاق را مشاهده کنید", + "See text messages posted to your active room": "پیام‌های متنی که در اتاق فعال شما ارسال شده‌اند را مشاهده کنید", + "See text messages posted to this room": "پیام‌های متنی که در این گروه ارسال شده‌اند را مشاهده کنید", + "See messages posted to your active room": "مشاهده‌ی پیام‌هایی که در اتاق فعال شما ارسال شده‌اند", + "See messages posted to this room": "مشاهده‌ی پیام‌هایی که در این اتاق ارسال شده‌اند", + "The %(capability)s capability": "قابلیت %(capability)s", + "See %(eventType)s events posted to your active room": "رخدادهای %(eventType)s را که در اتاق فعال شما ارسال شده، مشاهده کنید", + "See %(eventType)s events posted to this room": "رخداد‌های %(eventType)s را که در این اتاق ارسال شده‌اند مشاهده کنید", + "with state key %(stateKey)s": "با کلید وضعیت (state key) %(stateKey)s", + "Send stickers to this room as you": "با مشخصات کاربری خودتان در این گروه استیکر ارسال نمائید", + "See when people join, leave, or are invited to your active room": "هنگامی که افراد به اتاق فعال شما دعوت می‌شوند، آن را ترک می‌کنند و لیست افراد دعوت شده به آن را مشاهده کنید", + "See when the avatar changes in your active room": "تغییرات نمایه‌ی اتاق فعال خود را مشاهده کنید", + "Change the avatar of your active room": "نمایه‌ی اتاق فعال خود را تغییر دهید", + "See when the avatar changes in this room": "تغییرات نمایه‌ی این اتاق را مشاهده کنید", + "Change the avatar of this room": "نمایه‌ی این اتاق را تغییر دهید", + "See when the name changes in your active room": "تغییرات نام اتاق فعال خود را مشاهده کنید", + "Change the name of your active room": "نام اتاق فعال خود را تغییر دهید", + "See when the name changes in this room": "تغییرات نام این اتاق را مشاهده کنید", + "Change the name of this room": "نام این اتاق را تغییر دهید", + "See when the topic changes in your active room": "تغییرات عنوان را در اتاق فعال خود مشاهده کنید", + "Change the topic of your active room": "عنوان اتاق فعال خود را تغییر دهید", + "See when the topic changes in this room": "تغییرات عنوان این اتاق را مشاهده کنید", + "Change the topic of this room": "عنوان این اتاق را تغییر دهید", + "Change which room, message, or user you're viewing": "اتاق، پیام و کاربرانی را که مشاهده می‌کنید، تغییر دهید", + "Change which room you're viewing": "اتاق‌هایی را که مشاهده می‌کنید تغییر دهید", + "Send stickers into your active room": "در اتاق‌های فعال خود استیکر ارسال کنید", + "Send stickers into this room": "در این اتاق استیکر ارسال کنید", + "%(names)s and %(lastPerson)s are typing …": "%(names)s و %(lastPerson)s در حال نوشتن…", + "%(names)s and %(count)s others are typing …|one": "%(names)s و یک نفر دیگر در حال نوشتن…", + "%(names)s and %(count)s others are typing …|other": "%(names)s و %(count)s نفر دیگر در حال نوشتن…", + "%(displayName)s is typing …": "%(displayName)s در حال نوشتن…", + "Dark": "تاریک", + "Light": "روشن", + "%(senderName)s updated a ban rule that was matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s یک قاعده تحریم را که با %(oldGlob)s تطابق داشت، به دلیل (دلایل) %(reason)s به گونه‌ای به‌روزرسانی کرد که با %(newGlob)s تطابق داشته باشد", + "%(senderName)s changed a rule that was banning servers matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s یک قاعده تحریم سرورها را که با %(oldGlob)s تطابق داشت، به دلیل (دلایل) %(reason)s به گونه‌ای تغییر داد که با %(newGlob)s تطابق داشته باشد", + "%(senderName)s changed a rule that was banning rooms matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s یک قاعده تحریم اتاق‌ها را که با %(oldGlob)s تطابق داشت، به دلیل (دلایل) %(reason)s به گونه‌ای تغییر داد که با %(newGlob)s تطابق داشته باشد", + "%(senderName)s changed a rule that was banning users matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s یک قاعده تحریم کاربران را که با %(oldGlob)s تطابق داشت، به دلیل (دلایل) %(reason)s به گونه‌ای تغییر داد که با %(newGlob)s تطابق داشته باشد", + "%(senderName)s created a ban rule matching %(glob)s for %(reason)s": "%(senderName)s یک قاعده تحریم را که با %(glob)s تطابق دارد، به دلیل (دلایل) %(reason)s ایجاد کرد", + "%(senderName)s created a rule banning servers matching %(glob)s for %(reason)s": "%(senderName)s یک قاعده تحریم سرورها را که با %(glob)s تطابق دارد، به دلیل (دلایل) %(reason)s ایجاد کرد", + "%(senderName)s created a rule banning rooms matching %(glob)s for %(reason)s": "%(senderName)s یک قاعده تحریم اتاق‌ها را که با %(glob)s تطابق دارد، به دلیل (دلایل) %(reason)s ایجاد کرد", + "%(senderName)s created a rule banning users matching %(glob)s for %(reason)s": "%(senderName)s یک قاعده تحریم کاربران را که با %(glob)s تطابق دارد، به دلیل (دلایل) %(reason)s ایجاد کرد", + "%(senderName)s updated a ban rule matching %(glob)s for %(reason)s": "%(senderName)s یک قاعده تحریم را که با %(glob)s تطابق داشت، به دلیل (دلایل) %(reason)s به‌روزرسانی کرد", + "%(senderName)s updated the rule banning servers matching %(glob)s for %(reason)s": "%(senderName)s قاعده تحریم سرورها را که با %(glob)s تطابق داشت، به دلیل (دلایل) %(reason)s به‌روزرسانی کرد", + "%(senderName)s updated the rule banning rooms matching %(glob)s for %(reason)s": "%(senderName)s قاعده تحریم اتاق‌ها را که با %(glob)s تطابق داشت، به دلیل (دلایل) %(reason)s به‌روزرسانی کرد", + "%(senderName)s updated the rule banning users matching %(glob)s for %(reason)s": "%(senderName)s قاعده تحریم کاربران را که با %(glob)s تطابق داشت، به دلیل (دلایل) %(reason)s به‌روزرسانی کرد", + "%(senderName)s updated an invalid ban rule": "%(senderName)s یک قاعده‌ی تحریم نامعتبر را به‌روزرسانی کرد", + "%(senderName)s removed a ban rule matching %(glob)s": "%(senderName)s قاعده تحریمی را که با %(glob)s تطابق داشت، حذف کرد", + "%(senderName)s removed the rule banning servers matching %(glob)s": "%(senderName)s قاعده تحریم سرورها را که با %(glob)s تطابق داشت، حذف کرد", + "%(senderName)s removed the rule banning rooms matching %(glob)s": "%(senderName)s قاعده تحریم اتاق‌ها را که با %(glob)s تطابق داشت، حذف کرد", + "%(senderName)s removed the rule banning users matching %(glob)s": "%(senderName)s قاعده تحریم کاربران را که با %(glob)s تطابق داشت، حذف کرد", + "%(senderName)s has updated the widget layout": "%(senderName)s چیدمان ویجت را به‌روز کرد", + "%(widgetName)s widget removed by %(senderName)s": "ویجت %(widgetName)s توسط %(senderName)s حذف گردید", + "%(widgetName)s widget added by %(senderName)s": "ویجت %(widgetName)s توسط %(senderName)s اضافه شد", + "%(widgetName)s widget modified by %(senderName)s": "ویجت %(widgetName)s توسط %(senderName)s تغییر کرد", + "%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for %(oldGroups)s in this room.": "%(senderDisplayName)s قابلیت flair را در این اتاق برای %(newGroups)s فعال و برای %(oldGroups)s غیر فعال کرد." } From 11a3ff414ffced5768079e96327128e8496755d9 Mon Sep 17 00:00:00 2001 From: Kaede Date: Wed, 26 May 2021 15:54:53 +0000 Subject: [PATCH 275/999] Translated using Weblate (Japanese) Currently translated at 78.2% (2326 of 2974 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/ja/ --- src/i18n/strings/ja.json | 35 ++++++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/src/i18n/strings/ja.json b/src/i18n/strings/ja.json index 66c42f4249..495e240051 100644 --- a/src/i18n/strings/ja.json +++ b/src/i18n/strings/ja.json @@ -753,8 +753,8 @@ "Community %(groupId)s not found": "コミュニティ %(groupId)s が見つかりません", "Failed to load %(groupId)s": "%(groupId)s をロードできませんでした", "Failed to reject invitation": "招待を拒否できませんでした", - "This room is not public. You will not be able to rejoin without an invite.": "この部屋は公開されていません。 あなたは招待なしで再び参加することはできません。", - "Are you sure you want to leave the room '%(roomName)s'?": "本当にこの部屋「%(roomName)s」から退出してよろしいですか?", + "This room is not public. You will not be able to rejoin without an invite.": "この部屋は公開されていません。再度参加するには、招待が必要です。", + "Are you sure you want to leave the room '%(roomName)s'?": "この部屋「%(roomName)s」から退出してよろしいですか?", "Failed to leave room": "部屋からの退出に失敗しました", "Can't leave Server Notices room": "サーバー通知部屋を離れることはできません", "This room is used for important messages from the Homeserver, so you cannot leave it.": "この部屋はホームサーバーからの重要なメッセージに使用されるため、そこを離れることはできません。", @@ -2406,7 +2406,7 @@ "Suggested Rooms": "おすすめの部屋", "Explore space rooms": "スペース内の部屋を探索します", "You do not have permissions to add rooms to this space": "このスペースに部屋を追加する権限がありません", - "Add existing room": "既存の部屋を追加します", + "Add existing room": "既存の部屋を追加", "You do not have permissions to create new rooms in this space": "このスペースに新しい部屋を作成する権限がありません", "Send message": "メッセージを送ります", "Invite to this space": "このスペースに招待します", @@ -2417,8 +2417,8 @@ "Space options": "スペースのオプション", "Space Home": "スペースのホーム", "New room": "新しい部屋", - "Leave space": "スペースを離れる", - "Invite people": "人々を招待する", + "Leave space": "スペースを退出", + "Invite people": "人々を招待", "Share your public space": "公開スペースを共有する", "Invite members": "参加者を招待する", "Invite by email or username": "メールまたはユーザー名で招待する", @@ -2469,7 +2469,7 @@ "Invite to just this room": "この部屋に招待", "Invite to %(spaceName)s": "%(spaceName)s に招待", "Quick actions": "クイックアクション", - "A private space for you and your teammates": "", + "A private space for you and your teammates": "あなたとチームメイトのプライベートスペース", "Me and my teammates": "自分とチームメイト", "Just me": "自分専用", "Make sure the right people have access to %(name)s": "必要な人が %(name)s にアクセスできるようにします", @@ -2481,5 +2481,26 @@ "Invite to %(roomName)s": "%(roomName)s へ招待", "Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.": "Beta は、ウェブ、デスクトップ、Android で利用可能です。お使いのホームサーバーによっては一部機能が利用できない場合があります。", "%(brand)s will reload with Spaces enabled. Communities and custom tags will be hidden.": "%(brand)s はスペースが有効な状態で再読み込みされます。コミュニティとカスタムタグは非表示になります。", - "Communities are changing to Spaces": "コミュニティはスペースに生まれ変わります" + "Communities are changing to Spaces": "コミュニティはスペースに生まれ変わります", + "Beta feedback": "Beta フィードバック", + "%(featureName)s beta feedback": "%(featureName)s Beta フィードバック", + "Send feedback": "フィードバックを送信", + "Manage & explore rooms": "部屋の管理および検索", + "Select a room below first": "以下から部屋を選択してください", + "A private space to organise your rooms": "部屋を整理するためのプライベートスペース", + "Private space": "プライベートスペース", + "Leave Space": "スペースを退出", + "Make this space private": "このスペースを非公開にする", + "Welcome %(name)s": "ようこそ、%(name)s", + "Are you sure you want to leave the space '%(spaceName)s'?": "このスペース「%(spaceName)s」から退出してよろしいですか?", + "This space is not public. You will not be able to rejoin without an invite.": "このスペースは公開されていません。再度参加するには、招待が必要です。", + "You are the only person here. If you leave, no one will be able to join in the future, including you.": "この部屋のメンバーはあなただけです。あなたが退出すると、今後あなたを含めて誰もこの部屋に参加できなくなります。", + "Adding rooms... (%(progress)s out of %(count)s)|one": "部屋を追加中...", + "Adding rooms... (%(progress)s out of %(count)s)|other": "部屋を追加中... (%(progress)s / %(count)s)", + "Skip for now": "スキップ", + "What do you want to organise?": "どれを追加しますか?", + "Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.": "部屋や会話を追加できます。これはあなた専用のスペースで、他の人からは見えません。後から部屋や会話を追加することもできます。", + "Support": "サポート", + "You can change these anytime.": "ここで入力した情報はいつでも編集できます。", + "Add some details to help people recognise it.": "情報を入力してください。" } From bcb3c591a46d980c21e2d45cb5c394f6903dab8e Mon Sep 17 00:00:00 2001 From: Tuomas Hietala Date: Mon, 24 May 2021 20:28:17 +0000 Subject: [PATCH 276/999] Translated using Weblate (Finnish) Currently translated at 90.8% (2702 of 2974 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/fi/ --- src/i18n/strings/fi.json | 55 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/fi.json b/src/i18n/strings/fi.json index 5452176cd9..23140846b3 100644 --- a/src/i18n/strings/fi.json +++ b/src/i18n/strings/fi.json @@ -2950,5 +2950,58 @@ "A new login is accessing your account: %(name)s (%(deviceID)s) at %(ip)s": "Uusi kirjautuminen tilillesi: %(name)s (%(deviceID)s) osoitteesta %(ip)s", "This homeserver has been blocked by its administrator.": "Tämä kotipalvelin on ylläpitäjänsä estämä.", "You're already in a call with this person.": "Olet jo puhelussa tämän henkilön kanssa.", - "Already in call": "Olet jo puhelussa" + "Already in call": "Olet jo puhelussa", + "Please choose a strong password": "Valitse vahva salasana", + "You can add more later too, including already existing ones.": "Voit lisätä niitä myöhemmin, mukaan lukien olemassa olevia.", + "Let's create a room for each of them.": "Tehdään huone jokaiselle.", + "What do you want to organise?": "Mitä haluat järjestää?", + "Random": "Satunnainen", + "Search names and descriptions": "Etsi nimistä ja kuvauksista", + "Failed to remove some rooms. Try again later": "Joitakin huoneita ei voitu poistaa. Yritä myöhemmin uudelleen.", + "Select a room below first": "Valitse ensin huone alta", + "You can select all or individual messages to retry or delete": "Voit valita kaikki tai yksittäisiä viestejä yritettäväksi uudelleen tai poistettavaksi", + "Sending": "Lähetetään", + "Retry all": "Yritä kaikkia uudelleen", + "Delete all": "Poista kaikki", + "Some of your messages have not been sent": "Osaa viesteistäsi ei ole lähetetty", + "You are the only person here. If you leave, no one will be able to join in the future, including you.": "Olet ainoa henkilö täällä. Jos lähdet, kukaan ei voi liittyä tulevaisuudessa, et myöskään sinä.", + "Beta": "Beeta", + "Tap for more info": "Lisää tietoa napauttamalla", + "The server is not configured to indicate what the problem is (CORS).": "Palvelinta ei ole säädetty ilmoittamaan, mikä ongelma on kyseessä (CORS).", + "Invited people will be able to read old messages.": "Kutsutut ihmiset voivat lukea vanhoja viestejä.", + "We couldn't create your DM.": "Yksityisviestiä ei voitu luoda.", + "Thank you for your feedback, we really appreciate it.": "Kiitos palautteesta, arvostamme sitä.", + "Beta feedback": "Palautetta beetaversiosta", + "Want to add a new room instead?": "Haluatko kuitenkin lisätä uuden huoneen?", + "Add existing rooms": "Lisää olemassa olevia huoneita", + "Adding rooms... (%(progress)s out of %(count)s)|one": "Lisätään huonetta...", + "Adding rooms... (%(progress)s out of %(count)s)|other": "Lisätään huoneita... (%(progress)s out of %(count)s)", + "Not all selected were added": "Kaikkia valittuja ei lisätty", + "You are not allowed to view this server's rooms list": "Sinulla ei ole oikeuksia nähdä tämän palvelimen huoneluetteloa", + "View message": "Näytä viesti", + "%(count)s people you know have already joined|one": "%(count)s tuntemasi henkilö on jo liittynyt", + "%(count)s people you know have already joined|other": "%(count)s tuntemaasi ihmistä on jo liittynyt", + "View all %(count)s members|one": "Näytä yksi jäsen", + "View all %(count)s members|other": "Näytä kaikki %(count)s jäsentä", + "Add reaction": "Lisää reaktio", + "Error processing voice message": "Virhe ääniviestin käsittelyssä", + "Delete recording": "Poista äänitys", + "Stop the recording": "Lopeta äänitys", + "Record a voice message": "Äänitä viesti", + "We were unable to access your microphone. Please check your browser settings and try again.": "Mikrofoniasi ei voitu käyttää. Tarkista selaimesi asetukset ja yritä uudelleen.", + "We didn't find a microphone on your device. Please check your settings and try again.": "Laitteestasi ei löytynyt mikrofonia. Tarkista asetuksesi ja yritä uudelleen.", + "No microphone found": "Mikrofonia ei löytynyt", + "Unable to access your microphone": "Mikrofonia ei voi käyttää", + "Quick actions": "Pikatoiminnot", + "%(seconds)ss left": "%(seconds)s s jäljellä", + "Failed to send": "Lähettäminen epäonnistui", + "You have no ignored users.": "Et ole sivuuttanut käyttäjiä.", + "Warn before quitting": "Varoita ennen lopettamista", + "Manage & explore rooms": "Hallitse ja selaa huoneita", + "Connecting": "Yhdistetään", + "unknown person": "tuntematon henkilö", + "Allow Peer-to-Peer for 1:1 calls (if you enable this, the other party might be able to see your IP address)": "Salli vertaisyhteydet 1:1-puheluille (jos otat tämän käyttöön, toinen osapuoli saattaa nähdä IP-osoitteesi)", + "Send and receive voice messages": "Lähetä ja vastaanota ääniviestejä", + "Show options to enable 'Do not disturb' mode": "Näytä asetukset Älä häiritse -tilan ottamiseksi käyttöön", + "%(deviceId)s from %(ip)s": "%(deviceId)s osoitteesta %(ip)s" } From f476cbb80cb6d62401e8ace2f20ac3cfd7800c6a Mon Sep 17 00:00:00 2001 From: Besnik Bleta Date: Tue, 25 May 2021 16:19:18 +0000 Subject: [PATCH 277/999] Translated using Weblate (Albanian) Currently translated at 99.6% (2964 of 2974 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/sq/ --- src/i18n/strings/sq.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/sq.json b/src/i18n/strings/sq.json index 8935b5e348..bd294093f9 100644 --- a/src/i18n/strings/sq.json +++ b/src/i18n/strings/sq.json @@ -3356,5 +3356,10 @@ "Beta available for web, desktop and Android. Thank you for trying the beta.": "Beta e gatshme për web, desktop dhe Android. Faleminderit që provoni beta-n.", "If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "Nëse ikni, %(brand)s-i do të ringarkohet me Hapësira të çaktivizuara. Bashkësitë dhe etiketat vetjake do të jenë sërish të dukshme.", "Spaces are a new way to group rooms and people.": "Hapësirat janë një rrugë e re për të grupuar dhoma dhe njerëz.", - "Message search initialisation failed": "Dështoi gatitje kërkimi mesazhesh" + "Message search initialisation failed": "Dështoi gatitje kërkimi mesazhesh", + "Go to my space": "Kalo te hapësira ime", + "sends space invaders": "dërgon pushtues hapësire", + "Sends the given message with a space themed effect": "E dërgon mesazhin e dhënë me një efekt teme hapësinore", + "See when people join, leave, or are invited to your active room": "Shihni kur persona vijnë, ikin ose janë ftuar në dhomën tuaj aktive", + "See when people join, leave, or are invited to this room": "Shihni kur persona vijnë, ikin ose janë ftuar në këtë dhomë" } From b54e850dad582f92b108e10f4f0d732320c4acb1 Mon Sep 17 00:00:00 2001 From: Trendyne Date: Sun, 23 May 2021 16:42:00 +0000 Subject: [PATCH 278/999] Translated using Weblate (Icelandic) Currently translated at 22.1% (659 of 2974 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/is/ --- src/i18n/strings/is.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/is.json b/src/i18n/strings/is.json index 03fa871f29..3695075cc5 100644 --- a/src/i18n/strings/is.json +++ b/src/i18n/strings/is.json @@ -716,5 +716,8 @@ "%(duration)sm": "%(duration)sm", "%(duration)ss": "%(duration)ss", "Emoji picker": "Tjáningartáknmyndvalmynd", - "Show less": "Sýna minna" + "Show less": "Sýna minna", + "%(count)s messages deleted.|one": "%(count)s skilaboð eytt.", + "%(count)s messages deleted.|other": "%(count)s skilaboðum eytt.", + "Message deleted on %(date)s": "Skilaboð eytt á %(date)s" } From 1a51ed9ffd78618c6bf21850eff397f3126c9fc9 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Fri, 28 May 2021 09:34:08 +0100 Subject: [PATCH 279/999] Make breadcrumb animation run on the compositing layer --- res/css/structures/_RoomView.scss | 1 + res/css/views/rooms/_RoomBreadcrumbs.scss | 6 +++--- src/components/structures/ContextMenu.tsx | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/res/css/structures/_RoomView.scss b/res/css/structures/_RoomView.scss index cdbe47178d..3d3654bda0 100644 --- a/res/css/structures/_RoomView.scss +++ b/res/css/structures/_RoomView.scss @@ -237,6 +237,7 @@ hr.mx_RoomView_myReadMarker { position: relative; top: -1px; z-index: 1; + will-change: width; transition: width 400ms easeinsine 1s, opacity 400ms easeinsine 1s; width: 99%; opacity: 1; diff --git a/res/css/views/rooms/_RoomBreadcrumbs.scss b/res/css/views/rooms/_RoomBreadcrumbs.scss index 6512797401..152b0a45cd 100644 --- a/res/css/views/rooms/_RoomBreadcrumbs.scss +++ b/res/css/views/rooms/_RoomBreadcrumbs.scss @@ -32,14 +32,14 @@ limitations under the License. // first triggering the enter state with the newest breadcrumb off screen (-40px) then // sliding it into view. &.mx_RoomBreadcrumbs-enter { - margin-left: -40px; // 32px for the avatar, 8px for the margin + transform: translateX(-40px); // 32px for the avatar, 8px for the margin } &.mx_RoomBreadcrumbs-enter-active { - margin-left: 0; + transform: translateX(0); // Timing function is as-requested by design. // NOTE: The transition time MUST match the value passed to CSSTransition! - transition: margin-left 640ms cubic-bezier(0.66, 0.02, 0.36, 1); + transition: transform 640ms cubic-bezier(0.66, 0.02, 0.36, 1); } .mx_RoomBreadcrumbs_placeholder { diff --git a/src/components/structures/ContextMenu.tsx b/src/components/structures/ContextMenu.tsx index 78a9d26934..9d8665c176 100644 --- a/src/components/structures/ContextMenu.tsx +++ b/src/components/structures/ContextMenu.tsx @@ -390,7 +390,7 @@ export class ContextMenu extends React.PureComponent { } render(): React.ReactChild { - return ReactDOM.createPortal(this.renderMenu(), document.body); + return ReactDOM.createPortal(this.renderMenu(), getOrCreateContainer()); } } From 29c4d9ffd01277c280568ed374eb858b7f414f94 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Fri, 28 May 2021 10:03:46 +0100 Subject: [PATCH 280/999] Restore toggle LHS logic --- src/components/structures/MatrixChat.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index c01437b313..32916a590b 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -232,6 +232,7 @@ export default class MatrixChat extends React.PureComponent { private accountPasswordTimer?: NodeJS.Timeout; private focusComposer: boolean; private subTitleStatus: string; + private prevWindowWidth: number; private readonly loggedInView: React.RefObject; private readonly dispatcherRef: any; @@ -277,6 +278,7 @@ export default class MatrixChat extends React.PureComponent { } } + this.prevWindowWidth = UIStore.instance.windowWidth || 1000; UIStore.instance.on(UI_EVENTS.Resize, this.handleResize); this.pageChanging = false; @@ -1821,13 +1823,15 @@ export default class MatrixChat extends React.PureComponent { const LHS_THRESHOLD = 1000; const width = UIStore.instance.windowWidth; - if (width <= LHS_THRESHOLD && !this.state.collapseLhs) { - dis.dispatch({ action: 'hide_left_panel' }); - } - if (width > LHS_THRESHOLD && this.state.collapseLhs) { + if (this.prevWindowWidth < LHS_THRESHOLD && width >= LHS_THRESHOLD) { dis.dispatch({ action: 'show_left_panel' }); } + if (this.prevWindowWidth >= LHS_THRESHOLD && width < LHS_THRESHOLD) { + dis.dispatch({ action: 'hide_left_panel' }); + } + + this.prevWindowWidth = width; this.state.resizeNotifier.notifyWindowResized(); }; From 650b683761728c562b1f30cef7d72447c9e27d32 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Fri, 28 May 2021 10:31:42 +0100 Subject: [PATCH 281/999] Reposition sticky headers when layout has changed --- src/components/structures/LeftPanel.tsx | 7 +++++++ src/components/views/rooms/RoomList.tsx | 2 ++ src/components/views/rooms/RoomSublist.tsx | 4 ++++ 3 files changed, 13 insertions(+) diff --git a/src/components/structures/LeftPanel.tsx b/src/components/structures/LeftPanel.tsx index 8699c27fdc..7df4bcadf3 100644 --- a/src/components/structures/LeftPanel.tsx +++ b/src/components/structures/LeftPanel.tsx @@ -110,6 +110,12 @@ export default class LeftPanel extends React.Component { UIStore.instance.removeListener("ListContainer", this.refreshStickyHeaders); } + public componentDidUpdate(prevProps: IProps, prevState: IState): void { + if (prevState.activeSpace !== this.state.activeSpace) { + this.refreshStickyHeaders(); + } + } + private updateActiveSpace = (activeSpace: Room) => { this.setState({ activeSpace }); }; @@ -429,6 +435,7 @@ export default class LeftPanel extends React.Component { onBlur={this.onBlur} isMinimized={this.props.isMinimized} activeSpace={this.state.activeSpace} + onListCollapse={this.refreshStickyHeaders} />; const containerClasses = classNames({ diff --git a/src/components/views/rooms/RoomList.tsx b/src/components/views/rooms/RoomList.tsx index 896021f918..8b36341ed0 100644 --- a/src/components/views/rooms/RoomList.tsx +++ b/src/components/views/rooms/RoomList.tsx @@ -55,6 +55,7 @@ interface IProps { onKeyDown: (ev: React.KeyboardEvent) => void; onFocus: (ev: React.FocusEvent) => void; onBlur: (ev: React.FocusEvent) => void; + onListCollapse?: (isExpanded: boolean) => void; resizeNotifier: ResizeNotifier; isMinimized: boolean; activeSpace: Room; @@ -538,6 +539,7 @@ export default class RoomList extends React.PureComponent { extraTiles={extraTiles} resizeNotifier={this.props.resizeNotifier} alwaysVisible={ALWAYS_VISIBLE_TAGS.includes(orderedTagId)} + onListCollapse={this.props.onListCollapse} /> }); } diff --git a/src/components/views/rooms/RoomSublist.tsx b/src/components/views/rooms/RoomSublist.tsx index eee93f6b01..df00524b3b 100644 --- a/src/components/views/rooms/RoomSublist.tsx +++ b/src/components/views/rooms/RoomSublist.tsx @@ -78,6 +78,7 @@ interface IProps { alwaysVisible?: boolean; resizeNotifier: ResizeNotifier; extraTiles?: ReactComponentElement[]; + onListCollapse?: (isExpanded: boolean) => void; // TODO: Account for https://github.com/vector-im/element-web/issues/14179 } @@ -472,6 +473,9 @@ export default class RoomSublist extends React.Component { private toggleCollapsed = () => { this.layout.isCollapsed = this.state.isExpanded; this.setState({isExpanded: !this.layout.isCollapsed}); + if (this.props.onListCollapse) { + this.props.onListCollapse(!this.layout.isCollapsed) + } }; private onHeaderKeyDown = (ev: React.KeyboardEvent) => { From 2c750fcb7a3bc7317b4b8b0e6013aa3f43e584a8 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 28 May 2021 12:48:12 +0100 Subject: [PATCH 282/999] Fix overflow issue in suggestion tiles --- res/css/views/dialogs/_InviteDialog.scss | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/res/css/views/dialogs/_InviteDialog.scss b/res/css/views/dialogs/_InviteDialog.scss index a33871eca5..4016e7d2e3 100644 --- a/res/css/views/dialogs/_InviteDialog.scss +++ b/res/css/views/dialogs/_InviteDialog.scss @@ -193,6 +193,7 @@ limitations under the License. .mx_InviteDialog_roomTile_nameStack { display: inline-block; + overflow: hidden; } .mx_InviteDialog_roomTile_name { @@ -208,6 +209,13 @@ limitations under the License. margin-left: 7px; } + .mx_InviteDialog_roomTile_name, + .mx_InviteDialog_roomTile_userId { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + .mx_InviteDialog_roomTile_time { text-align: right; font-size: $font-12px; From ea263937095053bf10ded22c2e0a7ec3da8e7cb5 Mon Sep 17 00:00:00 2001 From: Nique Woodhouse Date: Fri, 28 May 2021 13:00:18 +0100 Subject: [PATCH 283/999] Styling amends to accommodate the invite dialog footer --- res/css/views/dialogs/_InviteDialog.scss | 34 ++++++++++++++----- src/components/views/dialogs/InviteDialog.tsx | 4 +-- src/i18n/strings/en_EN.json | 2 +- 3 files changed, 29 insertions(+), 11 deletions(-) diff --git a/res/css/views/dialogs/_InviteDialog.scss b/res/css/views/dialogs/_InviteDialog.scss index a33871eca5..bda576c44e 100644 --- a/res/css/views/dialogs/_InviteDialog.scss +++ b/res/css/views/dialogs/_InviteDialog.scss @@ -73,7 +73,7 @@ limitations under the License. } .mx_InviteDialog_section { - padding-bottom: 10px; + padding-bottom: 4px; h3 { font-size: $font-12px; @@ -98,11 +98,25 @@ limitations under the License. } } +.mx_InviteDialog_section_hidden_suggestions_disclaimer { + padding: 8px 0 16px 0; + font-size: $font-14px; + + > span { + color: $primary-fg-color; + font-weight: 600; + } + + > p { + margin:0; + } +} + .mx_InviteDialog_footer { border-top: 1px solid $input-border-color; > h3 { - margin: 8px 0; + margin: 12px 0; font-size: $font-12px; color: $muted-fg-color; font-weight: bold; @@ -113,7 +127,7 @@ limitations under the License. display: flex; justify-content: space-between; border-radius: 4px; - border: solid 1px $input-border-color; + border: solid 1px $light-fg-color; padding: 8px; > a { @@ -274,17 +288,21 @@ limitations under the License. } .mx_InviteDialog_userSections { - margin-top: 10px; + margin-top: 4px; overflow-y: auto; - padding-right: 45px; - height: calc(100% - 190px); // mx_InviteDialog's height minus some for the upper elements + padding: 0px 45px 4px 0px; + height: calc(100% - 175px); // mx_InviteDialog's height minus some for the upper and lower elements } + // Right margin for the design. We could apply this to the whole dialog, but then the scrollbar // for the user section gets weird. -.mx_InviteDialog_helpText, .mx_InviteDialog_addressBar { - margin-right: 45px; + margin: 8px 45px 0px 0px; +} + +.mx_InviteDialog_helpText { + margin:0px; } .mx_InviteDialog_helpText .mx_AccessibleButton_kind_link { diff --git a/src/components/views/dialogs/InviteDialog.tsx b/src/components/views/dialogs/InviteDialog.tsx index 22763ceda2..081004fa74 100644 --- a/src/components/views/dialogs/InviteDialog.tsx +++ b/src/components/views/dialogs/InviteDialog.tsx @@ -1341,8 +1341,8 @@ export default class InviteDialog extends React.PureComponent - { _t("Some results may be hidden for privacy.") } + extraSection =
      + { _t("Some suggestions may be hidden for privacy.") }

      { _t("If you can’t see who you’re looking for, send them your invite link below.") }

      ; const link = makeUserPermalink(MatrixClientPeg.get().getUserId()); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 9767d7ac76..8f5082e88a 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2246,7 +2246,7 @@ "Start a conversation with someone using their name or username (like ).": "Start a conversation with someone using their name or username (like ).", "This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click here": "This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click here", "Go": "Go", - "Some results may be hidden for privacy.": "Some results may be hidden for privacy.", + "Some suggestions may be hidden for privacy.": "Some suggestions may be hidden for privacy.", "If you can’t see who you’re looking for, send them your invite link below.": "If you can’t see who you’re looking for, send them your invite link below.", "Or send invite link": "Or send invite link", "Unnamed Space": "Unnamed Space", From 36e43270ca6acd79ad5244ec83b96344fd766592 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 28 May 2021 13:08:05 +0100 Subject: [PATCH 284/999] Apply suggestions from code review --- res/css/views/dialogs/_InviteDialog.scss | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/res/css/views/dialogs/_InviteDialog.scss b/res/css/views/dialogs/_InviteDialog.scss index 0d78589db2..bae086c7d5 100644 --- a/res/css/views/dialogs/_InviteDialog.scss +++ b/res/css/views/dialogs/_InviteDialog.scss @@ -108,7 +108,7 @@ limitations under the License. } > p { - margin:0; + margin: 0; } } @@ -298,7 +298,7 @@ limitations under the License. .mx_InviteDialog_userSections { margin-top: 4px; overflow-y: auto; - padding: 0px 45px 4px 0px; + padding: 0 45px 4px 0; height: calc(100% - 175px); // mx_InviteDialog's height minus some for the upper and lower elements } @@ -306,11 +306,11 @@ limitations under the License. // Right margin for the design. We could apply this to the whole dialog, but then the scrollbar // for the user section gets weird. .mx_InviteDialog_addressBar { - margin: 8px 45px 0px 0px; + margin: 8px 45px 0 0; } .mx_InviteDialog_helpText { - margin:0px; + margin: 0; } .mx_InviteDialog_helpText .mx_AccessibleButton_kind_link { From caaef630776a07c69654393a37bd62bb56398566 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 28 May 2021 13:11:48 +0100 Subject: [PATCH 285/999] delint1 --- res/css/views/dialogs/_InviteDialog.scss | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/res/css/views/dialogs/_InviteDialog.scss b/res/css/views/dialogs/_InviteDialog.scss index bae086c7d5..8c0421b989 100644 --- a/res/css/views/dialogs/_InviteDialog.scss +++ b/res/css/views/dialogs/_InviteDialog.scss @@ -17,6 +17,9 @@ limitations under the License. .mx_InviteDialog_addressBar { display: flex; flex-direction: row; + // Right margin for the design. We could apply this to the whole dialog, but then the scrollbar + // for the user section gets weird. + margin: 8px 45px 0 0; .mx_InviteDialog_editor { flex: 1; @@ -127,7 +130,7 @@ limitations under the License. display: flex; justify-content: space-between; border-radius: 4px; - border: solid 1px $light-fg-color; + border: solid 1px $light-fg-color; padding: 8px; > a { @@ -302,13 +305,6 @@ limitations under the License. height: calc(100% - 175px); // mx_InviteDialog's height minus some for the upper and lower elements } - -// Right margin for the design. We could apply this to the whole dialog, but then the scrollbar -// for the user section gets weird. -.mx_InviteDialog_addressBar { - margin: 8px 45px 0 0; -} - .mx_InviteDialog_helpText { margin: 0; } From 91b7f2551312c405257421221b4d237a3bd96682 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 28 May 2021 13:51:54 +0100 Subject: [PATCH 286/999] delint2 --- src/components/views/dialogs/InviteDialog.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/components/views/dialogs/InviteDialog.tsx b/src/components/views/dialogs/InviteDialog.tsx index 7cee61d579..ef7a31a177 100644 --- a/src/components/views/dialogs/InviteDialog.tsx +++ b/src/components/views/dialogs/InviteDialog.tsx @@ -408,9 +408,6 @@ export default class InviteDialog extends React.PureComponent Date: Fri, 28 May 2021 14:59:14 +0100 Subject: [PATCH 287/999] Use passive option for scroll handler --- src/CountlyAnalytics.ts | 4 +++- src/components/structures/AutoHideScrollbar.js | 15 ++++++++++++++- src/components/structures/IndicatorScrollbar.js | 4 +++- src/components/structures/LeftPanel.tsx | 7 +++++-- src/components/views/rooms/RoomSublist.tsx | 9 +++++++-- 5 files changed, 32 insertions(+), 7 deletions(-) diff --git a/src/CountlyAnalytics.ts b/src/CountlyAnalytics.ts index aac53b188b..5545ed8483 100644 --- a/src/CountlyAnalytics.ts +++ b/src/CountlyAnalytics.ts @@ -816,7 +816,9 @@ export default class CountlyAnalytics { window.addEventListener("mousemove", this.onUserActivity); window.addEventListener("click", this.onUserActivity); window.addEventListener("keydown", this.onUserActivity); - window.addEventListener("scroll", this.onUserActivity); + // Using the passive option to not block the main thread + // https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#improving_scrolling_performance_with_passive_listeners + window.addEventListener("scroll", this.onUserActivity, { passive: true }); this.activityIntervalId = setInterval(() => { this.inactivityCounter++; diff --git a/src/components/structures/AutoHideScrollbar.js b/src/components/structures/AutoHideScrollbar.js index 14f7c9ca83..a2dcff2731 100644 --- a/src/components/structures/AutoHideScrollbar.js +++ b/src/components/structures/AutoHideScrollbar.js @@ -23,6 +23,20 @@ export default class AutoHideScrollbar extends React.Component { this._collectContainerRef = this._collectContainerRef.bind(this); } + componentDidMount() { + if (this.containerRef && this.props.onScroll) { + // Using the passive option to not block the main thread + // https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#improving_scrolling_performance_with_passive_listeners + this.containerRef.addEventListener("scroll", this.props.onScroll, { passive: true }); + } + } + + componentWillUnmount() { + if (this.containerRef && this.props.onScroll) { + this.containerRef.removeEventListener("scroll", this.props.onScroll); + } + } + _collectContainerRef(ref) { if (ref && !this.containerRef) { this.containerRef = ref; @@ -41,7 +55,6 @@ export default class AutoHideScrollbar extends React.Component { ref={this._collectContainerRef} style={this.props.style} className={["mx_AutoHideScrollbar", this.props.className].join(" ")} - onScroll={this.props.onScroll} onWheel={this.props.onWheel} tabIndex={this.props.tabIndex} > diff --git a/src/components/structures/IndicatorScrollbar.js b/src/components/structures/IndicatorScrollbar.js index 341ab2df71..51a3b287f0 100644 --- a/src/components/structures/IndicatorScrollbar.js +++ b/src/components/structures/IndicatorScrollbar.js @@ -59,7 +59,9 @@ export default class IndicatorScrollbar extends React.Component { _collectScroller(scroller) { if (scroller && !this._scrollElement) { this._scrollElement = scroller; - this._scrollElement.addEventListener("scroll", this.checkOverflow); + // Using the passive option to not block the main thread + // https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#improving_scrolling_performance_with_passive_listeners + this._scrollElement.addEventListener("scroll", this.checkOverflow, { passive: true }); this.checkOverflow(); } } diff --git a/src/components/structures/LeftPanel.tsx b/src/components/structures/LeftPanel.tsx index 7df4bcadf3..a5079c0ed5 100644 --- a/src/components/structures/LeftPanel.tsx +++ b/src/components/structures/LeftPanel.tsx @@ -97,6 +97,9 @@ export default class LeftPanel extends React.Component { public componentDidMount() { UIStore.instance.trackElementDimensions("ListContainer", this.listContainerRef.current); UIStore.instance.on("ListContainer", this.refreshStickyHeaders); + // Using the passive option to not block the main thread + // https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#improving_scrolling_performance_with_passive_listeners + this.listContainerRef.current.addEventListener("scroll", this.onScroll, { passive: true }); } public componentWillUnmount() { @@ -108,6 +111,7 @@ export default class LeftPanel extends React.Component { SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.updateActiveSpace); UIStore.instance.stopTrackingElementDimensions("ListContainer"); UIStore.instance.removeListener("ListContainer", this.refreshStickyHeaders); + this.listContainerRef.current.removeEventListener("scroll", this.onScroll); } public componentDidUpdate(prevProps: IProps, prevState: IState): void { @@ -295,7 +299,7 @@ export default class LeftPanel extends React.Component { } } - private onScroll = (ev: React.MouseEvent) => { + private onScroll = (ev: Event) => { const list = ev.target as HTMLDivElement; this.handleStickyHeaders(list); }; @@ -459,7 +463,6 @@ export default class LeftPanel extends React.Component {
      { private headerButton = createRef(); private sublistRef = createRef(); + private tilesRef = createRef(); private dispatcherRef: string; private layout: ListLayout; private heightAtStart: number; @@ -246,11 +247,15 @@ export default class RoomSublist extends React.Component { public componentDidMount() { this.dispatcherRef = defaultDispatcher.register(this.onAction); RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.onListsUpdated); + // Using the passive option to not block the main thread + // https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#improving_scrolling_performance_with_passive_listeners + this.tilesRef.current.addEventListener("scroll", this.onScrollPrevent, { passive: true }); } public componentWillUnmount() { defaultDispatcher.unregister(this.dispatcherRef); RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.onListsUpdated); + this.tilesRef.current.removeEventListener("scroll", this.onScrollPrevent); } private onListsUpdated = () => { @@ -755,7 +760,7 @@ export default class RoomSublist extends React.Component { ); } - private onScrollPrevent(e: React.UIEvent) { + private onScrollPrevent(e: Event) { // the RoomTile calls scrollIntoView and the browser may scroll a div we do not wish to be scrollable // this fixes https://github.com/vector-im/element-web/issues/14413 (e.target as HTMLDivElement).scrollTop = 0; @@ -884,7 +889,7 @@ export default class RoomSublist extends React.Component { className="mx_RoomSublist_resizeBox" enable={handles} > -
      +
      {visibleTiles}
      {showNButton} From 18188538f6ca656d1388855faeaed0fbbb3afc2b Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Fri, 28 May 2021 14:59:54 +0100 Subject: [PATCH 288/999] Move the read receipt animation to the compositing layer --- 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 5d1dd04383..5d477d63c9 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -280,6 +280,7 @@ $left-gutter: 64px; height: $font-14px; width: $font-14px; + will-change: left, top; transition: left var(--transition-short) ease-out, top var(--transition-standard) ease-out; From fd69fce1bac58911a9829a610385dbeeea469281 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Fri, 28 May 2021 17:37:29 +0100 Subject: [PATCH 289/999] guard event listener from null values --- src/components/structures/LeftPanel.tsx | 4 ++-- src/components/views/rooms/RoomSublist.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/structures/LeftPanel.tsx b/src/components/structures/LeftPanel.tsx index a5079c0ed5..5b6b9c3717 100644 --- a/src/components/structures/LeftPanel.tsx +++ b/src/components/structures/LeftPanel.tsx @@ -99,7 +99,7 @@ export default class LeftPanel extends React.Component { UIStore.instance.on("ListContainer", this.refreshStickyHeaders); // Using the passive option to not block the main thread // https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#improving_scrolling_performance_with_passive_listeners - this.listContainerRef.current.addEventListener("scroll", this.onScroll, { passive: true }); + this.listContainerRef.current?.addEventListener("scroll", this.onScroll, { passive: true }); } public componentWillUnmount() { @@ -111,7 +111,7 @@ export default class LeftPanel extends React.Component { SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.updateActiveSpace); UIStore.instance.stopTrackingElementDimensions("ListContainer"); UIStore.instance.removeListener("ListContainer", this.refreshStickyHeaders); - this.listContainerRef.current.removeEventListener("scroll", this.onScroll); + this.listContainerRef.current?.removeEventListener("scroll", this.onScroll); } public componentDidUpdate(prevProps: IProps, prevState: IState): void { diff --git a/src/components/views/rooms/RoomSublist.tsx b/src/components/views/rooms/RoomSublist.tsx index cd3d9bc73a..0bb7381dbc 100644 --- a/src/components/views/rooms/RoomSublist.tsx +++ b/src/components/views/rooms/RoomSublist.tsx @@ -249,13 +249,13 @@ export default class RoomSublist extends React.Component { RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.onListsUpdated); // Using the passive option to not block the main thread // https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#improving_scrolling_performance_with_passive_listeners - this.tilesRef.current.addEventListener("scroll", this.onScrollPrevent, { passive: true }); + this.tilesRef.current?.addEventListener("scroll", this.onScrollPrevent, { passive: true }); } public componentWillUnmount() { defaultDispatcher.unregister(this.dispatcherRef); RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.onListsUpdated); - this.tilesRef.current.removeEventListener("scroll", this.onScrollPrevent); + this.tilesRef.current?.removeEventListener("scroll", this.onScrollPrevent); } private onListsUpdated = () => { From d66bafd1a664387cfcc3e7886db9ab22c650772e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 29 May 2021 03:12:59 +0000 Subject: [PATCH 290/999] Bump ws from 7.4.2 to 7.4.6 Bumps [ws](https://github.com/websockets/ws) from 7.4.2 to 7.4.6. - [Release notes](https://github.com/websockets/ws/releases) - [Commits](https://github.com/websockets/ws/compare/7.4.2...7.4.6) Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 35aac66e26..0ff235a660 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8438,9 +8438,9 @@ write@1.0.3: mkdirp "^0.5.1" ws@^7.2.3: - version "7.4.2" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.2.tgz#782100048e54eb36fe9843363ab1c68672b261dd" - integrity sha512-T4tewALS3+qsrpGI/8dqNMLIVdq/g/85U98HPMa6F0m6xTbvhXU6RCQLqPH3+SlomNV/LdY6RXEbBpMH6EOJnA== + version "7.4.6" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c" + integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A== xml-name-validator@^3.0.0: version "3.0.0" From d72c773e2deab6a906be61ad3d5adc6ec7ea1083 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 29 May 2021 08:26:53 +0200 Subject: [PATCH 291/999] clearStoredEditorState when canceling editing using a shortcut MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/rooms/EditMessageComposer.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/views/rooms/EditMessageComposer.js b/src/components/views/rooms/EditMessageComposer.js index 897388d5d2..f0980af7ae 100644 --- a/src/components/views/rooms/EditMessageComposer.js +++ b/src/components/views/rooms/EditMessageComposer.js @@ -168,6 +168,7 @@ export default class EditMessageComposer extends React.Component { if (nextEvent) { dis.dispatch({action: 'edit_event', event: nextEvent}); } else { + this._clearStoredEditorState(); dis.dispatch({action: 'edit_event', event: null}); dis.fire(Action.FocusComposer); } From 53ebf3b8e31560ee71d744d203f653b3dd36b32c Mon Sep 17 00:00:00 2001 From: Aaron Raimist Date: Sat, 29 May 2021 01:37:19 -0500 Subject: [PATCH 292/999] Don't include via when sharing room alias Signed-off-by: Aaron Raimist --- src/utils/permalinks/Permalinks.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/permalinks/Permalinks.ts b/src/utils/permalinks/Permalinks.ts index d87c826cc2..cecd79dd53 100644 --- a/src/utils/permalinks/Permalinks.ts +++ b/src/utils/permalinks/Permalinks.ts @@ -149,7 +149,7 @@ export class RoomPermalinkCreator { // Prefer to use canonical alias for permalink if possible const alias = this.room.getCanonicalAlias(); if (alias) { - return getPermalinkConstructor().forRoom(alias, this._serverCandidates); + return getPermalinkConstructor().forRoom(alias); } } return getPermalinkConstructor().forRoom(this.roomId, this._serverCandidates); From ccdd2311f447111f4cf56725834ba08be78abc08 Mon Sep 17 00:00:00 2001 From: Aaron Raimist Date: Sat, 29 May 2021 01:38:41 -0500 Subject: [PATCH 293/999] Make "share this room" use aliases if possible Signed-off-by: Aaron Raimist --- src/utils/permalinks/Permalinks.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/utils/permalinks/Permalinks.ts b/src/utils/permalinks/Permalinks.ts index cecd79dd53..2f268aaa0c 100644 --- a/src/utils/permalinks/Permalinks.ts +++ b/src/utils/permalinks/Permalinks.ts @@ -293,16 +293,16 @@ export function makeRoomPermalink(roomId: string): string { // If the roomId isn't actually a room ID, don't try to list the servers. // Aliases are already routable, and don't need extra information. - if (roomId[0] !== '!') return getPermalinkConstructor().forRoom(roomId, []); + if (roomId[0] !== '!') return getPermalinkConstructor().forShareableRoom(roomId, []); const client = MatrixClientPeg.get(); const room = client.getRoom(roomId); if (!room) { - return getPermalinkConstructor().forRoom(roomId, []); + return getPermalinkConstructor().forShareableRoom(roomId, []); } const permalinkCreator = new RoomPermalinkCreator(room); permalinkCreator.load(); - return permalinkCreator.forRoom(); + return permalinkCreator.forShareableRoom(); } export function makeGroupPermalink(groupId: string): string { From 5734304cf9053bbaea3b7d05e581acca5d60bc38 Mon Sep 17 00:00:00 2001 From: Hivaa Date: Mon, 31 May 2021 18:37:00 +0000 Subject: [PATCH 294/999] Translated using Weblate (Persian) Currently translated at 95.3% (2835 of 2974 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/fa/ --- src/i18n/strings/fa.json | 2045 +++++++++++++++++++++++++++++++++++++- 1 file changed, 2044 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/fa.json b/src/i18n/strings/fa.json index c6331c4d78..99c1252dd9 100644 --- a/src/i18n/strings/fa.json +++ b/src/i18n/strings/fa.json @@ -826,5 +826,2048 @@ "%(widgetName)s widget removed by %(senderName)s": "ویجت %(widgetName)s توسط %(senderName)s حذف گردید", "%(widgetName)s widget added by %(senderName)s": "ویجت %(widgetName)s توسط %(senderName)s اضافه شد", "%(widgetName)s widget modified by %(senderName)s": "ویجت %(widgetName)s توسط %(senderName)s تغییر کرد", - "%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for %(oldGroups)s in this room.": "%(senderDisplayName)s قابلیت flair را در این اتاق برای %(newGroups)s فعال و برای %(oldGroups)s غیر فعال کرد." + "%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for %(oldGroups)s in this room.": "%(senderDisplayName)s قابلیت flair را در این اتاق برای %(newGroups)s فعال و برای %(oldGroups)s غیر فعال کرد.", + "The server has denied your request.": "سرور درخواست شما را رد کرده است.", + "Use your Security Key to continue.": "برای ادامه از کلید امنیتی خود استفاده کنید.", + "This session, or the other session": "این نشست، یا نشست دیگر", + "%(creator)s created and configured the room.": "%(creator)s اتاق را ایجاد و پیکربندی کرد.", + "Report Content to Your Homeserver Administrator": "گزارش محتوا به مدیر سرور خود", + "Be found by phone or email": "از طریق تلفن یا ایمیل پیدا شوید", + "Find others by phone or email": "دیگران را از طریق تلفن یا ایمیل پیدا کنید", + "Sign out and remove encryption keys?": "خروج از حساب کاربری و حذف کلیدهای رمزنگاری؟", + "Remember my selection for this widget": "انتخاب من برای این ابزارک را بخاطر بسپار", + "I don't want my encrypted messages": "پیام‌های رمزشده‌ی خود را نمی‌خواهم", + "Upgrade this room to version %(version)s": "این اتاق را به نسخه %(version)s ارتقا دهید", + "Please enter the code it contains:": "لطفا کدی را که در آن وجود دارد وارد کنید:", + "Failed to save space settings.": "تنظیمات فضای کاری ذخیره نشد.", + "Not a valid Security Key": "کلید امنیتی معتبری نیست", + "This widget would like to:": "این ابزارک تمایل دارد:", + "Unable to set up keys": "تنظیم کلیدها امکان پذیر نیست", + "%(completed)s of %(total)s keys restored": "%(completed)s از %(total)s کلید بازیابی شدند", + "a new cross-signing key signature": "یک کلید امضای متقابل جدید", + "a new master key signature": "یک شاه‌کلید جدید", + "Close dialog or context menu": "بستن پنجره یا منو", + "Your account is not secure": "حساب کاربری شما امن نیست", + "Please fill why you're reporting.": "لطفا توضیح دهید که چرا گزارش می‌دهید.", + "Password is allowed, but unsafe": "گذرواژه مجاز است ، اما ناامن است", + "Upload files (%(current)s of %(total)s)": "بارگذاری فایل‌ها (%(current)s از %(total)s)", + "Failed to decrypt %(failedCount)s sessions!": "رمزگشایی %(failedCount)s نشست موفقیت‌آمیز نبود!", + "Unable to load backup status": "بارگیری و نمایش وضعیت نسخه‌ی پشتیبان امکان‌پذیر نیست", + "Link to most recent message": "پیوند به آخرین پیام", + "Clear Storage and Sign Out": "فضای ذخیره‌سازی را پاک کرده و از حساب کاربری خارج شوید", + "Error whilst fetching joined communities": "هنگام واکشی اجتماع‌هایی که عضو آن‌ها هستید، خطایی رخ داد", + "Make this space private": "این فضای کاری را خصوصی کن", + "Unable to validate homeserver": "تأیید اعتبار سرور امکان‌پذیر نیست", + "Sign into your homeserver": "وارد سرور خود شوید", + "%(creator)s created this DM.": "%(creator)s این گفتگو را ایجاد کرد.", + "You’re all caught up": "همه‌ی کارها را انجام دادید", + "The server is offline.": "سرور آفلاین است.", + "You're all caught up.": "همه‌ی کارها را انجام دادید", + "Successfully restored %(sessionCount)s keys": "کلیدهای %(sessionCount)s با موفقیت بازیابی شدند", + "Fetching keys from server...": "واکشی کلیدها از سرور ...", + "Restoring keys from backup": "بازیابی کلیدها از نسخه پشتیبان", + "Interactively verify by Emoji": "تأیید تعاملی با استفاده از شکلک", + "Manually Verify by Text": "تائید دستی با استفاده از متن", + "a device cross-signing signature": "کلید امضای متقابل یک دستگاه", + "Upload %(count)s other files|one": "بارگذاری %(count)s فایل دیگر", + "Upload %(count)s other files|other": "بارگذاری %(count)s فایل دیگر", + "Room Settings - %(roomName)s": "تنظیمات اتاق - %(roomName)s", + "Start using Key Backup": "شروع استفاده از نسخه‌ی پشتیبان کلید", + "Unable to restore backup": "بازیابی نسخه پشتیبان امکان پذیر نیست", + "Clear cache and resync": "پاک کردن حافظه‌ی کش و همگام سازی مجدد", + "Failed to upgrade room": "اتاق ارتقاء نیافت", + "Link to selected message": "پیوند به پیام انتخاب شده", + "Failed to load %(groupId)s": "بارگیری و نمایش %(groupId)s انجام نشد", + "Community %(groupId)s not found": "اجتماع %(groupId)s یافت نشد", + "Unable to restore session": "امکان بازیابی نشست وجود ندارد", + "Verify other login": "ورود دیگر را تائید کنید", + "Reset event store": "پاک‌کردن مخزن رخداد", + "Reset event store?": "پاک‌کردن مخزن رخداد؟", + "%(count)s messages deleted.|one": "%(count)s پیام پاک شد.", + "%(count)s messages deleted.|other": "%(count)s پیام پاک شد.", + "Invite to %(roomName)s": "دعوت به %(roomName)s", + "View dev tools": "مشاهده ابزارهای توسعه", + "Upgrade to %(hostSignupBrand)s": "ارتقاء به %(hostSignupBrand)s", + "Enter Security Key": "کلید امنیتی را وارد کنید", + "Enter Security Phrase": "عبارت امنیتی را وارد کنید", + "Incorrect Security Phrase": "عبارت امنیتی نادرست است", + "Security Key mismatch": "عدم تطابق کلید امنیتی", + "Invalid Security Key": "کلید امنیتی نامعتبر است", + "Wrong Security Key": "کلید امنیتی اشتباه است", + "Specify a homeserver": "یک سرور مشخص کنید", + "Continuing without email": "ادامه بدون ایمیل", + "Approve widget permissions": "دسترسی‌های ابزارک را تائید کنید", + "Server isn't responding": "سرور پاسخ نمی دهد", + "Unable to upload": "بارگذاری امکان پذیر نیست", + "Signature upload failed": "بارگذاری امضا انجام نشد", + "Signature upload success": "موفقیت در بارگذاری امضا", + "Cancelled signature upload": "بارگذاری امضا لغو شد", + "a key signature": "یک امضای کلیدی", + "Clear cross-signing keys": "کلیدهای امضای متقابل را پاک کن", + "Destroy cross-signing keys?": "کلیدهای امضای متقابل نابود شود؟", + "This wasn't me": "این من نبودم", + "Recently Direct Messaged": "گفتگوهای خصوصی اخیر", + "Upgrade public room": "ارتقاء اتاق عمومی", + "Upgrade private room": "ارتقاء اتاق خصوصی", + "Automatically invite users": "به طور خودکار کاربران را دعوت کن", + "Resend %(unsentCount)s reaction(s)": "بازارسال %(unsentCount)s واکنش", + "Missing session data": "", + "Manually export keys": "کلیدها را به صورت دستی استخراج (Export)کن", + "No backup found!": "نسخه پشتیبان یافت نشد!", + "Incompatible local cache": "حافظه‌ی محلی ناسازگار", + "Upgrade Room Version": "ارتقاء نسخه‌ی اتاق", + "Share Room Message": "به اشتراک گذاشتن پیام اتاق", + "Collapse Reply Thread": "جمع کردن ریسه‌ی پاسخ", + "Reset everything": "همه چیز را بازراه‌اندازی (reset) کنید", + "Consult first": "ابتدا مشورت کنید", + "Save Changes": "ذخیره تغییرات", + "Leave Space": "ترک فضای کاری", + "Space settings": "تنظیمات فضای کاری", + "Unnamed Space": "فضای کاری بدون نام", + "Remember this": "این را به یاد داشته باش", + "Invalid URL": "آدرس URL نامعتبر", + "About homeservers": "درباره سرورها", + "Learn more": "بیشتر بدانید", + "Other homeserver": "سرور دیگر", + "Decline All": "رد کردن همه", + "Modal Widget": "ابزارک کمکی", + "Updating %(brand)s": "به‌روزرسانی %(brand)s", + "Looks good!": "به نظر خوب میاد!", + "Security Key": "کلید امنیتی", + "Security Phrase": "عبارت امنیتی", + "Keys restored": "کلیدها بازیابی شدند", + "Upload completed": "بارگذاری انجام شد", + "Your password": "گذرواژه شما", + "Not Trusted": "قابل اعتماد نیست", + "Session key": "کلید نشست", + "Session name": "نام نشست", + "Verify session": "تائید نشست", + "New session": "نشست جدید", + "Country Dropdown": "لیست کشور", + "Verification Request": "درخواست تأیید", + "Send report": "ارسال گزارش", + "Integration Manager": "مدیر یکپارچه‌سازی", + "Command Help": "راهنمای دستور", + "Message edits": "ویرایش پیام", + "Upload all": "بارگذاری همه", + "Upload Error": "خطای بارگذاری", + "Cancel All": "لغو همه", + "Upload files": "بارگذاری فایل‌ها", + "Phone (optional)": "شماره تلفن (اختیاری)", + "Email (optional)": "ایمیل (اختیاری)", + "Share Community": "به اشتراک‌گذاری اجتماع", + "Share User": "به اشتراک‌گذاری کاربر", + "Share Room": "به اشتراک‌گذاری اتاق", + "Send Logs": "ارسال گزارش ها", + "Suggestions": "پیشنهادات", + "Recent Conversations": "گفتگوهای اخیر", + "The following users might not exist or are invalid, and cannot be invited: %(csvNames)s": "این کاربران ممکن است وجود نداشته یا نامعتبر باشند و نمی‌توان آنها را دعوت کرد: %(csvNames)s", + "Failed to find the following users": "این کاربران یافت نشدند", + "Failed to transfer call": "انتقال تماس انجام نشد", + "A call can only be transferred to a single user.": "تماس فقط می تواند به یک کاربر منتقل شود.", + "We couldn't invite those users. Please check the users you want to invite and try again.": "ما نتوانستیم آن کاربران را دعوت کنیم. لطفاً کاربرانی را که می خواهید دعوت کنید بررسی کرده و دوباره امتحان کنید.", + "Something went wrong trying to invite the users.": "در تلاش برای دعوت از کاربران مشکلی پیش آمد.", + "We couldn't create your DM.": "نتوانستیم گفتگوی خصوصی مد نظرتان را ایجاد کنیم.", + "Failed to invite the following users to chat: %(csvUsers)s": "دعوت از این کاربران برای شروع گفتگو موفقیت‌آمیز نبود: %(csvUsers)s", + "Invite by email": "دعوت از طریق ایمیل", + "Click the button below to confirm your identity.": "برای تأیید هویت خود بر روی دکمه زیر کلیک کنید.", + "Confirm to continue": "برای ادامه تأیید کنید", + "To continue, use Single Sign On to prove your identity.": "برای ادامه از احراز هویت یکپارچه جهت اثبات هویت خود استفاده نمائید.", + "Your %(brand)s doesn't allow you to use an Integration Manager to do this. Please contact an admin.": "٪ (مارک) s شما اجازه استفاده از مدیر ادغام را برای این کار نمی دهد. لطفا با مدیر تماس بگیرید", + "Integrations not allowed": "یکپارچه‌سازی‌ها اجازه داده نشده‌اند", + "Enable 'Manage Integrations' in Settings to do this.": "برای انجام این کار 'مدیریت پکپارچه‌سازی‌ها' را در تنظیمات فعال نمائید.", + "Integrations are disabled": "پکپارچه‌سازی‌ها غیر فعال هستند", + "Incoming Verification Request": "درخواست تأیید دریافتی", + "Waiting for partner to confirm...": "منتظر تائید طرف مقابل...", + "Verifying this device will mark it as trusted, and users who have verified with you will trust this device.": "با تأیید این دستگاه، آن را به عنوان مورد اعتماد علامت‌گذاری کرده و کاربرانی که شما را تأیید کرده اند، به این دستگاه اعتماد خواهند کرد.", + "Verify this device to mark it as trusted. Trusting this device gives you and other users extra peace of mind when using end-to-end encrypted messages.": "این دستگاه را تأیید کنید تا به عنوان مورد اعتماد علامت‌گذاری شود. اعتماد به این دستگاه در هنگام استفاده از رمزنگاری سرتاسر آرامش و اطمینان بیشتری را برای شما به ارمغان می‌آورد.", + "Verifying this user will mark their session as trusted, and also mark your session as trusted to them.": "با تأیید این کاربر ، نشست وی به عنوان مورد اعتماد علامت‌گذاری شده و همچنین نشست شما به عنوان مورد اعتماد برای وی علامت‌گذاری خواهد شد.", + "Verify this user to mark them as trusted. Trusting users gives you extra peace of mind when using end-to-end encrypted messages.": "این کاربر را تأیید کنید تا به عنوان کاربر مورد اعتماد علامت‌گذاری شود. اعتماد به کاربران آرامش و اطمینان بیشتری به شما در استفاده از رمزنگاری سرتاسر می‌دهد.", + "Minimize dialog": "کوچک‌کردن پنجره", + "Maximize dialog": "بزرگ‌کردن پنجره", + "%(hostSignupBrand)s Setup": "راه‌اندازی %(hostSignupBrand)s", + "You should know": "باید بدانید", + "Terms of Service": "شرایط استفاده از خدمات", + "Privacy Policy": "سیاست حفظ حریم خصوصی", + "Cookie Policy": "سیاست کوکی", + "Learn more in our , and .": "درباره‌ی ، و بیشتر بدانید.", + "Continuing temporarily allows the %(hostSignupBrand)s setup process to access your account to fetch verified email addresses. This data is not stored.": "ادامه‌ی موقت به فرآیند راه‌اندازی%(hostSignupBrand)s اجازه می‌دهد به حساب کاربری شما برای تائید آدرس‌های ایمیلتان دسترسی داشته باشد. این داده‌ها ذخیره نمی‌شوند.", + "Failed to connect to your homeserver. Please close this dialog and try again.": "اتصال به سرور شما انجام نشد. لطفاً این پنجره را بسته و دوباره امتحان کنید.", + "Abort": "لغوکردن", + "Are you sure you wish to abort creation of the host? The process cannot be continued.": "آیا مطمئن هستید که می‌خواهید ایجاد هاست را لغو کنید؟ این فرآیند را نمی‌توان ادامه داد.", + "Confirm abort of host creation": "لغو ایجاد هاست را تأیید کنید", + "Please view existing bugs on Github first. No match? Start a new one.": "لطفاً ابتدا اشکالات موجود را در گیتهاب برنامه را مشاهده کنید. با اشکال شما مطابقتی وجود ندارد؟ مورد جدیدی را ثبت کنید.", + "Report a bug": "گزارش اشکال", + "PRO TIP: If you start a bug, please submit debug logs to help us track down the problem.": "نکته‌ای برای کاربران حرفه‌ای: اگر به مشکل نرم‌افزاری در برنامه برخورد کردید، لطفاً لاگ‌های مشکل را ارسال کنید تا به ما در ردیابی و رفع آن کمک کند.", + "There are two ways you can provide feedback and help us improve %(brand)s.": "به دو روش می توانید بازخوردهای خود را برای کمک به ما در بهبود %(brand)s برسانید.", + "Comment": "نظر", + "Add comment": "افزودن نظر", + "Please go into as much detail as you like, so we can track down the problem.": "لطفاً به اندازه دلخواه با جزئیات توضیح دهید تا بتوانیم مشکل را ردیابی و حل کنیم.", + "Tell us below how you feel about %(brand)s so far.": "در زیر به ما بگویید که تاکنون چه احساسی نسبت به %(brand)s داشته‌اید.", + "Rate %(brand)s": "به %(brand)s امتیاز دهید", + "Feedback sent": "بازخورد ارسال شد", + "Update community": "به‌روزرسانی اجتماع", + "There was an error updating your community. The server is unable to process your request.": "در به‌روزرسانی اجتماع شما خطایی روی داد. سرور قادر به پردازش درخواست شما نیست.", + "Developer Tools": "ابزارهای توسعه‌دهنده", + "Toolbox": "جعبه ابزار", + "Edit Values": "ویرایش مقادیر", + "Values at explicit levels in this room:": "مقادیر در سطوح مشخص در این اتاق:", + "Values at explicit levels:": "مقدار در سطوح مشخص:", + "Value in this room:": "مقدار در این اتاق:", + "Value:": "مقدار:", + "Save setting values": "ذخیره مقادیر تنظیمات", + "Values at explicit levels in this room": "مقادیر در سطوح مشخص در این اتاق:", + "Values at explicit levels": "مقادیر در سطوح مشخص", + "Settable at room": "قابل تنظیم در اتاق", + "Settable at global": "قابل تنظیم به شکل سراسری", + "Level": "سطح", + "Setting definition:": "تعریف تنظیم:", + "This UI does NOT check the types of the values. Use at your own risk.": "این واسط کاربری تایپ مقادیر را بررسی نمی‌کند. با مسئولیت خود استفاده کنید.", + "Caution:": "احتیاط:", + "Setting:": "تنظیم:", + "Value in this room": "مقدار در این اتاق", + "Value": "مقدار", + "Setting ID": "شناسه تنظیم", + "Failed to save settings": "تنظیمات ذخیره نشد", + "Settings Explorer": "تنظیمات جستجوگر", + "There was an error finding this widget.": "هنگام یافتن این ابزارک خطایی روی داد.", + "Active Widgets": "ابزارک‌های فعال", + "Search names and descriptions": "جستجوی نام‌ها و توضیحات", + "If you can't find the room you're looking for, ask for an invite or create a new room.": "اگر نمی‌توانید اتاقی را که به دنبال آن می‌گردید پیدا کنید ، بخواهید که شما را به آن دعوت کنند یا یک اتاق جدید بسازید.", + "To view %(spaceName)s, turn on the Spaces beta": "برای مشاهده %(spaceName)s، قابلیت فضای کاری بتا را فعال نمائید", + "To join %(spaceName)s, turn on the Spaces beta": "برای پیوستن به %(spaceName)s، قابلیت فضای کاری بتا را فعال نمائید", + "Failed to create initial space rooms": "ایجاد اتاق‌های اولیه در فضای کاری موفق نبود", + "What do you want to organise?": "چه چیزی را می‌خواهید سازماندهی کنید؟", + "You might enable this if the room will only be used for collaborating with internal teams on your homeserver. This cannot be changed later.": "اگر اتاق فقط برای همکاری با تیم های داخلی در سرور خانه شما استفاده شود ، ممکن است این قابلیت را فعال کنید. این بعدا نمی تواند تغییر کند.", + "Enable end-to-end encryption": "فعال کردن رمزنگاری سرتاسر", + "Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.": "گفتگوهای خصوصی یا اتاق‌هایی را برای افزودن انتخاب کنید. این فقط یک فضای کاری برای شماست، هیچ کس از وجود آن مطلع نخواهد شد. می‌توانید موارد بیشتری را بعدا اضافه کنید.", + "Your server requires encryption to be enabled in private rooms.": "سرور شما به گونه‌ای تنظیم شده‌است که فعال بودن رمزنگاری سرتاسر در اتاق‌های خصوصی اجباری می‌باشد.", + "You can’t disable this later. Bridges & most bots won’t work yet.": "بعداً نمی توانید این را لغو کنید. پل‌ها و بیشتر ربات‌ها هنوز کار نمی کنند.", + "Share %(name)s": "به اشتراک‌گذاری %(name)s", + "It's just you at the moment, it will be even better with others.": "در حال حاضر فقط شما حضور دارید ، با دیگران حتی بهتر هم خواهد بود.", + "Private rooms can be found and joined by invitation only. Public rooms can be found and joined by anyone in this community.": "اتاق‌های خصوصی را فقط با دعوت می‌توان یافت و به آن‌ها پیوست. اتاق‌های عمومی را هر کسی در این اجتماع می‌تواند بیابد و به آن بپیوندد.", + "Go to my first room": "برو به اتاق اول من", + "Private rooms can be found and joined by invitation only. Public rooms can be found and joined by anyone.": "اتاق‌های خصوصی را فقط با دعوت می‌توان یافت و به آن‌ها پیوست. اما اتاق‌های عمومی را هر کسی می تواند را پیدا کند و به آن‌ها ملحق شود.", + "Please enter a name for the room": "لطفاً نامی برای اتاق وارد کنید", + "Community ID": "شناسه اجتماع", + "Community Name": "نام اجتماع", + "Create Community": "ایجاد اجتماع", + "Something went wrong whilst creating your community": "هنگام ایجاد اجتماع مشکلی پیش آمد", + "Community IDs may only contain characters a-z, 0-9, or '=_-./'": "شناسه اجتماع باید فقط حاوی کاراکتر‌های a-z، 0-9 یا '=_-./' باشد", + "Community IDs cannot be empty.": "شناسه اجتماع نمی تواند خالی باشد.", + "An image will help people identify your community.": "تصویر به افراد کمک می کند تا با سهولت بیشتری اجتماع شما پیدا کنند.", + "Add image (optional)": "افزودن تصویر (اختیاری)", + "Enter name": "ورود نام", + "What's the name of your community or team?": "نام اجتماع یا تیم شما چیست؟", + "You can change this later if needed.": "در صورت نیاز می توانید بعداً این مورد را تغییر دهید.", + "Use this when referencing your community to others. The community ID cannot be changed.": "هنگام ارجاع دادن دیگران به اجتماع خود، از این مورد استفاده کنید. شناسه اجتماع قابل تغییر نیست.", + "Community ID: +:%(domain)s": "شناسه اجتماع: %(domain)s:+", + "There was an error creating your community. The name may be taken or the server is unable to process your request.": "هنگام ایجاد اجتماع خطایی روی داد. ممکن است نام گرفته شده‌باشد و یا اینکه سرور نتواند درخواست شما را پردازش کند.", + "Clear all data": "پاک کردن همه داده ها", + "Clearing all data from this session is permanent. Encrypted messages will be lost unless their keys have been backed up.": "پاک کردن همه داده های این جلسه غیرقابل بازگشت است. پیامهای رمزگذاری شده از بین می‌روند مگر اینکه از کلیدهای آنها پشتیبان تهیه شده باشد.", + "Clear all data in this session?": "همه داده‌های این نشست پاک شود؟", + "Go to my space": "برو به محیط کاری من", + "Who are you working with?": "با چه کسانی کار می‌کنید؟", + "Make sure the right people have access to %(name)s": "اطمینان حاصل کنید که افراد مناسب به %(name)s دسترسی دارند", + "Just me": "فقط من", + "A private space to organise your rooms": "یک فضای کار خصوصی برای منظم‌کردن اتاق‌هایتان", + "Me and my teammates": "من و هم‌تیمی‌هایم", + "A private space for you and your teammates": "یک فضای کار خصوصی برای شما و هم تیمی‌هایتان", + "Reason (optional)": "دلیل (اختیاری)", + "Are you sure you wish to remove (delete) this event? Note that if you delete a room name or topic change, it could undo the change.": "آیا مطمئن هستید که می خواهید این رویداد را حذف کنید؟ توجه داشته باشید که اگر نام اتاق یا تغییر موضوع را حذف کنید، این تغییر می تواند لغو شود.", + "Failed to invite the following users to your space: %(csvUsers)s": "امکان دعوت کاربرانی که در ادامه آمده‌اند به فضای کاری شما میسر نیست: %(csvUsers)s", + "Invite your teammates": "هم‌تیمی‌های خود را دعوت کنید", + "Make sure the right people have access. You can invite more later.": "اطمینان حاصل کنید که افراد مناسب دسترسی دارند. بعداً می توانید افراد بیشتری دعوت کنید.", + "This is an experimental feature. For now, new users receiving an invite will have to open the invite on to actually join.": "این یک قابلیت آزمایشی است. برای الان، کاربران جدیدی که دعوتنامه دریافت می‌کنند باید آن را بر روی باز کنند تا بتوانند عضو شوند.", + "Invite by username": "دعوت به نام کاربری", + "What are some things you want to discuss in %(spaceName)s?": "برخی از مواردی که می خواهید درباره‌ی آن‌ها در %(spaceName)s بحث کنید، چیست؟", + "Let's create a room for each of them.": "بیایید برای هر یک از آنها یک اتاق درست کنیم.", + "You can add more later too, including already existing ones.": "بعداً می توانید موارد بیشتری را اضافه کنید ، از جمله موارد موجود.", + "What projects are you working on?": "روی چه پروژه‌هایی کار می‌کنید؟", + "Confirm Removal": "تأیید حذف", + "Removing…": "در حال حذف…", + "Invite people to join %(communityName)s": "از افراد برای پیوستن به %(communityName)s دعوت کنید", + "Send %(count)s invites|one": "ارسال %(count)s دعوت", + "Send %(count)s invites|other": "ارسال %(count)s دعوت", + "Show": "نمایش", + "Hide": "پنهان کردن", + "People you know on %(brand)s": "افرادی که در %(brand)s می‌شناسید", + "Add another email": "ایمیل دیگری اضافه کنید", + "Unable to load commit detail: %(msg)s": "بارگیری جزئیات commit انجام نشد: %(msg)s", + "If there is additional context that would help in analysing the issue, such as what you were doing at the time, room IDs, user IDs, etc., please include those things here.": "اگر زمینه دیگری وجود دارد که می تواند به تجزیه و تحلیل مسئله کمک کند، مانند آنچه در آن زمان انجام می دادید، شناسه اتاق، شناسه کاربر و غیره ، لطفاً موارد ذکر شده را در اینجا وارد کنید.", + "Notes": "یادداشت‌ها", + "We'll create rooms for each of them. You can add more later too, including already existing ones.": "ما برای هر یک از آنها اتاق ایجاد خواهیم کرد. بعداً می توانید موارد دیگر را اضافه کنید ، از جمله موارد موجود.", + "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "سعی شد یک نقطه‌ی زمانی خاص در پیام‌های این اتاق بارگیری و نمایش داده شود، اما شما دسترسی لازم برای مشاهده‌ی پیام را ندارید.", + "GitHub issue": "مسئله GitHub", + "Download logs": "دانلود گزارش‌ها", + "Before submitting logs, you must create a GitHub issue to describe your problem.": "قبل از ارسال گزارش‌ها، برای توصیف مشکل خود باید یک مسئله در GitHub ایجاد کنید.", + "Tried to load a specific point in this room's timeline, but was unable to find it.": "سعی شد یک نقطه‌ی زمانی خاص در پیام‌های این اتاق بارگیری و نمایش داده شود، اما پیداکردن آن میسر نیست.", + "Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "گزارش‌های اشکال زدایی حاوی داده‌های استفاده از برنامه شامل نام کاربری شما، شناسه‌ها یا نام مستعار اتاق‌ها یا گروه‌هایی است که بازدید کرده‌اید و همچنین نام‌ کاربری کاربران دیگر. این گزارش‌ها حاوی پیام‌های شما نمی‌باشند.", + "Failed to load timeline position": "بارگیری و نمایش پیام‌ها با مشکل مواجه شد", + "Uploading %(filename)s and %(count)s others|other": "در حال بارگذاری %(filename)s و %(count)s مورد دیگر", + "Uploading %(filename)s and %(count)s others|zero": "در حال بارگذاری %(filename)s", + "Uploading %(filename)s and %(count)s others|one": "در حال بارگذاری %(filename)s و %(count)s مورد دیگر", + "Failed to find the general chat for this community": "گفتگوی عمومی برای این اجتماع پیدا نشد", + "Reminder: Your browser is unsupported, so your experience may be unpredictable.": "یادآوری: مرورگر شما پشتیبانی نمی شود ، بنابراین ممکن است تجربه شما غیرقابل پیش بینی باشد.", + "Preparing to download logs": "در حال آماده سازی برای بارگیری گزارش ها", + "Got an account? Sign in": "حساب کاربری دارید؟ وارد شوید", + "Failed to send logs: ": "ارسال گزارش با خطا مواجه شد: ", + "New here? Create an account": "تازه وارد هستید؟ یک حساب کاربری ایجاد کنید", + "Thank you!": "با سپاس!", + "The email address linked to your account must be entered.": "آدرس ایمیلی که به حساب کاربری شما متصل است، باید وارد شود.", + "Logs sent": "گزارش‌های مربوط ارسال شد", + "Preparing to send logs": "در حال آماده سازی برای ارسال گزارش ها", + "Please tell us what went wrong or, better, create a GitHub issue that describes the problem.": "لطفاً به ما بگویید چه مشکلی پیش آمد و یا اینکه لطف کنید و یک مسئله GitHub ایجاد کنید که مشکل را توصیف کند.", + "Changing your password will reset any end-to-end encryption keys on all of your sessions, making encrypted chat history unreadable. Set up Key Backup or export your room keys from another session before resetting your password.": "تغییر گذرواژه ، تمام کلیدهای رمزنگاریِ سرتاسر در تمام نشست‌های شما را پاک کرده و تاریخچه چت‌های رمزشده را غیرقابل خواندن می‌کند. قبل از تغییر گذرواژه خود ، پشتیبان‌گیری از کلید‌ها را تنظیم یا کلیدهای اتاق‌های خود را از نشست دیگری استخراج (Export) کنید.", + "Send feedback": "ارسال بازخورد", + "You may contact me if you have any follow up questions": "در صورت داشتن هرگونه سوال پیگیری ممکن است با من تماس بگیرید", + "Feedback": "بازخورد", + "To leave the beta, visit your settings.": "برای خروج از بتا به بخش تنظیمات مراجعه کنید.", + "Your platform and username will be noted to help us use your feedback as much as we can.": "سیستم‌عامل و نام کاربری شما ثبت خواهد شد تا به ما کمک کند تا جایی که می توانیم از نظرات شما استفاده کنیم.", + "%(featureName)s beta feedback": "بازخورد بتا برای %(featureName)s", + "A verification email will be sent to your inbox to confirm setting your new password.": "برای تأیید تنظیم گذرواژه جدید ، یک ایمیل تأیید به صندوق ورودی شما ارسال می شود.", + "An email has been sent to %(emailAddress)s. Once you've followed the link it contains, click below.": "یک ایمیل به %(emailAddress)s ارسال شده‌است. بعد از کلیک بر روی لینک داخل آن ایمیل، روی مورد زیر کلیک کنید.", + "Done": "انجام شد", + "Thank you for your feedback, we really appreciate it.": "از بازخورد شما متشکریم ، ما واقعاً از آن استقبال می کنیم.", + "Beta feedback": "بازخورد بتا", + "Close dialog": "بستن گفتگو", + "Invite anyway": "به هر حال دعوت کن", + "You have been logged out of all sessions and will no longer receive push notifications. To re-enable notifications, sign in again on each device.": "شما از همه نشست‌ها خارج شده و دیگر اعلان پیام‌ها را دریافت نخواهید کرد. برای فعال کردن مجدد اعلان‌ها در هر دستگاه، دوباره وارد شوید.", + "Invite anyway and never warn me again": "به هر حال دعوت کن و دیگر هرگز به من هشدار نده", + "Unable to find profiles for the Matrix IDs listed below - would you like to invite them anyway?": "برای شناسه‌های ماتریکس زیر، پروفایلی پیدا نشد - آیا به هر حال می خواهید آنها را دعوت کنید؟", + "Invalid homeserver discovery response": "پاسخ جستجوی سرور معتبر نیست", + "Failed to get autodiscovery configuration from server": "دریافت پیکربندیِ جستجوی خودکار از سرور موفقیت‌آمیز نبود", + "The following users may not exist": "کاربران زیر ممکن است وجود نداشته باشند", + "Use an identity server to invite by email. Manage in Settings.": "از یک سرور هویت‌سنجی برای دعوت از طریق ایمیل استفاده کنید. اینکار را می‌توانید از طریق بخش تنظیمات انجام دهید.", + "Invalid base_url for m.homeserver": "base_url نامعتبر برای m.homeserver", + "Homeserver URL does not appear to be a valid Matrix homeserver": "به نظر می‌رسد که آدرس سرور، متعلق به یک سرور معتبر نباشد", + "Use an identity server to invite by email. Use the default (%(defaultIdentityServerName)s) or manage in Settings.": "از یک سرور هویت‌سنجی برای دعوت از طریق ایمیل استفاده کنید. از پیش فرض (%(defaultIdentityServerName)s) استفاده کنید یا آن را در بخش تنظیمات مدیریت کنید.", + "Invalid identity server discovery response": "پاسخ نامعتبر برای جستجوی سرور هویت‌سنجی", + "Invalid base_url for m.identity_server": "base_url نامعتبر برای سرور m.identity_server", + "Identity server URL does not appear to be a valid identity server": "به نظر می‌رسد آدرس سرور هویت‌سنجی، متعلق به یک سرور هویت‌سنجی معتبر نیست", + "Try using one of the following valid address types: %(validTypesList)s.": "از یکی از انواع آدرس‌های معتبر زیر استفاده کنید: %(validTypesList)s.", + "Wrong file type": "نوع فایل اشتباه است", + "Confirm encryption setup": "راه‌اندازی رمزگذاری را تأیید کنید", + "You have entered an invalid address.": "آدرسی که وارد کرده‌اید نامعتبر است.", + "General failure": "خطای عمومی", + "That doesn't look like a valid email address": "آدرس ایمیل نامعتبر است", + "This homeserver does not support login using email address.": "این سرور از ورود با استفاده از آدرس ایمیل پشتیبانی نمی کند.", + "Please contact your service administrator to continue using this service.": "لطفاً برای ادامه استفاده از این سرویس با مدیر سرور خود تماس بگیرید .", + "email address": "آدرس ایمیل", + "Matrix Room ID": "شناسه اتاق ماتریکس", + "Matrix ID": "شناسه ماتریکس", + "Create a new room": "ایجاد اتاق جدید", + "This account has been deactivated.": "این حساب غیر فعال شده است.", + "Please note you are logging into the %(hs)s server, not matrix.org.": "لطفا توجه کنید شما به سرور %(hs)s وارد شده‌اید، و نه سرور matrix.org.", + "Want to add a new room instead?": "آیا می‌خواهید یک اتاق جدید را بیفزایید؟", + "Add existing rooms": "افزودن اتاق‌های موجود", + "Space selection": "انتخاب فضای کاری", + "There was a problem communicating with the homeserver, please try again later.": "در برقراری ارتباط با سرور مشکلی پیش آمده، لطفاً چند لحظه‌ی دیگر مجددا امتحان کنید.", + "Filter your rooms and spaces": "اتاق‌ها و فضاهای کاری خود را فیلتر کنید", + "Adding rooms... (%(progress)s out of %(count)s)|one": "در حال افزودن اتاق‌ها...", + "Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or enable unsafe scripts.": "امکان اتصال به سرور از طریق پروتکل‌های HTTP و HTTPS در مروگر شما میسر نیست. یا از HTTPS استفاده کرده و یا حالت اجرای غیرامن اسکریپت‌ها را فعال کنید.", + "Adding rooms... (%(progress)s out of %(count)s)|other": "در حال افزودن اتاق‌ها... (%(progress)s از %(count)s)", + "Not all selected were added": "همه‌ی موارد انتخاب شده، اضافه نشدند", + "Matrix rooms": "اتاق‌های ماتریکس", + "%(networkName)s rooms": "اتاق‌های %(networkName)s", + "Add a new server...": "افزودن سرور جدید ...", + "Server name": "نام سرور", + "Enter the name of a new server you want to explore.": "نام سرور جدیدی که می خواهید در آن کاوش کنید را وارد کنید.", + "Can't connect to homeserver - please check your connectivity, ensure your homeserver's SSL certificate is trusted, and that a browser extension is not blocking requests.": "اتصال به سرور میسر نیست - لطفا اتصال اینترنت خود را بررسی کنید؛ اطمینان حاصل کنید گواهینامه‌ی SSL سرور شما قابل اعتماد است، و اینکه پلاگینی بر روی مرورگر شما مانع از ارسال درخواست به سرور نمی‌شود.", + "Add a new server": "افزودن سرور جدید", + "Matrix": "ماتریکس", + "Remove server": "حذف سرور", + "Are you sure you want to remove %(serverName)s": "آیا مطمئن هستید که می خواهید %(serverName)s را حذف کنید", + "Your server": "سرور شما", + "Can't find this server or its room list": "این سرور و یا لیست اتاق‌های آن پیدا نمی شود", + "You are not allowed to view this server's rooms list": "شما مجاز به مشاهده لیست اتاق‌های این سرور نمی‌باشید", + "Looks good": "به نظر خوب میاد", + "Unable to query for supported registration methods.": "درخواست از روش‌های پشتیبانی‌شده‌ی ثبت‌نام میسر نیست.", + "Enter a server name": "نام سرور را وارد کنید", + "And %(count)s more...|other": "و %(count)s مورد بیشتر ...", + "Sign in with single sign-on": "با احراز هویت یکپارچه وارد شوید", + "This server does not support authentication with a phone number.": "این سرور از قابلیت احراز با شماره تلفن پشتیبانی نمی کند.", + "Continue with %(provider)s": "با %(provider)s ادامه دهید", + "That username already exists, please try another.": "این نام کاربری از قبل وجود دارد ، لطفاً نام دیگری را امتحان کنید.", + "Homeserver": "سرور", + "Continue with %(ssoButtons)s": "با %(ssoButtons)s ادامه بده", + "Join millions for free on the largest public server": "به بزرگترین سرور عمومی با میلیون ها نفر کاربر بپیوندید", + "%(ssoButtons)s Or %(usernamePassword)s": "%(ssoButtons)s یا %(usernamePassword)s", + "Your new account (%(newAccountId)s) is registered, but you're already logged into a different account (%(loggedInUserId)s).": "حساب جدید شما (%(newAccountId)s) s) ثبت شده‌است ، اما شما قبلاً به حساب کاربری دیگری (%(loggedInUserId)s) وارد شده‌اید.", + "You can use the custom server options to sign into other Matrix servers by specifying a different homeserver URL. This allows you to use Element with an existing Matrix account on a different homeserver.": "می توانید از طریق گزینه‌ی سرور سفارشی با استفاده از آدرس سروری دیگر، به دیگر سرورهای Matrix متصل شوید. با این کار می توانید از Element جهت اتصال به یک اکانت ماتریکس بر روی سروری دیگر استفاده کنید.", + "Continue with previous account": "با حساب کاربری قبلی ادامه دهید", + "Log in to your new account.": "به حساب کاربری جدید خود وارد شوید.", + "You can now close this window or log in to your new account.": "اکنون می توانید این پنجره را ببندید یا به حساب کاربری جدید خود وارد شوید.", + "Use another login": "از ورود دیگری استفاده کنید", + "Failed to re-authenticate due to a homeserver problem": "به دلیل مشکلی که در سرور وجود دارد ، احراز هویت مجدد انجام نشد", + "You cannot sign in to your account. Please contact your homeserver admin for more information.": "نمی توانید وارد حساب کاربری خود شوید. لطفا برای اطلاعات بیشتر با مدیر سرور خود تماس بگیرید.", + "Server Options": "گزینه های سرور", + "This address is already in use": "این آدرس قبلاً استفاده شده‌است", + "Warning: Your personal data (including encryption keys) is still stored in this session. Clear it if you're finished using this session, or want to sign in to another account.": "هشدار: داده های شخصی شما (از جمله کلیدهای رمزنگاری) هنوز در این نشست ذخیره می شوند. اگر استفاده از این نشست را به پایان رسانده‌اید یا می‌خواهید وارد حساب کاربری دیگری شوید ، آن را پاک کنید.", + "This address is available to use": "این آدرس برای استفاده در دسترس است", + "Please provide a room address": "لطفاً آدرس اتاق را ارائه تعیین کنید", + "e.g. my-room": "به عنوان مثال، my-room", + "Some characters not allowed": "برخی از کاراکترها مجاز نیستند", + "Command Autocomplete": "تکمیل خودکار دستور", + "Community Autocomplete": "تکمیل خودکار اجتماع", + "Room address": "آدرس اتاق", + "Results from DuckDuckGo": "نتایج از موتور جستجوی DuckDuckGo", + "In reply to ": "در پاسخ به", + "DuckDuckGo Results": "نتایج موتور جستجوی DuckDuckGo", + "Emoji Autocomplete": "تکمیل خودکار شکلک", + "Notify the whole room": "به کل اتاق اطلاع بده", + "Unable to load event that was replied to, it either does not exist or you do not have permission to view it.": "بارگیری رویدادی که به آن پاسخ داده شد امکان پذیر نیست، یا وجود ندارد یا شما اجازه مشاهده آن را ندارید.", + "Room Notification": "اعلان اتاق", + "Notification Autocomplete": "تکمیل خودکار اعلان", + "Room Autocomplete": "تکمیل خودکار اتاق", + "Space Autocomplete": "تکمیل خودکار فضای کاری", + "User Autocomplete": "تکمیل خودکار کاربر", + "We'll store an encrypted copy of your keys on our server. Secure your backup with a Security Phrase.": "ما یک نسخه رمزگذاری شده از کلیدهای شما را در سرور خود ذخیره خواهیم کرد. پشتیبان خود را با یک عبارت امنیتی ایمن کنید.", + "For maximum security, this should be different from your account password.": "برای حداکثر امنیت ، این باید با گذرواژه‌ی حساب شما متفاوت باشد.", + "Enter a Security Phrase": "یک عبارت امنیتی وارد کنید", + "Great! This Security Phrase looks strong enough.": "عالی! این عبارت امنیتی به اندازه کافی قوی به نظر می رسد.", + "QR Code": "کد QR", + "Custom level": "سطح دلخواه", + "Set up with a Security Key": "یک کلید امنیتی تنظیم کنید", + "Power level": "سطح قدرت", + "%(oneUser)smade no changes %(count)s times|one": "%(oneUser)s هیچ تغییری ایجاد نکرد", + "%(oneUser)smade no changes %(count)s times|other": "%(oneUser)s %(count)s مرتبه هیچ تغییری ایجاد نکرد", + "That matches!": "مطابقت دارد!", + "Use a different passphrase?": "از عبارت امنیتی دیگری استفاده شود؟", + "That doesn't match.": "مطابقت ندارد.", + "Go back to set it again.": "برای تنظیم مجدد آن به عقب برگردید.", + "%(severalUsers)smade no changes %(count)s times|one": "%(severalUsers)s هیچ تغییری ایجاد نکردند", + "%(severalUsers)smade no changes %(count)s times|other": "%(severalUsers)s %(count)s تغییری ایجاد نکردند", + "Enter your Security Phrase a second time to confirm it.": "عبارت امنیتی خود را برای تائید مجددا وارد کنید.", + "%(oneUser)schanged their avatar %(count)s times|one": "%(oneUser)s آواتار خود را تغییر داد", + "Repeat your Security Phrase...": "عبارت امنیتی خود را تکرار کنید ...", + "Your Security Key is a safety net - you can use it to restore access to your encrypted messages if you forget your Security Phrase.": "کلید امنیتی شما یک راهکار امنیتی است - اگر عبارت امنیتی خود را فراموش کنید ، می توانید از آن برای بازیابی دسترسی به پیام‌های رمز‌شده خود استفاده کنید.", + "%(oneUser)schanged their avatar %(count)s times|other": "%(oneUser)s آواتار خود را %(count)s مرتبه تغییر داده‌است", + "Keep a copy of it somewhere secure, like a password manager or even a safe.": "یک نسخه از آن را در جایی امن نگهداری کنید.", + "Your Security Key": "کلید امنیتی شما", + "%(severalUsers)schanged their avatar %(count)s times|one": "%(severalUsers)s آواتار خود را تغییر دادند", + "Your Security Key has been copied to your clipboard, paste it to:": "از کلید امنیتی شما رونوشت گرفته شده است، آن را الصاق کنید به:", + "%(severalUsers)schanged their avatar %(count)s times|other": "%(severalUsers)s %(count)s مرتبه آواتار خود را تغییر دادند", + "%(oneUser)schanged their name %(count)s times|one": "%(oneUser)s نام خود را تغییر داد", + "%(oneUser)schanged their name %(count)s times|other": "%(oneUser)s نام خود را %(count)s مرتبه تغییر داد", + "Your Security Key is in your Downloads folder.": "کلید امنیتی شما در پوشه‌ Downloads قرار دارد.", + "Print it and store it somewhere safe": "این را پرینت کرده و در جایی امن نگهداری کنید", + "Save it on a USB key or backup drive": "بر روی فلش مموری یا پارتیشن پشتیبان ذخیره کنید", + "Copy it to your personal cloud storage": "روی فضای شخصی خودتان نسخه‌ی رونوشت بگیرید", + "Your keys are being backed up (the first backup could take a few minutes).": "در حال پیشتیبان‌گیری از کلیدهای شما (اولین نسخه پشتیبان ممکن است چند دقیقه طول بکشد).", + "%(severalUsers)schanged their name %(count)s times|one": "%(severalUsers)s نام خود را تغییر دادند", + "Without setting up Secure Message Recovery, you won't be able to restore your encrypted message history if you log out or use another session.": "بدون تنظیم امکان پشتیبان‌گیریِ امن پیام‌ها، در صورت ورود به سیستم یا استفاده از نشست دیگر ، نمی توانید تاریخچه‌ی پیام‌های رمزشده خود را بازیابی کنید.", + "Set up Secure Message Recovery": "امکان بازیابی پیام‌های امن را تنظیم کنید", + "Secure your backup with a Security Phrase": "نسخه‌ی پشتیبان خود را با استفاده از عبارت امنیتی امن کنید", + "Confirm your Security Phrase": "عبارت امنیتی خود را تأیید کنیدعبارت امنیتی خود را تائید نمائید", + "Make a copy of your Security Key": "از کلید امنیتی خود رونوشت بگیرید", + "%(severalUsers)schanged their name %(count)s times|other": "%(severalUsers)s نام خود را %(count)s بار تغییر دادند", + "was kicked %(count)s times|one": "اخراج شد", + "Starting backup...": "آغاز پشتیبان‌گیری...", + "Success!": "موفقیت‌آمیز بود!", + "Create key backup": "ساختن نسخه‌ی پشتیبان کلید", + "Unable to create key backup": "ایجاد کلید پشتیبان‌گیری امکان‌پذیر نیست", + "Generate a Security Key": "یک کلید امنیتی ایجاد کنید", + "We’ll generate a Security Key for you to store somewhere safe, like a password manager or a safe.": "ما یک کلید امنیتی برای شما تولید کردیم تا آن را در یک جای امن ذخیره کنید.", + "was kicked %(count)s times|other": "%(count)s بار اخراج شد", + "Use a secret phrase only you know, and optionally save a Security Key to use for backup.": "از یک عبارت محرمانه که فقط خودتان می‌دانید استفاده کنید، و محض احتیاط کلید امینی خود را برای استفاده هنگام پشتیبان‌گیری ذخیره نمائید.", + "were kicked %(count)s times|one": "اخراج شدند", + "were kicked %(count)s times|other": "%(count)s بار اخراج شدند", + "was unbanned %(count)s times|one": "رفع تحریم شد", + "was unbanned %(count)s times|other": "%(count)s بار رفع تحریم شد", + "were unbanned %(count)s times|one": "رفع تحریم شد", + "were unbanned %(count)s times|other": "%(count)s بار رفع تحریم شد", + "Verification Requests": "درخواست های تأیید", + "View Servers in Room": "مشاهده سرورها در اتاق", + "Explore Account Data": "کاوش داده‌های حساب کاربری", + "Explore Room State": "کاوش حالت اتاق", + "Filter results": "پالایش نتایج", + "Send Account Data": "ارسال اطلاعات حساب کاربری", + "Event Content": "محتوای رخداد", + "State Key": "کلید حالت", + "Event Type": "نوع رخداد", + "Failed to send custom event.": "رخداد سفارشی ارسال نشد.", + "Event sent!": "رخداد ارسال شد!", + "You must specify an event type!": "شما باید نوع رخداد را مشخص کنید!", + "Send Custom Event": "ارسال رخداد سفارشی", + "Please forget all messages I have sent when my account is deactivated (Warning: this will cause future users to see an incomplete view of conversations)": "لطفا بعد از غیرفعال کردن حساب کاربری، تمام پیام‌هایی را که ارسال کرده‌ام، فراموش کن ( هشدار: این امر باعث می شود کاربران آینده نمای ناقصی از مکالماتی که شما در آن‌ها حضور داشته‌اید را مشاهده کنند)", + "Message visibility in Matrix is similar to email. Our forgetting your messages means that messages you have sent will not be shared with any new or unregistered users, but registered users who already have access to these messages will still have access to their copy.": "قابلیت مشاهده پیام در پروتکل ماتریکس مانند ایمیل است. فراموش کردن پیام‌های شما به این معنی است که پیام‌هایی که ارسال کرده‌اید با هیچ کاربر جدید یا ثبت نشده‌ای به اشتراک گذاشته نخواهد شد ، اما کاربران ثبت نام شده‌ای که از قبل به این پیام ها دسترسی دارند همچنان دسترسی خود را خواهند داشت.", + "Deactivating your account does not by default cause us to forget messages you have sent. If you would like us to forget your messages, please tick the box below.": "غیرفعال کردن حساب شما به طور پیش فرض باعث نمی شود پیام‌های ارسالی شما را فراموش کنیم. اگر می‌خواهید پیام های شما را فراموش کنیم ، لطفاً کادر زیر را علامت بزنید.", + "This will make your account permanently unusable. You will not be able to log in, and no one will be able to re-register the same user ID. This will cause your account to leave all rooms it is participating in, and it will remove your account details from your identity server. This action is irreversible.": "این کار باعث می شود حساب کاربری شما برای همیشه غیرقابل استفاده شود. شما قادر به ورود به برنامه نخواهید بود و هیچ کس نمی تواند همان شناسه کاربری مشابه را دوباره ثبت کند. این باعث می شود که حساب شما از همه اتاق هایی که در آن‌ها عضو بودید خارج شده و جزئیات حساب شما از سرور هویت‌سنجی حذف گردد. این عمل برگشت‌ناپذیر است.", + "Server did not return valid authentication information.": "سرور اطلاعات احراز هویت معتبری را باز نگرداند.", + "Server did not require any authentication": "سرور به احراز هویت احتیاج نداشت", + "There was a problem communicating with the server. Please try again.": "مشکلی در برقراری ارتباط با سرور وجود داشت. لطفا دوباره تلاش کنید.", + "To continue, please enter your password:": "برای ادامه لطفا گذرواژه خود را وارد کنید:", + "Confirm account deactivation": "غیرفعال کردن حساب کاربری را تأیید کنید", + "Are you sure you want to deactivate your account? This is irreversible.": "آیا از غیرفعال‌کردن حساب کاربری خود اطمینان دارید؟ این کار غیر قابل بازگشت است.", + "Confirm your account deactivation by using Single Sign On to prove your identity.": "برای غیرفعال‌کردن حساب کاربری خود ابتدا باید هویت خود را ثابت کنید که برای این کار می‌توانید از احراز هویت یکپارچه استفاده کنید.", + "Continue With Encryption Disabled": "با رمزنگاری غیرفعال ادامه بده", + "Incompatible Database": "پایگاه داده ناسازگار", + "You've previously used a newer version of %(brand)s with this session. To use this version again with end to end encryption, you will need to sign out and back in again.": "شما قبلاً با این نشست از نسخه جدیدتر %(brand)s استفاده کرده‌اید. برای استفاده مجدد از این نسخه با قابلیت رمزنگاری سرتاسر ، باید از حسابتان خارج شده و دوباره وارد برنامه شوید.", + "To avoid losing your chat history, you must export your room keys before logging out. You will need to go back to the newer version of %(brand)s to do this": "برای جلوگیری از از دست دادن تاریخچه‌ی گفتگوی خود باید قبل از ورود به برنامه ، کلیدهای اتاق خود را استخراج (Export) کنید. برای این کار باید از نسخه جدیدتر %(brand)s استفاده کنید", + "Sign out": "خروج از حساب کاربری", + "Block anyone not part of %(serverName)s from ever joining this room.": "از عضوشدن کاربرانی در این اتاق که حساب آن‌ها متعلق به سرور %(serverName)s است، جلوگیری کن.", + "Make this room public": "این اتاق را عمومی کنید", + "Topic (optional)": "موضوع (اختیاری)", + "Create a room in %(communityName)s": "یک اتاق در %(communityName)s بسازید", + "Create a private room": "ساختن اتاق خصوصی", + "Create a public room": "ساختن اتاق عمومی", + "You might disable this if the room will be used for collaborating with external teams who have their own homeserver. This cannot be changed later.": "اگر از اتاق برای همکاری با تیم های خارجی که سرور خود را دارند استفاده شود ، ممکن است این را غیرفعال کنید. این نمی‌تواند بعدا تغییر کند.", + "If you can't find the room you're looking for, ask for an invite or Create a new room.": "اگر نمی توانید اتاقی را که به دنبال آن می گردید پیدا کنید ، از یکی از اعضای آن بخواهید شما را دعوت کند و یا یک اتاق جدید بسازید.", + "You can't send any messages until you review and agree to our terms and conditions.": "تا زمانی که شرایط و ضوابط سرویس ما را مطالعه و با آن موافقت نکنید، نمی توانید هیچ پیامی ارسال کنید.", + "Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. Please contact your service administrator to continue using the service.": "پیام شما ارسال نشد زیرا این سرور به محدودیت تعداد کاربر فعال ماهانه‌ی خود رسیده است. لطفاً برای ادامه استفاده از سرویس با مدیر سرور خود تماس بگیرید .", + "Your message wasn't sent because this homeserver has been blocked by it's administrator. Please contact your service administrator to continue using the service.": "پیام شما ارسال نشد زیرا این سرور توسط مدیر آن مسدود شده است. لطفاً برای ادامه استفاده از سرویس با مدیر سرور خود تماس بگیرید .", + "Your message wasn't sent because this homeserver has exceeded a resource limit. Please contact your service administrator to continue using the service.": "پیام شما ارسال نشد زیرا این سرور از محدودیت منابع فراتر رفته است. لطفاً برای ادامه استفاده از سرویس با مدیر سرور خود تماس بگیرید .", + "Some of your messages have not been sent": "بعضی از پیام‌های شما ارسال نشده‌اند", + "You can select all or individual messages to retry or delete": "شما می‌توانید یک یا همه‌ی پیام‌ها را برای تلاش مجدد یا حذف انتخاب کنید", + "Sent messages will be stored until your connection has returned.": "پیام‌های ارسالی تا زمان بازگشت اتصال شما ذخیره خواهند ماند.", + "Server may be unavailable, overloaded, or search timed out :(": "سرور ممکن است در دسترس نباشد ، بار زیادی روی آن قرار گرفته یا زمان جستجو به پایان رسیده‌باشد :(", + "You have %(count)s unread notifications in a prior version of this room.|other": "شما %(count)s اعلان خوانده‌نشده در نسخه‌ی قبلی این اتاق دارید.", + "You have %(count)s unread notifications in a prior version of this room.|one": "شما %(count)s اعلان خوانده‌نشده در نسخه‌ی قبلی این اتاق دارید.", + "%(count)s members|other": "%(count)s عضو", + "%(count)s members|one": "%(count)s عضو", + "%(count)s rooms|other": "%(count)s اتاق", + "%(count)s rooms|one": "%(count)s اتاق", + "This room is suggested as a good one to join": "این اتاق به عنوان یک گزینه‌ی خوب برای عضویت پیشنهاد می شود", + "Suggested": "پیشنهادی", + "Your server does not support showing space hierarchies.": "سرور شما از نمایش سلسله مراتبی فضاهای کاری پشتیبانی نمی کند.", + "%(count)s rooms and %(numSpaces)s spaces|other": "%(count)s اتاق و %(numSpaces)s فضای کاری", + "%(count)s rooms and %(numSpaces)s spaces|one": "%(count)s اتاق و %(numSpaces)s فضای کاری", + "%(count)s rooms and 1 space|other": "%(count)s اتاق و یک فضای کاری", + "%(count)s rooms and 1 space|one": "%(count)s اتاق و یک فضای‌کاری", + "Select a room below first": "ابتدا یک اتاق از لیست زیر انتخاب کنید", + "Failed to remove some rooms. Try again later": "حذف برخی اتاق‌ها با مشکل همراه بود. لطفا بعدا تلاش فرمائید", + "Mark as not suggested": "علامت‌گذاری به عنوان پیشنهاد‌نشده", + "Mark as suggested": "علامت‌گذاری به عنوان پیشنهاد‌شده", + "You may want to try a different search or check for typos.": "ممکن است بخواهید یک جستجوی دیگر انجام دهید یا غلط‌های املایی را بررسی کنید.", + "was banned %(count)s times|one": "تحریم شد", + "was banned %(count)s times|other": "%(count)s بار تحریم شد", + "Safeguard against losing access to encrypted messages & data by backing up encryption keys on your server.": "برای در امان ماندن در برابر از دست‌دادن پیام‌ها و داده‌های رمزشده‌ی خود، از کلید‌های رمزنگاری خود یک نسخه‌ی پشتیبان بر روی سرور قرار دهید.", + "were banned %(count)s times|one": "تحریم شد", + "were banned %(count)s times|other": "%(count)s بار تحریم شد", + "was invited %(count)s times|one": "دعوت شد", + "was invited %(count)s times|other": "%(count)s بار دعوت شده است", + "Enter your account password to confirm the upgrade:": "گذرواژه‌ی خود را جهت تائيد عملیات ارتقاء وارد کنید:", + "were invited %(count)s times|one": "دعوت شدند", + "were invited %(count)s times|other": "%(count)s بار دعوت شده‌اند", + "Restore your key backup to upgrade your encryption": "برای ارتقاء رمزنگاری، ابتدا نسخه‌ی پشتیبان خود را بازیابی کنید", + "%(oneUser)shad their invitation withdrawn %(count)s times|one": "%(oneUser)s دعوت خود را پس گرفته است", + "%(oneUser)shad their invitation withdrawn %(count)s times|other": "%(oneUser)s دعوت خود را %(count)s مرتبه پس‌گرفته‌است", + "Restore": "بازیابی", + "%(severalUsers)shad their invitations withdrawn %(count)s times|one": "%(severalUsers)s دعوت‌های خود را پس‌گرفتند", + "%(name)s cancelled verifying": "%(name)s تأیید هویت را لغو کرد", + "You cancelled verifying %(name)s": "شما تأیید هویت %(name)s را لغو کردید", + "You verified %(name)s": "شما هویت %(name)s را تأیید کردید", + "You have ignored this user, so their message is hidden. Show anyways.": "شما این کاربر را نادیده گرفته‌اید، بنابراین پیام او نمایش داده نمی‌شود. نمایش بده.", + "Video conference started by %(senderName)s": "کنفرانس ویدئویی توسط %(senderName)s آغاز شده است", + "Video conference updated by %(senderName)s": "کنفرانس ویدیویی توسط %(senderName)s به روز شد", + "Video conference ended by %(senderName)s": "کنفرانس ویدیویی توسط %(senderName)s به پایان رسید", + "Join the conference from the room information card on the right": "از طریق کارت اطلاعات اتاق در سمت راست، به کنفرانس بپیوندید", + "Join the conference at the top of this room": "از بالای این اتاق به کنفرانس بپوندید", + "Show image": "نمایش تصویر", + "Error decrypting image": "خطا در رمزگشایی تصویر", + "Invalid file%(extra)s": "پرونده نامعتبر%(extra)s", + "Message Actions": "اقدامات پیام", + "Reply": "پاسخ", + "Retry": "تلاش مجدد", + "Edit": "ویرایش", + "React": "واکنش", + "Error decrypting audio": "خطا در رمزگشایی صدا", + "The encryption used by this room isn't supported.": "رمزگذاری استفاده شده توسط این اتاق پشتیبانی نمی شود.", + "Encryption not enabled": "رمزگذاری فعال نیست", + "Ignored attempt to disable encryption": "تلاش برای غیرفعال کردن رمزگذاری نادیده گرفته شد", + "Encryption enabled": "رمزگذاری فعال است", + "Messages in this room are end-to-end encrypted. When people join, you can verify them in their profile, just tap on their avatar.": "پیام های موجود در این اتاق به صورت سرتاسر رمزگذاری شده‌اند. هنگام ورود افراد، می توانید آن‌ها را از طریق نمایه آن‌ها تأیید کنید، کافی است روی آواتار آن‌ها ضربه بزنید.", + "Messages here are end-to-end encrypted. Verify %(displayName)s in their profile - tap on their avatar.": "پیام های اینجا به صورت سرتاسر رمزگذاری شده هستند. %(displayName)s را در نمایه خود تأیید کنید - روی آواتار او ضربه بزنید.", + "Compare emoji": "مقایسه شکلک", + "Verification cancelled": "تأیید هویت لغو شد", + "You cancelled verification.": "شما تأیید هویت را لغو کردید.", + "%(displayName)s cancelled verification.": "%(displayName)s تایید هویت را لغو کرد.", + "You cancelled verification on your other session.": "شما تأیید صحت جلسه دیگر خود را لغو کردید.", + "Verification timed out.": "مهلت تأیید تمام شد.", + "Start verification again from their profile.": "دوباره تأیید را از نمایه آنها شروع کنید.", + "Start verification again from the notification.": "از اعلان دوباره تأیید را شروع کنید.", + "Got it": "فهمیدم", + "Verified": "تأیید شد", + "You've successfully verified %(displayName)s!": "شما%(displayName)s را با موفقیت تأیید کردید!", + "You've successfully verified %(deviceName)s (%(deviceId)s)!": "شما با موفقیت %(deviceName)s (%(deviceId)s) را تأیید کردید!", + "You've successfully verified your device!": "شما با موفقیت دستگاه خود را تأیید کردید!", + "In encrypted rooms, verify all users to ensure it’s secure.": "در اتاق های رمزگذاری شده ، برای اطمینان از امنیت اتاق، همه کاربران را تأیید هویت کنید.", + "Verify all users in a room to ensure it's secure.": "برای اطمینان از امنیت اتاق، هویت همه‌ی کاربران حاضر در اتاق را تأیید کنید.", + "Almost there! Is %(displayName)s showing the same shield?": "تقریباً تمام شد! آیا %(displayName)s نیز سپر مشابهی را نشان می‌دهد؟", + "Almost there! Is your other session showing the same shield?": "تقریباً انجام شد! آیا نشست دیگر شما همان سپر را نشان می دهد؟", + "Verify by emoji": "تأیید توسط شکلک", + "Verify by comparing unique emoji.": "با مقایسه شکلک تأیید کنید.", + "If you can't scan the code above, verify by comparing unique emoji.": "اگر نمی توانید کد بالا را اسکن کنید ، با مقایسه شکلک منحصر به فرد، او را تأیید کنید.", + "Ask %(displayName)s to scan your code:": "از %(displayName)s بخواهید که کد شما را اسکن کند:", + "Verify by scanning": "با اسکن تأیید کنید", + "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.": "نشستی که می خواهید تأیید کنید از اسکن کد QR یا تأیید شکلک پشتیبانی نمی کند ، یعنی همان چیزی که %(brand)s پشتیبانی می کند. با کلاینت دیگری امتحان کنید.", + "Security": "امنیت", + "Edit devices": "ویرایش دستگاه‌ها", + "This client does not support end-to-end encryption.": "این کلاینت از رمزگذاری سرتاسر پشتیبانی نمی کند.", + "Role": "نقش", + "Failed to deactivate user": "غیرفعال کردن کاربر انجام نشد", + "Deactivate user": "غیرفعال کردن کاربر", + "Deactivating this user will log them out and prevent them from logging back in. Additionally, they will leave all the rooms they are in. This action cannot be reversed. Are you sure you want to deactivate this user?": "با غیرفعال کردن این کاربر، او از سیستم خارج شده و از ورود مجدد وی جلوگیری می‌شود. علاوه بر این، او تمام اتاق هایی را که در آن هست ترک می کند. این عمل قابل برگشت نیست. آیا مطمئن هستید که می خواهید این کاربر را غیرفعال کنید؟", + "Deactivate user?": "کاربر غیرفعال شود؟", + "You will not be able to undo this change as you are promoting the user to have the same power level as yourself.": "شما نمی توانید این تغییر را باطل کنید زیرا در حال ارتقا سطح قدرت یک کاربر به سطح قدرت خود هستید.", + "Failed to change power level": "تغییر سطح قدرت انجام نشد", + "Failed to remove user from community": "کاربر از اجتماع حذف نشد", + "Failed to withdraw invitation": "دعوت پس گرفته نشد", + "Remove this user from community?": "این کاربر از اجتماع حذف شود؟", + "Disinvite this user from community?": "دعوت این کاربر به اجتماع لغو شود؟", + "Remove from community": "حذف از اجتماع", + "Unmute": "صدادار", + "Failed to mute user": "کاربر بی صدا نشد", + "Ban this user?": "کاربر تحریم شود؟", + "Unban this user?": "لغو تحریم این کاربر؟", + "Ban": "تحریم", + "Remove recent messages": "حذف پیام‌های اخیر", + "Remove %(count)s messages|one": "حذف ۱ پیام", + "Remove %(count)s messages|other": "حذف %(count)s پیام", + "For a large amount of messages, this might take some time. Please don't refresh your client in the meantime.": "برای مقدار زیادی پیام ممکن است مدتی طول بکشد. لطفا در این بین مرورگر خود را refresh نکنید.", + "You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?|one": "شما در شرف حذف ۱ پیام از کاربر %(user)s هستید. این قابل بازگشت نیست. آیا مایل هستید ادامه دهید؟", + "You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?|other": "شما در شرف حذف %(count)s پیام از کاربر %(user)s هستید. این قابل بازگشت نیست. آیا مایل هستید ادامه دهید؟", + "Remove recent messages by %(user)s": "حذف پیام‌های اخیر %(user)s", + "Try scrolling up in the timeline to see if there are any earlier ones.": "در پیام‌ها بالا بروید تا ببینید آیا موارد قدیمی وجود دارد یا خیر.", + "No recent messages by %(user)s found": "هیچ پیام جدیدی برای %(user)s یافت نشد", + "Failed to kick": "اخراج انجام نشد", + "Kick this user?": "اخراج این کاربر؟", + "Disinvite this user?": "عدم دعوت این کاربر؟", + "Kick": "اخراج", + "Demote": "تنزل رتبه", + "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.": "شما نمی توانید این تغییر را لغو کنید زیرا در حال تنزل خود هستید، اگر آخرین کاربر ممتاز در اتاق باشید بازپس گیری امتیازات غیرممکن است.", + "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the space it will be impossible to regain privileges.": "شما نمی توانید این تغییر را لغو کنید زیرا در حال تنزل خود هستید، اگر آخرین کاربر ممتاز در فضای کاری باشید، بازپس گیری امتیازات غیرممکن است.", + "Demote yourself?": "خودتان را تنزل می‌دهید؟", + "Direct message": "پیام مستقیم", + "Share Link to User": "اشتراک لینک برای کاربر", + "Invite": "دعوت", + "Mention": "اشاره", + "Jump to read receipt": "پرش به آخرین پیام خوانده شده", + "Hide sessions": "مخفی کردن نشست‌ها", + "%(count)s sessions|one": "%(count)s نشست", + "%(count)s sessions|other": "%(count)s نشست", + "Hide verified sessions": "مخفی کردن نشست‌های تأیید شده", + "%(count)s verified sessions|one": "1 نشست تأیید شده", + "%(count)s verified sessions|other": "%(count)s نشست تایید شده", + "Not trusted": "غیرقابل اعتماد", + "Trusted": "قابل اعتماد", + "Room settings": "تنظیمات اتاق", + "Share room": "به اشتراک گذاری اتاق", + "Show files": "نمایش پرونده ها", + "%(count)s people|one": "%(count)s نفر", + "%(count)s people|other": "%(count)s نفر", + "About": "درباره", + "Not encrypted": "رمزگذاری نشده", + "Add widgets, bridges & bots": "افزودن ابزارک‌ها، پل‌ها و ربات‌ها", + "Edit widgets, bridges & bots": "ویرایش ابزارک ها ، پل ها و ربات ها", + "Widgets": "ابزارک ها", + "Set my room layout for everyone": "چیدمان اتاق من را برای همه تنظیم کن", + "Options": "گزینه ها", + "Unpin a widget to view it in this panel": "برای مشاهده ویجت در این صفحه، پین آن را بردارید", + "Unpin": "برداشتن پین", + "You can only pin up to %(count)s widgets|other": "فقط می توانید تا %(count)s ابزارک را پین کنید", + "Room Info": "اطلاعات اتاق", + "One of the following may be compromised:": "ممکن است یکی از موارد زیر به در معرض خطر باشد:", + "Yours, or the other users’ session": "نشست شما و یا کاربران دیگر", + "Yours, or the other users’ internet connection": "اتصال اینترنت شما و یا کاربران دیگر", + "The homeserver the user you’re verifying is connected to": "سروی که کاربری که شما تأیید کرده‌اید به آن متصل هستند", + "Your homeserver": "سرور شما", + "Your messages are not secure": "پیام های شما ایمن نیستند", + "For extra security, verify this user by checking a one-time code on both of your devices.": "برای امنیت بیشتر، با بررسی کد یکبارمصرف در هر دو دستگاه، این کاربر را تأیید کنید.", + "Verify User": "تأیید هویت کاربر", + "In encrypted rooms, your messages are secured and only you and the recipient have the unique keys to unlock them.": "در اتاق‌های رمزگذاری شده، پیام‌های شما امن هستند و فقط شما و گیرنده کلیدهای منحصر به فرد برای باز کردن قفل آن‌ها را دارید.", + "Messages in this room are not end-to-end encrypted.": "پیام های موجود در این اتاق به صورت سرتاسر رمزگذاری نشده‌اند.", + "Your messages are secured and only you and the recipient have the unique keys to unlock them.": "پیام‌های شما امن هستند و فقط شما و گیرنده کلیدهای منحصر به فرد برای باز کردن قفل آنها را دارید.", + "Failed to perform homeserver discovery": "جستجوی سرور با موفقیت انجام نشد", + "Direct Messages": "پیام مستقیم", + "You can add existing spaces to a space.": "شما می‌توانید فضاهای کاری موجود را به یک فضای کاری اضافه کنید.", + "This homeserver doesn't offer any login flows which are supported by this client.": "این سرور هیچ سازوکار ورودی را که توسط این کلاینت پشتیبانی شود، ارائه نمی‌دهد.", + "Feeling experimental?": "آیا می‌خواهید قابلیت‌های آزمایشی را تجربه کنید؟", + "Messages in this room are end-to-end encrypted.": "پیام‌های موجود در این اتاق به صورت سرتاسر رمزگذاری شده‌اند.", + "Start Verification": "شروع تایید هویت", + "Accepting…": "پذیرش…", + "Waiting for %(displayName)s to accept…": "منتظر قبول کردن توسط %(displayName)s…", + "Accept on your other login…": "در نشست دیگر خود قبول کنید…", + "Back": "بازگشت", + "When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website.": "هنگامی که فردی یک URL را در پیام خود قرار می دهد، می توان با مشاهده پیش نمایش آن URL، اطلاعات بیشتری در مورد آن پیوند مانند عنوان ، توضیحات و یک تصویر از وب سایت دریافت کرد.", + "In encrypted rooms, like this one, URL previews are disabled by default to ensure that your homeserver (where the previews are generated) cannot gather information about links you see in this room.": "در اتاق های رمزگذاری شده، مانند این اتاق، پیش نمایش URL به طور پیش فرض غیرفعال است تا اطمینان حاصل شود که سرور شما (جایی که پیش نمایش ها ایجاد می شود) نمی تواند اطلاعات مربوط به پیوندهایی را که در این اتاق مشاهده می کنید جمع آوری کند.", + "URL previews are disabled by default for participants in this room.": "پیش نمایش URL به طور پیش فرض برای شرکت کنندگان در این اتاق غیرفعال است.", + "URL previews are enabled by default for participants in this room.": "پیش نمایش URL به طور پیش فرض برای شرکت کنندگان در این اتاق فعال است.", + "You have disabled URL previews by default.": "شما به طور پیش فرض پیش نمایش url را غیر فعال کرده اید.", + "You have enabled URL previews by default.": "شما به طور پیش فرض پیش نمایش url را فعال کرده اید.", + "Publish this room to the public in %(domain)s's room directory?": "این اتاق را در فهرست اتاق %(domain)s برای عموم منتشر شود؟", + "Room avatar": "آواتار اتاق", + "Room Topic": "موضوع اتاق", + "Room Name": "نام اتاق", + "New community ID (e.g. +foo:%(localDomain)s)": "شناسه جدید اجتماع (به عنوان مثال %(localDomain)s+foo::)", + "Show %(count)s more|other": "نمایش %(count)s مورد بیشتر", + "Show %(count)s more|one": "نمایش %(count)s مورد بیشتر", + "Jump to first invite.": "به اولین دعوت بروید.", + "Jump to first unread room.": "به اولین اتاق خوانده نشده بروید.", + "List options": "لیست گزینه‌ها", + "A-Z": "حروف الفبا", + "Activity": "فعالیت", + "Sort by": "مرتب سازی بر اساس", + "Show previews of messages": "مشاهده پیش‌نمایش پیام‌ها", + "Show rooms with unread messages first": "ابتدا اتاق های با پیام خوانده نشده را نمایش بده", + "%(errcode)s was returned while trying to access the room. If you think you're seeing this message in error, please submit a bug report.": "%(errcode)s هنگام تلاش برای دسترسی به اتاق بازگردانده شد. اگر فکر می کنید این پیام را به اشتباه مشاهده می کنید ، لطفا گزارش خطا ارسال کنید.", + "Try again later, or ask a room admin to check if you have access.": "بعداً دوباره امتحان کنید، یا از مدیر اتاق بخواهید که دسترسی شما را بررسی کند.", + "%(roomName)s is not accessible at this time.": "در حال حاضر %(roomName)s قابل دسترسی نیست.", + "This room doesn't exist. Are you sure you're at the right place?": "این اتاق وجود ندارد آیا مطمئن هستید که در جای مناسب قرار دارید؟", + "%(roomName)s does not exist.": "%(roomName)s وجود ندارد.", + "%(roomName)s can't be previewed. Do you want to join it?": "پیش بینی %(roomName)s امکان پذیر نیست. آیا می خواهید به آن بپیوندید؟", + "You're previewing %(roomName)s. Want to join it?": "شما در حال پیش نمایش %(roomName)s هستید. می خواهید به آن بپیوندید؟", + "Reject & Ignore user": "رد کردن و نادیده گرفتن کاربر", + " invited you": " شما را دعوت کرد", + "Do you want to join %(roomName)s?": "آیا می خواهید ب %(roomName)s بپیوندید؟", + "Start chatting": "گپ زدن را شروع کن", + " wants to chat": " می‌خواهد چت کند", + "Do you want to chat with %(user)s?": "آیا می خواهید با %(user)s چت کنید؟", + "Share this email in Settings to receive invites directly in %(brand)s.": "برای دریافت مستقیم دعوت در %(brand)s این ایمیل را در تنظیمات به اشتراک بگذارید.", + "Use an identity server in Settings to receive invites directly in %(brand)s.": "برای دریافت مستقیم دعوت در %(brand)s یک سرور هویت‌سنجی در تنظیمات مشخص کنید.", + "This invite to %(roomName)s was sent to %(email)s": "این دعوت به %(roomName)s به %(email)s ارسال شد", + "Link this email with your account in Settings to receive invites directly in %(brand)s.": "برای دریافت مستقیم دعوت در %(brand)s این ایمیل را به حساب خود در تنظیمات متصل کنید.", + "This invite to %(roomName)s was sent to %(email)s which is not associated with your account": "این دعوت به %(roomName)s به %(email)s ارسال شده است که با حساب شما مرتبط نیست", + "Join the discussion": "به بحث بپیوندید", + "You can still join it because this is a public room.": "هنوز می توانید به آن بپیوندید زیرا این یک اتاق عمومی است.", + "Try to join anyway": "به هر حال عضو شدن را تلاش کن", + "You can only join it with a working invite.": "فقط با یک دعوت نامه معتبر می توانید به آن بپیوندید.", + "An error (%(errcode)s) was returned while trying to validate your invite. You could try to pass this information on to a room admin.": "هنگام تلاش برای تأیید دعوت شما، خطایی (%(errcode)s) رخ داده است. می توانید این اطلاعات را به مدیر اتاق منتقل کنید.", + "Something went wrong with your invite to %(roomName)s": "در دعوت شما به %(roomName)s مشکلی پیش آمده است", + "You were banned from %(roomName)s by %(memberName)s": "شما از %(roomName)s توسط %(memberName)s محروم شدید", + "Re-join": "دوباره بپیوندید", + "Forget this room": "فراموش کردن این اتاق", + "Reason: %(reason)s": "دلیل: %(reason)s", + "You were kicked from %(roomName)s by %(memberName)s": "شما توسط %(memberName)s از %(roomName)s اخراج شدید", + "Loading room preview": "در حال بارگیری پیش نمایش اتاق", + "Sign Up": "ثبت نام", + "Join the conversation with an account": "پیوستن به گفتگو با یک حساب کاربری", + "Rejecting invite …": "رد کردن دعوت …", + "Loading …": "بارگذاری …", + "Joining room …": "در حال پیوستن به اتاق …", + "This room": "این اتاق", + "%(count)s results|one": "%(count)s نتیجه", + "%(count)s results|other": "%(count)s نتیجه", + "%(count)s results in all spaces|one": "%(count)s نتیجه در تمامی فضا‌های کاری", + "%(count)s results in all spaces|other": "%(count)s نتیجه در تمامی فضا‌های کاری", + "Use the + to make a new room or explore existing ones below": "از + برای ایجاد یک اتاق جدید استفاده کنید و یا در اتاق های موجود زیر کاوش کنید", + "Quick actions": "اقدامات سریع", + "Explore all public rooms": "کاوش در تمام اتاق‌های عمومی", + "Start a new chat": "چت جدیدی را شروع کنید", + "Can't see what you’re looking for?": "نمی توانید چیزی را که به دنبال آن می‌گردید، ببینید؟", + "Empty room": "اتاق خالی", + "Custom Tag": "برچسب سفارشی", + "Suggested Rooms": "اتاق‌های پیشنهادی", + "Historical": "تاریخی", + "System Alerts": "هشدارهای سیستم", + "Low priority": "اولویت کم", + "Explore public rooms": "کاوش در اتاق‌های عمومی", + "Explore community rooms": "کاوش در اتاق‌های اجتماع", + "You do not have permissions to add rooms to this space": "شما اجازه افزودن اتاق به این فضای کاری را ندارید", + "You do not have permissions to create new rooms in this space": "شما اجازه ایجاد اتاق جدید در این فضای کاری را ندارید", + "Add room": "افزودن اتاق", + "Rooms": "اتاق‌ها", + "Start chat": "شروع چت", + "People": "افراد", + "Favourites": "موردعلاقه‌ها", + "Invites": "دعوت‌ها", + "Open dial pad": "باز کردن صفحه شماره‌گیری", + "Start a Conversation": "شروع مکالمه", + "Video call": "تماس تصویری", + "Voice call": "تماس صوتی", + "Show Widgets": "نمایش ابزارک‌ها", + "Hide Widgets": "پنهان‌کردن ابزارک‌ها", + "Join Room": "به اتاق بپیوندید", + "(~%(count)s results)|one": "(~%(count)s نتیجه)", + "(~%(count)s results)|other": "(~%(count)s نتیجه)", + "No recently visited rooms": "اخیراً از اتاقی بازدید نشده است", + "Recently visited rooms": "اتاق‌هایی که به تازگی بازدید کرده‌اید", + "Room %(name)s": "اتاق %(name)s", + "Replying": "پاسخ دادن", + "Seen by %(displayName)s (%(userName)s) at %(dateTime)s": "دیده شده توسط %(displayName)s (%(userName)s در %(dateTime)s)", + "Seen by %(userName)s at %(dateTime)s": "دیده شده توسط %(userName)s در %(dateTime)s", + "Unknown": "ناشناخته", + "Offline": "آفلاین", + "Idle": "بلااستفاده", + "Online": "آنلاین", + "Unknown for %(duration)s": "ناشناخته به مدت %(duration)s", + "Offline for %(duration)s": "آفلاین به مدت %(duration)s", + "Idle for %(duration)s": "بلااستفاده برای مدت %(duration)s", + "Online for %(duration)s": "آنلاین برای مدت %(duration)s", + "%(duration)sd": "%(duration)s روز", + "%(duration)sh": "%(duration)s ساعت", + "%(duration)sm": "%(duration)s دقیقه", + "%(duration)ss": "%(duration)s ثانیه", + "Jump to message": "رفتن به پیام", + "Unpin Message": "لغو پین پیام", + "Pinned Messages": "پیام‌های پین شده", + "Loading...": "بارگذاری...", + "No pinned messages.": "هیچ پیام پین شده‌ای وجود ندارد.", + "This is the start of .": "این شروع است.", + "Add a photo, so people can easily spot your room.": "عکس اضافه کنید تا افراد بتوانند به راحتی اتاق شما را ببینند.", + "Invite to just this room": "فقط به این اتاق دعوت کنید", + "%(displayName)s created this room.": "%(displayName)s این اتاق را ایجاد کرده است.", + "You created this room.": "شما این اتاق را ایجاد کردید.", + "Add a topic to help people know what it is about.": "یک موضوع اضافه کنید تا به افراد کمک کنید از آنچه در آن است مطلع شوند.", + "Topic: %(topic)s ": "موضوع: %(topic)s ", + "Topic: %(topic)s (edit)": "موضوع: %(topic)s (ویرایش)", + "This is the beginning of your direct message history with .": "این ابتدای تاریخچه پیام مستقیم شما با است.", + "Only the two of you are in this conversation, unless either of you invites anyone to join.": "فقط شما دو نفر در این مکالمه حضور دارید ، مگر اینکه یکی از شما کس دیگری را به عضویت دعوت کند.", + "Code block": "بلوک کد", + "Strikethrough": "خط روی متن", + "Italics": "مورب", + "Bold": "پررنگ", + "%(seconds)ss left": "%(seconds)s ثانیه باقی‌مانده", + "You do not have permission to post to this room": "شما اجازه ارسال در این اتاق را ندارید", + "This room has been replaced and is no longer active.": "این اتاق جایگزین شده‌است و دیگر فعال نیست.", + "The conversation continues here.": "گفتگو در اینجا ادامه دارد.", + "Send a message…": "ارسال یک پیام…", + "Send an encrypted message…": "ارسال پیام رمزگذاری شده …", + "Send a reply…": "ارسال پاسخ …", + "Send an encrypted reply…": "ارسال پاسخ رمزگذاری شده …", + "Upload file": "آپلود فایل", + "Emoji picker": "انتخاب کننده شکلک", + "Send message": "ارسال پیام", + "%(userName)s (power %(powerLevelNumber)s)": "%(userName)s (سطح قدرت %(powerLevelNumber)s)", + "Invited": "دعوت شد", + "Invite to this space": "به این فضای کاری دعوت کنید", + "Invite to this community": "به این انجمن دعوت کنید", + "and %(count)s others...|one": "و یکی دیگر ...", + "and %(count)s others...|other": "و %(count)s مورد دیگر ...", + "Close preview": "بستن پیش نمایش", + "Scroll to most recent messages": "به جدیدترین پیام‌ها بروید", + "Please select the destination room for this message": "لطفاً اتاق مقصد را برای این پیام انتخاب کنید", + "Failed to send": "ارسال با خطا مواجه شد", + "Your message was sent": "پیام شما ارسال شد", + "Encrypting your message...": "رمزگذاری پیام شما ...", + "Sending your message...": "در حال ارسال پیام شما ...", + "The authenticity of this encrypted message can't be guaranteed on this device.": "صحت این پیام رمزگذاری شده در این دستگاه تضمین نمی شود.", + "Encrypted by a deleted session": "با یک نشست حذف شده رمزگذاری شده است", + "Unencrypted": "رمزگذاری نشده", + "Encrypted by an unverified session": "توسط یک نشست تأیید نشده رمزگذاری شده است", + "This message cannot be decrypted": "این پیام نمی‌تواند رمزگشایی شود", + "Export room keys": "استخراج کلیدهای اتاق", + "%(nameList)s %(transitionList)s": "%(nameList)s.%(transitionList)s", + "This process allows you to export the keys for messages you have received in encrypted rooms to a local file. You will then be able to import the file into another Matrix client in the future, so that client will also be able to decrypt these messages.": "این فرآیند به شما این امکان را می‌دهد تا کلیدهایی را که برای رمزگشایی پیام‌هایتان در اتاق‌های رمزشده نیاز دارید، در قالب یک فایل محلی استخراج کنید. بعد از آن می‌توانید این فایل را در هر کلاینت دیگری وارد (Import) کرده و قادر به رمزگشایی و مشاهده‌ی پیام‌های رمزشده‌ی مذکور باشید.", + "Language Dropdown": "منو زبان", + "View message": "مشاهده پیام", + "Information": "اطلاعات", + "Download": "دانلود", + "Rotate Right": "چرخش به راست", + "Rotate Left": "چرخش به چپ", + "Zoom in": "بزرگنمایی", + "Zoom out": "کوچک نمایی", + "%(count)s people you know have already joined|one": "%(count)s نفر از افرادی که می شناسید قبلاً پیوسته‌اند", + "%(count)s people you know have already joined|other": "%(count)s نفر از افرادی که می شناسید قبلاً به آن پیوسته‌اند", + "%(count)s members including %(commaSeparatedMembers)s|one": "%(commaSeparatedMembers)s", + "%(count)s members including %(commaSeparatedMembers)s|other": "%(count)s عضو شامل %(commaSeparatedMembers)s", + "Including %(commaSeparatedMembers)s": "شامل %(commaSeparatedMembers)s", + "The exported file will allow anyone who can read it to decrypt any encrypted messages that you can see, so you should be careful to keep it secure. To help with this, you should enter a passphrase below, which will be used to encrypt the exported data. It will only be possible to import the data by using the same passphrase.": "فایل استخراج‌شده به هر کسی که به آن دسترسی داشته باشد اجازه می‌دهد تا تمام پیام‌هایی که شما می‌توانید آن‌ها را ببینید، مشاهده کند؛ پس باید مراقب باشید و آن را امن نگه دارید. به این منظور، شما باید عبارت امنیتی را در زیر وارد کنید.. این عبارت برای رمزکردن داده‌های استخراج‌شده‌ی شما استفاده می‌شود. در این صورت تنها راه واردکردن (Import) این داده‌ها و مشاهده‌ی آن‌ها استفاده از همین عبارت امنیتی خواهد بود.", + "View all %(count)s members|one": "نمایش ۱ عضو", + "View all %(count)s members|other": "نمایش همه %(count)s عضو", + "expand": "گشودن", + "collapse": "بستن", + "Please create a new issue on GitHub so that we can investigate this bug.": "لطفا در GitHub یک مسئله جدید ایجاد کنید تا بتوانیم این اشکال را بررسی کنیم.", + "No results": "بدون نتیجه", + "Join": "پیوستن", + "Windows": "پنجره‌ها", + "Enter passphrase": "عبارت امنیتی را وارد کنید", + "Screens": "صفحه نمایش‌ها", + "Share your screen": "به اشتراک‌گذاری صفحه نمایش", + "Confirm passphrase": "عبارت امنیتی را تائید کنید", + "This version of %(brand)s does not support searching encrypted messages": "این نسخه از %(brand)s از جستجوی پیام های رمزگذاری شده پشتیبانی نمی کند", + "Import room keys": "واردکردن (Import) کلیدهای اتاق", + "This version of %(brand)s does not support viewing some encrypted files": "این نسخه از %(brand)s از مشاهده برخی از پرونده های رمزگذاری شده پشتیبانی نمی کند", + "This process allows you to import encryption keys that you had previously exported from another Matrix client. You will then be able to decrypt any messages that the other client could decrypt.": "این فرآیند به شما اجازه می‌دهد تا کلیدهای امنیتی را وارد (Import) کنید، کلیدهایی که قبلا از کلاینت‌های دیگر خود استخراج (Export) کرده‌اید. پس از آن شما می‌توانید هر پیامی را که کلاینت دیگر قادر به رمزگشایی آن بوده را، رمزگشایی و مشاهده کنید.", + "Use the Desktop app to search encrypted messages": "برای جستجوی میان پیام‌های رمز شده از نسخه دسکتاپ استفاده کنید", + "Use the Desktop app to see all encrypted files": "برای مشاهده همه پرونده های رمز شده از نسخه دسکتاپ استفاده کنید", + "Widget added by": "ابزارک اضافه شده توسط", + "The export file will be protected with a passphrase. You should enter the passphrase here, to decrypt the file.": "فایل استخراج‌شده با یک عبارت امنیتی محافظت می‌شود. برای رمزگشایی فایل باید عبارت امنیتی را وارد کنید.", + "Popout widget": "بیرون انداختن ابزارک", + "This widget may use cookies.": "این ابزارک ممکن است از کوکی استفاده کند.", + "Widgets do not use message encryption.": "ابزارک ها از رمزگذاری پیام استفاده نمی کنند.", + "File to import": "فایل برای واردکردن (Import)", + "Using this widget may share data with %(widgetDomain)s.": "استفاده از این ابزارک ممکن است داده‌هایی را با %(widgetDomain)s به اشتراک بگذارد.", + "New Recovery Method": "روش بازیابی جدید", + "A new Security Phrase and key for Secure Messages have been detected.": "یک عبارت امنیتی و کلید جدید برای پیام‌رسانی امن شناسایی شد.", + "Using this widget may share data with %(widgetDomain)s & your Integration Manager.": "استفاده از این ابزارک ممکن است داده‌هایی را با %(widgetDomain)s و سیستم مدیریت ادغام به اشتراک بگذارد.", + "If you didn't set the new recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.": "اگر روش بازیابی جدیدی را تنظیم نکرده‌اید، ممکن است حمله‌کننده‌ای تلاش کند به حساب کاربری شما دسترسی پیدا کند. لطفا گذرواژه حساب کاربری خود را تغییر داده و فورا یک روش جدیدِ بازیابی در بخش تنظیمات انتخاب کنید.", + "Widget ID": "شناسه ابزارک", + "Room ID": "شناسه اتاق", + "%(brand)s URL": "آدرس %(brand)s", + "Your theme": "پوسته شما", + "Your user ID": "شناسه کاربری شما", + "Your avatar URL": "URL آواتار شما", + "Your display name": "نام نمایشی شما", + "Any of the following data may be shared:": "هر یک از داده های زیر ممکن است به اشتراک گذاشته شود:", + "This session is encrypting history using the new recovery method.": "این نشست تاریخچه‌ی پیام‌های رمزشده را با استفاده از روش جدیدِ بازیابی، رمز می‌کند.", + "Unknown Address": "آدرس ناشناخته", + "Cancel search": "لغو جستجو", + "Quick Reactions": "واکنش سریع", + "Categories": "دسته بندی ها", + "Flags": "پرچم ها", + "Symbols": "نمادها", + "Objects": "اشیاء", + "Go to Settings": "برو به تنظیمات", + "Travel & Places": "سفر و اماکن", + "Activities": "فعالیت ها", + "Set up Secure Messages": "پیام‌رسانی امن را تنظیم کنید", + "Food & Drink": "غذا و نوشیدنی", + "Recovery Method Removed": "روش بازیابی حذف شد", + "Animals & Nature": "حیوانات و طبیعت", + "Smileys & People": "لبخند و افراد", + "This session has detected that your Security Phrase and key for Secure Messages have been removed.": "نشست فعلی تشخیص داده که عبارت امنیتی و کلید لازم شما برای پیام‌رسانی امن حذف شده‌است.", + "Frequently Used": "متداول", + "You're not currently a member of any communities.": "شما در حال حاضر عضو هیچ اجتماعی نیستید.", + "Display your community flair in rooms configured to show it.": "عنوان شما در اجتماع را در اتاق‌هایی که برای نمایش آن پیکربندی شده‌اند، نمایش بده.", + "If you did this accidentally, you can setup Secure Messages on this session which will re-encrypt this session's message history with a new recovery method.": "اگر این کار را به صورت تصادفی انجام دادید، می‌توانید سازوکار پیام امن را برای این نشست تنظیم کرده که باعث می‌شود تمام تاریخچه‌ی این نشست با استفاده از یک روش جدیدِ بازیابی، مجددا رمزشود.", + "Something went wrong when trying to get your communities.": "هنگام تلاش برای دریافت اجتماع‌های شما مشکلی پیش آمد.", + "If you didn't remove the recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.": "اگر متد بازیابی را حذف نکرده‌اید، ممکن است حمله‌کننده‌ای سعی در دسترسی به حساب‌کاربری شما داشته باشد. گذرواژه حساب کاربری خود را تغییر داده و فورا یک روش بازیابی را از بخش تنظیمات خود تنظیم کنید.", + "Filter community rooms": "فیلتر اتاق های اجتماع", + "Add rooms to this community": "افزودن اتاق به این اجتماع", + "Only visible to community members": "قابل مشاهده فقط برای اعضای اجتماع", + "Visible to everyone": "قابل مشاهده برای همه", + "Visibility in Room List": "قابل مشاهده بودن در لیست اتاق", + "The visibility of '%(roomName)s' in %(groupId)s could not be updated.": "قابلیت مشاهده %(roomName)s در %(groupId)s بروز نشد.", + "Message downloading sleep time(ms)": "زمان خواب بارگیری پیام (ms)", + "Something went wrong!": "مشکلی پیش آمد!", + "Failed to remove '%(roomName)s' from %(groupId)s": "حذف %(roomName)s از %(groupId)s انجام نشد", + "Toggle this dialog": "کلیک کنید", + "Failed to remove room from community": "حذف اتاق از اجتماع انجام نشد", + "Removing a room from the community will also remove it from the community page.": "حذف یک اتاق از اجتماع، آن را از صفحه اجتماع نیز حذف می کند.", + "Move autocomplete selection up/down": "انتخاب تکمیل‌کننده خودکار را بالا/پایین ببرید", + "Are you sure you want to remove '%(roomName)s' from %(groupId)s?": "آیا مطمئن هستید که می خواهید %(roomName)s را از %(groupId)s حذف کنید؟", + "%(severalUsers)sjoined %(count)s times|one": "%(severalUsers)s عضو شدند", + "example": "مثال", + "Example": "مثال", + "Skip": "بیخیال", + "Import": "واردکردن (Import)", + "Export": "استخراج (Export)", + "Space": "فضای کاری", + "Esc": "خروج", + "Super": "فوق العاده", + "Theme added!": "پوسته اضافه شد!", + "Find a room…": "یافتن اتاق…", + "If you've joined lots of rooms, this might take a while": "اگر عضو اتاق‌های بسیار زیادی هستید، ممکن است این فرآیند مقدای به طول بیانجامد", + "Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.": "مدیر سرور شما قابلیت رمزنگاری سرتاسر برای اتاق‌ها و گفتگوهای خصوصی را به صورت پیش‌فرض غیرفعال کرده‌است.", + "To link to this room, please add an address.": "برای لینک دادن به این اتاق، لطفا یک نشانی برای آن اضافه کنید.", + "Filter community members": "فیلتر اعضای اجتماع", + "Failed to load group members": "اعضای گروه بارگیری نشد", + "Can't load this message": "بارگیری این پیام امکان پذیر نیست", + "Submit logs": "ارسال لاگ‌ها", + "edited": "ویرایش شده", + "Edited at %(date)s. Click to view edits.": "ویرایش شده در %(date)s. برای مشاهده ویرایش ها کلیک کنید.", + "Click to view edits": "برای مشاهده ویرایش ها کلیک کنید", + "Edited at %(date)s": "ویرایش شده در %(date)s", + "You are about to be taken to a third-party site so you can authenticate your account for use with %(integrationsUrl)s. Do you wish to continue?": "شما در آستانه هدایت شدن به یک سایت ثالث هستید بنابراین می توانید حساب خود را برای استفاده با %(integrationsUrl)s احراز هویت کنید. آیا مایل هستید ادامه دهید؟", + "Add an Integration": "یکپارچه سازی اضافه کنید", + "This room is a continuation of another conversation.": "این اتاق ادامه گفتگوی دیگر است.", + "Click here to see older messages.": "برای دیدن پیام های قدیمی اینجا کلیک کنید", + "%(senderDisplayName)s changed the room avatar to ": "%(senderDisplayName)s آواتار اتاق را به تغییر داد", + "%(senderDisplayName)s removed the room avatar.": "%(senderDisplayName)s آواتار اتاق را حذف کرد.", + "%(senderDisplayName)s changed the avatar for %(roomName)s": "%(senderDisplayName)s آواتار خود را در %(roomName)s تغییر داد", + "Message deleted on %(date)s": "پیام در %(date)s حذف شد", + "Message deleted by %(name)s": "پیام توسط %(name)s حذف شد", + "Message deleted": "پیغام پاک شد", + "reacted with %(shortName)s": " واکنش نشان داد با %(shortName)s", + " reacted with %(content)s": " واکنش نشان داد با %(content)s", + "Reactions": "واکنش ها", + "Show all": "نمایش همه", + "Add reaction": "افزودن واکنش", + "Error processing voice message": "خطا در پردازش پیام صوتی", + "Error decrypting video": "خطا در رمزگشایی ویدیو", + "You sent a verification request": "شما یک درخواست تأیید هویت ارسال کرده‌اید", + "%(name)s wants to verify": "%(name)s می‌خواهد تأیید هویت کند", + "Declining …": "در حال رد کردن …", + "Accepting …": "در حال پذیرش …", + "%(name)s cancelled": "%(name)s لغو کرد", + "%(name)s declined": "%(name)s رد کرد", + "You cancelled": "شما لغو کردید", + "You declined": "شما رد کردید", + "%(name)s accepted": "%(name)s پذیرفت", + "You accepted": "پذیرفتید", + "Re-request encryption keys from your other sessions.": "درخواست مجدد کلید‌های رمزنگاری از نشست‌های دیگر شما.", + "Mod": "معاون", + "Hint: Begin your message with // to start it with a slash.": "نکته: پیام خود را با // شروع کنید تا با یک اسلش شروع شود.", + "You can use /help to list available commands. Did you mean to send this as a message?": "برای لیست کردن دستورات موجود می توانید از /help استفاده کنید. آیا قصد داشتید این پیام را به عنوان متم ارسال کنید؟", + "Read Marker off-screen lifetime (ms)": "خواندن نشانگر طول عمر خارج از صفحه نمایش (میلی ثانیه)", + "Notifications on the following keywords follow rules which can’t be displayed here:": "اعلان‌های مخصوص کلمات کلیدی زیر از قوانینی پیروی می کنند که نمی توانند در اینجا نمایش داده شوند:", + "sends space invaders": "ارسال مهاجمان فضایی", + "Sends the given message with a space themed effect": "پیام داده شده را به صورت مضمون فضای کاری ارسال می کند", + "Send anonymous usage data which helps us improve %(brand)s. This will use a cookie.": "ارسال داده‌ها به صورت ناشناس به ما در بهبود %(brand)s کمک می‌کند. برای این مورد از کوکی استفاده می‌شود.", + "If disabled, messages from encrypted rooms won't appear in search results.": "اگر غیر فعال شود، پیام‌های اتاق‌های رمزشده در نتایج جستجوها نمایش داده نمی‌شوند.", + "Disable": "غیرفعال‌کردن", + "Currently indexing: %(currentRoom)s": "هم‌اکنون ایندکس می‌شوند: %(currentRoom)s", + "%(brand)s is securely caching encrypted messages locally for them to appear in search results:": "%(brand)s پیام‌های رمزشده را به صورت امن و محلی ذخیره کرده تا در نتایج جستجو نمایش دهد:", + "Short keyboard patterns are easy to guess": "الگوهای کوتاه صفحه کلید به راحتی قابل حدس هستند", + "Straight rows of keys are easy to guess": "ردیف کلیدهای مستقیم به راحتی قابل حدس هستند", + "Common names and surnames are easy to guess": "نام و نام خانوادگی‌های متداول به راحتی قابل حدس زدن هستند", + "Names and surnames by themselves are easy to guess": "به راحتی می توان نام و نام خانوادگی را حدس زد", + "Predictable substitutions like '@' instead of 'a' don't help very much": "جایگزین‌های قابل پیش بینی مانند '@' به جای 'a' کمک زیادی نمی کند", + "Send %(eventType)s events as you in your active room": "رویدادهای %(eventType)s هنگامی که در اتاق فعال خود هستید ارسال شود", + "Unrecognised command: %(commandText)s": "دستور نامفهوم: %(commandText)s", + "Unknown Command": "دستور ناشناس", + "Server unavailable, overloaded, or something else went wrong.": "سرور در دسترس نیست، یا حجم بار روی آن زیاد شده و یا خطای دیگری رخ داده است.", + "Server error": "خطای سرور", + "Everyone in this room is verified": "همه‌ی اعضای این اتاق تائید شده‌اند", + "This room is end-to-end encrypted": "این اتاق به صورت سرتاسر رمزشده است", + "Someone is using an unknown session": "فردی از یک نشست ناشناس استفاده می‌کند", + "You have verified this user. This user has verified all of their sessions.": "شما این کاربر را تائید کرده‌اید. این کاربر تمام نشست‌های خود را تائيد کرده‌است.", + "You have not verified this user.": "شما این کاربر را تائید نکرده‌اید.", + "This user has not verified all of their sessions.": "این کاربر هیچ‌کدام از نشست‌های خود را تائید نکرده است.", + "Phone Number": "شماره تلفن", + "A text message has been sent to +%(msisdn)s. Please enter the verification code it contains.": "یک پیام متنی به +%(msisdn)s ارسال شد. لطفا کد تائید موجود در آن را وارد کنید.", + "Remove %(phone)s?": "%(phone)s را پاک می‌کنید؟", + "Email Address": "آدرس ایمیل", + "We've sent you an email to verify your address. Please follow the instructions there and then click the button below.": "جهت تائيد آدرس ایمیل، ما یک ایمیل برای شما ارسال کردیم. لطفا فرآیند موجود در ایمیل را پی گرفته و سپس بر روی دکمه‌ی زیر کلیک نمائید.", + "Unable to add email address": "امکان اضافه‌کردن آدرس ایمیل وجود ندارد", + "This doesn't appear to be a valid email address": "به نظر می‌رسد این یک آدرس ایمیل معتبر نیست", + "Invalid Email Address": "آدرس ایمیل نامعتبر", + "Remove %(email)s?": "%(email)s را پاک می‌کنید؟", + "Unable to remove contact information": "حذف اطلاعات تماس امکان‌پذیر نیست", + "Discovery options will appear once you have added a phone number above.": "امکانات کاوش و جستجو بلافاصله بعد از اضافه‌کردن شماره تلفن در بالا ظاهر خواهند شد.", + "Verification code": "کد تائید", + "Please enter verification code sent via text.": "لطفا کد تائیدی را که از طریق متن ارسال شده‌است، وارد کنید.", + "Unable to verify phone number.": "امکان تائید شماره تلفن وجود ندارد.", + "Unable to share phone number": "امکان به اشتراک‌گذاری شماره تلفن وجود ندارد", + "Unable to revoke sharing for phone number": "لغو اشتراک‌گذاری شماره تلفن امکان‌پذیر نیست", + "Discovery options will appear once you have added an email above.": "امکانات کاوش و جستجو بلافاصله بعد از اضافه‌کردن یک ایمیل در بالا ظاهر خواهند شد.", + "Once enabled, encryption for a room cannot be disabled. Messages sent in an encrypted room cannot be seen by the server, only by the participants of the room. Enabling encryption may prevent many bots and bridges from working correctly. Learn more about encryption.": "پس از فعال‌کردن رمزنگاری برای یک اتاق، امکان غیرفعال‌کردن آن وجود ندارد. پیام‌هایی که در اتاق‌های رمزشده ارسال می‌شوند، توسط سرور دیده نشده و فقط اعضای اتاق امکان مشاهده‌ی آن‌ها را دارند. فعال‌کردن رمزنگاری برای یک اتاق می‌تواند باعث از کار افتادن بسیاری از بات‌ها و پل‌های ارتباطی (bridges) شود. در مورد رمزنگاری بیشتری بدانید.", + "Send %(eventType)s events": "ارسال رخدادهای %(eventType)s", + "An error occurred changing the room's power level requirements. Ensure you have sufficient permissions and try again.": "در تغییر الزامات سطح دسترسی اتاق خطایی رخ داد. از داشتن دسترسی‌های کافی اطمینان حاصل کرده و مجددا امتحان کنید.", + "Error changing power level requirement": "خطا در تغییر الزامات سطح دسترسی", + "Banned by %(displayName)s": "توسط %(displayName)s تحریم شد", + "Change server ACLs": "لیست‌های کنترل دسترسی (ACL) سرور را تغییر دهید", + "This room is bridging messages to the following platforms. Learn more.": "این اتاق، ارتباط بین پیام‌ها و پلتفورم‌های زیر را ایجاد می‌کند. بیشتر بدانید.", + "This room isn’t bridging messages to any platforms. Learn more.": "این اتاق پیام‌های شما را با هیچ پلتفورمی ارتباط نمی‌دهد. بیشتر بدانید.", + "View older messages in %(roomName)s.": "پیام‌های قدیمی اتاق %(roomName)s را مشاهده کنید.", + "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.": "هشدار: به‌روزرسانی یک اتاق، اعضای آن را به صورت خودکار به نسخه‌ی جدید همان اتاق اضافه نمی‌کند. ما یک لینک به نسخه‌ی جدید اتاق را در نسخه‌ی قدیمی اتاق قرار می‌دهیم - اعضای اتاق برای اضافه‌شدن به نسخه‌ی جدید اتاق باید بر روی آن لینک کلیک کنند.", + "You may need to manually permit %(brand)s to access your microphone/webcam": "ممکن است لازم باشد دسترسی %(brand)s به میکروفون/دوربین را به صورت دستی فعال کنید", + "Manage the names of and sign out of your sessions below or verify them in your User Profile.": "نام نشست‌های زیر و خارج‌شدن از آن‌ها را مدیریت کرده و یا آن‌ها از در پروفایل کاربری خود تائید کنید.", + "%(brand)s collects anonymous analytics to allow us to improve the application.": "%(brand)s داده‌های تجزیه و تحلیلی را به صورت غیرمشهود و جهت بهبود برنامه جمع‌آوری می‌کند.", + "Accept all %(invitedRooms)s invites": "همه‌ی دعوت‌های %(invitedRooms)s را قبول کن", + "Reject all %(invitedRooms)s invites": "همه‌ی دعوت‌های %(invitedRooms)s را رد کن", + "Bulk options": "گزینه‌های دسته‌جمعی", + "Read Marker lifetime (ms)": "مدت‌زمان نشانه‌ی خوانده‌شده (ms)", + "Composer": "سازنده", + "Show tray icon and minimize window to it on close": "آیکون جعبه را نشان داده و هنگام بسته‌شدن، پنجره را در قالب آن کوچک کن", + "Always show the window menu bar": "همیشه نوار فهرست پنجره را نشان بده", + "Warn before quitting": "قبل از خروج هشدا بده", + "Start automatically after system login": "پس از ورود به سیستم به صورت خودکار آغاز کن", + "Subscribe": "اضافه‌شدن", + "Room ID or address of ban list": "شناسه‌ی اتاق یا آدرس لیست تحریم", + "If this isn't what you want, please use a different tool to ignore users.": "اگر این چیزی نیست که شما می‌خواهید، از یک ابزار دیگر برای نادیده‌گرفتن کاربران استفاده نمائيد.", + "Subscribing to a ban list will cause you to join it!": "ثبت‌نام کردن در یک لیست تحریم باعث می‌شود شما هم عضو آن شوید!", + "eg: @bot:* or example.org": "برای مثال: @bot:* یا example.org", + "Your personal ban list holds all the users/servers you personally don't want to see messages from. After ignoring your first user/server, a new room will show up in your room list named 'My Ban List' - stay in this room to keep the ban list in effect.": "لیست تحریم شخصی شما همه‌ی کاربران و سرورهایی را که شما تمایلی به دیدن پیام‌های آن‌ها ندارید را در خود جای می‌دهد. بعد از نادیده‌گرفتن اولین کاربر یا سرور، یک اتاق جدید در لیست اتاق‌های شما با نام 'لیست تحریم من' نمایش داده می‌شود - برای اینکه لیست تحریم‌ها کار کند، اتاق را ترک نکنید.", + "Ignoring people is done through ban lists which contain rules for who to ban. Subscribing to a ban list means the users/servers blocked by that list will be hidden from you.": "نادیده‌گرفتن افراد توسط لیست تحریم صورت می‌گیرد که حاوی قوانینی برای تشخیص این است که چه کسی را تحریم کند. اضافه‌شدن به لیست تحریم به این معناست که کاربر/سرور بلاک شده و از دید شما پنهان خواهد بود.", + "You can reset your password, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "شما می‌توانید گذرواژه‌ی خود را تغییر دهید، اما برخی از قابلیت ها تا زمان بازگشت سرور هویت‌سنجی در دسترس نخواهند بود. اگر مدام این هشدار را می‌بینید، پیکربندی خود را بررسی کرده یا با مدیر سرور تماس بگیرید.", + "Add users and servers you want to ignore here. Use asterisks to have %(brand)s match any characters. For example, @bot:* would ignore all users that have the name 'bot' on any server.": "کاربران و سرورهایی که قصد نادیده گرفتن آن‌ها را دارید در این‌جا اضافه کنید. در %(brand)s از ستاره (*) برای مچ‌شدن با هر کاراکتری استفاده کنید. برای مثال، @bot:* همه‌ی کاربران یا سرورهایی را که نام 'bot' در آن‌ها وجود دارد، نادیده می‌گیرد.", + "⚠ These settings are meant for advanced users.": "⚠ این تنظیمات برای کاربران حرفه‌ای قرار داده شده‌است.", + "You are currently subscribed to:": "شما هم‌اکنون مشترک شده‌اید در:", + "You are currently ignoring:": "شما در حال حاضر این موارد را نادیده گرفته‌اید:", + "Ban list rules - %(roomName)s": "قوانین لیست تحریم - %(roomName)s", + "Feeling experimental? Labs are the best way to get things early, test out new features and help shape them before they actually launch. Learn more.": "تمایل به آزمایش‌کردن دارید؟ آزمایشگاه بهترین مکان برای دریافت چیزهای جدید، تست قابلیت‌های نو و کمک به رفع مشکلات آن‌ها قبل از انتشار نهایی است. بیشتر بدانید.", + "%(brand)s version:": "نسخه‌ی %(brand)s:", + "To report a Matrix-related security issue, please read the Matrix.org Security Disclosure Policy.": "برای گزارش مشکلات امنیتی مربوط به ماتریکس، لطفا سایت Matrix.org بخش Security Disclosure Policy را مطالعه فرمائید.", + "If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "اگر از طریق گیتهاب مشکلی را ثبت کرده‌اید، لاگ این مشکلات می‌تواند به ما در جهت کشف و حل آن‌ها کمک کند. لاگ مشکلات حاوی داده‌های مورد استفاده برنامه نظیر نام کاربری، شناسه یا نام مستعار اتاق‌ها یا فضاهای کاری که به آن‌ها سر زده‌اید و یا نام کاربری سایر کاربران می‌شود. این داده‌ها حاوی پیام‌های شما نمی‌شوند.", + "Chat with %(brand)s Bot": "گفتگو با بات %(brand)s", + "For help with using %(brand)s, click here or start a chat with our bot using the button below.": "برای گرفتن کمک در استفاده از %(brand)s، اینجا کلید کرده یا با استفاده از دکمه‌ی زیر اقدام به شروع گفتگو با بات ما نمائید.", + "For help with using %(brand)s, click here.": "برای گرفتن کمک در استفاده از %(brand)s، اینجا کلیک کنید.", + "Agree to the identity server (%(serverName)s) Terms of Service to allow yourself to be discoverable by email address or phone number.": "با شرایط و ضوایط سرویس سرور هویت‌سنجی (%(serverName)s) موافقت کرده تا بتوانید از طریق آدرس ایمیل و شماره تلفن قابل یافته‌شدن باشید.", + "Spell check dictionaries": "دیکشنری برای چک کردن املاء", + "Appearance Settings only affect this %(brand)s session.": "تنظیمات ظاهری برنامه تنها همین نشست %(brand)s را تحت تاثیر قرار می‌دهد.", + "Set the name of a font installed on your system & %(brand)s will attempt to use it.": "نام فونتی که بر روی سیستم‌تان نصب است را وارد کرده و %(brand)s سعی می‌کند از آن استفاده کند.", + "Use between %(min)s pt and %(max)s pt": "از عددی بین %(min)s pt و %(max)s pt استفاده کنید", + "Custom font size can only be between %(min)s pt and %(max)s pt": "اندازه فونت دلخواه تنها می‌تواند عددی بین %(min)s pt و %(max)s pt باشد", + "New version available. Update now.": "نسخه‌ی جدید موجود است. هم‌اکنون به‌روزرسانی کنید.", + "Use an Integration Manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "از یک مدیر پکپارچه‌سازی (%(serverName)s) برای مدیریت بات‌ها، ویجت‌ها و پک‌های استیکر استفاده کنید.", + "Using an identity server is optional. If you choose not to use an identity server, you won't be discoverable by other users and you won't be able to invite others by email or phone.": "استفاده از سرور هویت‌سنجی اختیاری است. اگر تصمیم بگیرید از سرور هویت‌سنجی استفاده نکنید، شما با استفاده از آدرس ایمیل و شماره تلفن قابل یافته‌شدن و دعوت‌شدن توسط سایر کاربران نخواهید بود.", + "Disconnecting from your identity server will mean you won't be discoverable by other users and you won't be able to invite others by email or phone.": "قطع ارتباط با سرور هویت‌سنجی به این معناست که شما از طریق ادرس ایمیل و شماره تلفن، بیش از این قابل یافته‌شدن و دعوت‌شدن توسط کاربران دیگر نیستید.", + "You are not currently using an identity server. To discover and be discoverable by existing contacts you know, add one below.": "در حال حاضر از سرور هویت‌سنجی استفاده نمی‌کنید. برای یافتن و یافته‌شدن توسط مخاطبان موجود که شما آن‌ها را می‌شناسید، یک مورد در پایین اضافه کنید.", + "Identity Server": "سرور هویت‌سنجی", + "If you don't want to use to discover and be discoverable by existing contacts you know, enter another identity server below.": "اگر تمایل به استفاده از برای یافتن و یافته‌شدن توسط مخاطبان خود را ندارید، سرور هویت‌سنجی دیگری را در پایین وارد کنید.", + "You are currently using to discover and be discoverable by existing contacts you know. You can change your identity server below.": "در حال حاضر شما از برای یافتن و یافته‌شدن توسط مخاطبانی که می‌شناسید، استفاده می‌کنید. می‌توانید سرور هویت‌سنجی خود را در زیر تغییر دهید.", + "Identity Server (%(server)s)": "سرور هویت‌سنجی (%(server)s)", + "We recommend that you remove your email addresses and phone numbers from the identity server before disconnecting.": "توصیه می‌کنیم آدرس‌های ایمیل و شماره تلفن‌های خود را پیش از قطع ارتباط با سرور هویت‌سنجی از روی آن پاک کنید.", + "You are still sharing your personal data on the identity server .": "شما هم‌چنان داده‌های شخصی خودتان را بر روی سرور هویت‌سنجی به اشتراک می‌گذارید.", + "Disconnect anyway": "در هر صورت قطع کن", + "wait and try again later": "صبر کرده و بعدا دوباره امتحان کنید", + "contact the administrators of identity server ": "با مدیران سرور هویت‌سنجی تماس بگیرید", + "check your browser plugins for anything that might block the identity server (such as Privacy Badger)": "پلاگین‌های مرورگر خود را بررسی کنید تا مبادا سرور هویت‌سنجی را بلاک کرده باشند (پلاگینی مانند Privacy Badger)", + "You should:": "شما باید:", + "You should remove your personal data from identity server before disconnecting. Unfortunately, identity server is currently offline or cannot be reached.": "شما باید قبل از قطع اتصال، داده‌های شخصی خود را از سرور هویت‌سنجی پاک کنید. متاسفانه سرور هویت‌سنجی هم‌اکنون آفلاین بوده و یا دسترسی به آن امکان‌پذیر نیست.", + "Disconnect": "قطع شو", + "Disconnect from the identity server ?": "از سرور هویت‌سنجی قطع می‌شوید؟", + "Disconnect identity server": "اتصال با سرور هویت‌سنجی را قطع کن", + "The identity server you have chosen does not have any terms of service.": "سرور هویت‌سنجی که انتخاب کرده‌اید شرایط و ضوابط سرویس ندارد.", + "Terms of service not accepted or the identity server is invalid.": "شرایط و ضوابط سرویس پذیرفته نشده و یا سرور هویت‌سنجی معتبر نیست.", + "Disconnect from the identity server and connect to instead?": "ارتباط با سرور هویت‌سنجی قطع شده و در عوض به متصل شوید؟", + "Change identity server": "تغییر سرور هویت‌سنجی", + "Checking server": "در حال بررسی سرور", + "Could not connect to Identity Server": "اتصال به سرور هیوت‌سنجی امکان پذیر نیست", + "Not a valid Identity Server (status code %(code)s)": "سرور هویت‌سنجی معتبر نیست (کد وضعیت %(code)s)", + "Identity Server URL must be HTTPS": "پروتکل آدرس سرور هویت‌سنجی باید HTTPS باشد", + "not ready": "آماده نیست", + "ready": "آماده", + "Secret storage:": "حافظه نهان:", + "in account data": "در داده‌های حساب کاربری", + "Secret storage public key:": "کلید عمومی حافظه نهان:", + "Backup key cached:": "کلید پشتیبان ذخیره شد:", + "not stored": "ذخیره نشد", + "Backup key stored:": "کلید پشتیبان ذخیره شد:", + "Back up your encryption keys with your account data in case you lose access to your sessions. Your keys will be secured with a unique Security Key.": "در صورت از دست رفتن دسترسی به نشست‌هایتان، از کلیدهای رمزنگاری و داده‌های حساب کاربری خود نسخه‌ی پشتیبان تهیه نمائید. کلیدهای شما توسط کلید منحضر به فرد امنیتی (Security Key) امن خواهند ماند.", + "unexpected type": "تایپ (نوع) غیرمنتظره", + "well formed": "خوش‌ساخت", + "Back up your keys before signing out to avoid losing them.": "پیش از خروج از حساب کاربری، از کلید‌های خود پشتیبان بگیرید تا آن‌ها را از دست ندهید.", + "Your keys are not being backed up from this session.": "کلید‌های شما از این نشست پشتیبان‌گیری نمی‌شود.", + "Algorithm:": "الگوریتم:", + "Backup version:": "نسخه‌ی پشتیبان:", + "This backup is trusted because it has been restored on this session": "این نسخه‌ی پشتیبان قابل اعتماد است چرا که بر روی این نشست بازیابی شد", + "Backup is not signed by any of your sessions": "نسخه پشتیبان توسط هیچ کدام از نشست‌های شما امضاء نشده‌است", + "Backup has an invalid signature from unverified session ": "نسخه پشتیبان دارای امضاء نامعتبر از نشست تائیدنشده‌ی می‌باشد", + "Backup has an invalid signature from verified session ": "نسخه‌ی پشتبان دارای امضاء نامعتبر از نشست تائیدشده‌ی می‌باشد", + "Backup has a valid signature from unverified session ": "نسخه پشتیبان دارای امضاء معتبر از نشست تائيد نشده‌ی می‌باشد", + "Backup has a valid signature from verified session ": "نسخه پشتیبان دارای امضاء معتبر از نشست تائیدشده‌ی می‌باشد", + "Backup has an invalid signature from this session": "نسخه پشتیبان دارای امضاء نامعتبر از طرف این نشست است", + "Backup has a valid signature from this session": "نسخه پشتیبان دارای امضاء معتبر از طرف این نشست است", + "Backup has a signature from unknown session with ID %(deviceId)s": "نسخه پشتیبان دارای امضاء از نشست ناشناس با شناسه‌ی %(deviceId)s است", + "Backup has a signature from unknown user with ID %(deviceId)s": "نسخه پشتیبان دارای امضاء از کاربر ناشناس با شناسه‌ی %(deviceId)s است", + "Backup has a invalid signature from this user": "نسخه پشتیبان دارای امضاء نامعتبر از این کاربر است", + "Backup has a valid signature from this user": "نسخه پشتیبان دارای امضاء معتبر از این کاربر است", + "All keys backed up": "از همه کلیدها نسخه‌ی پشتیبان گرفته شد", + "Backing up %(sessionsRemaining)s keys...": "در حال پیشیبان‌گیری کلید‌های %(sessionsRemaining)s ...", + "Connect this session to Key Backup": "این نشست را به کلید پشتیبان‌گیر متصل کن", + "Connect this session to key backup before signing out to avoid losing any keys that may only be on this session.": "پیش از خروج از حساب کاربری، این نشست را به کلید پشتیبان‌گیر متصل نمائید. با این کار مانع از گم‌شدن کلیدهای که فقط بر روی این نشست وجود دارند می‌شوید.", + "This session is not backing up your keys, but you do have an existing backup you can restore from and add to going forward.": "این نشست از کلیدهای شما پشتیبان‌گیری نمی‌کند، با این حال شما یک نسخه‌ی پشتیبان موجود دارید که می‌توانید آن را بازیابی کنید.", + "This session is backing up your keys. ": "این نشست در حال پشتیبان‌گیری از کلیدهای شماست. ", + "Restore from Backup": "بازیابی از نسخه‌ی پشتیبان", + "Unable to load key backup status": "امکان بارگیری و نمایش وضعیت کلید پشتیبان وجود ندارد", + "Are you sure? You will lose your encrypted messages if your keys are not backed up properly.": "آیا اطمینان دارید؟ در صورتی که از کلیدهای شما به درستی پشتیبان‌گیری نشده باشد، تمام پیام‌های رمزشده‌ی خود را از دست خواهید داد.", + "Delete Backup": "پاک‌کردن نسخه پشتیبان (Backup)", + "Save": "ذخیره", + "Profile picture": "تصویر پروفایل", + "Display Name": "نام نمایشی", + "Profile": "پروفایل", + "Upgrade to your own domain": "به دامنه‌ی خودتان به روز‌رسانی کنید", + "The operation could not be completed": "امکان تکمیل عملیات وجود ندارد", + "Failed to save your profile": "ذخیره‌ی تنظیمات شما موفقیت‌آمیز نبود", + "Enable audible notifications for this session": "فعال‌سازی اعلان‌های صدادار برای این نشست", + "Show message in desktop notification": "پیام‌ها را در اعلان دسکتاپ نشان بده", + "Enable desktop notifications for this session": "فعال‌سازی اعلان‌های دسکتاپ برای این نشست", + "You might have configured them in a client other than %(brand)s. You cannot tune them in %(brand)s but they still apply.": "ممکن است شما آن‌ها را بر روی کلاینت دیگری به غیر از %(brand)s پیکربندی کرده باشید. شما نمی‌توانید آن موارد را بر روی %(brand)s تغییر دهید.", + "There are advanced notifications which are not shown here.": "اعلان‌های پیشرفته‌ای وجود دارد که در این‌جا نمایش داده نمی‌شود.", + "Add an email address to configure email notifications": "برای راه‌اندازی اعلان‌های ایمیلی یک آدرس ایمیل اضافه کنید", + "Clear notifications": "پاک‌کردن اعلان‌ها", + "The integration manager is offline or it cannot reach your homeserver.": "مدیر یکپارچه‌سازی‌ یا آفلاین است و یا نمی‌تواند به سرور شما متصل شود.", + "Cannot connect to integration manager": "امکان اتصال به مدیر یکپارچه‌سازی‌ها وجود ندارد", + "Connecting to integration manager...": "در حال اتصال به مدیر پکپارچه‌سازی...", + "Message search initialisation failed": "آغاز فرآیند جستجوی پیام‌ها با شکست همراه بود", + "%(brand)s can't securely cache encrypted messages locally while running in a web browser. Use %(brand)s Desktop for encrypted messages to appear in search results.": "%(brand)s نمی‌تواند پیام‌های رمزشده را به شکل امن و به صورت محلی در هنگامی که مرورگر در حال فعالیت است ذخیره کند. از %(brand)s نسخه‌ی دسکتاپ برای نمایش پیام‌های رمزشده در نتایج جستجو استفاده نمائید.", + "%(brand)s is missing some components required for securely caching encrypted messages locally. If you'd like to experiment with this feature, build a custom %(brand)s Desktop with search components added.": "%(brand)s بعضی از مولفه‌های مورد نیاز برای ذخیره امن پیام‌های رمزشده به صورت محلی را ندارد. اگر تمایل به استفاده از این قابلیت دارید، یک نسخه‌ی دلخواه از %(brand)s با مولفه‌های مورد نظر بسازید.", + "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|other": "پیام‌های رمزشده را به صورتی محلی و امن ذخیره کرده تا در نتایج جستجو ظاهر شوند، با استفاده از %(size)s برای ذخیره‌ی پیام‌ها از اتاق‌های %(rooms)s.", + "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|one": "پیام‌های رمزشده را به صورتی محلی و امن ذخیره کرده تا در نتایج جستجو ظاهر شوند، با استفاده از %(size)s برای ذخیره‌ی پیام‌ها از اتاق %(rooms)s.", + "Securely cache encrypted messages locally for them to appear in search results.": "پیام‌های رمزشده را به صورتی محلی و امن ذخیره کرده تا در نتایج جستجو ظاهر شوند.", + "Manage": "مدیریت", + "Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.": "به صورت جداگانه هر نشستی که با بقیه‌ی کاربران دارید را تائید کنید تا به عنوان نشست قابل اعتماد نشانه‌گذاری شود، با این کار می‌توانید به دستگاه‌های امضاء متقابل اعتماد نکنید.", + "Encryption": "رمزنگاری", + "Last seen": "آخرین بازدید", + "You'll need to authenticate with the server to confirm the upgrade.": "برای تائید ارتقاء، نیاز به احراز هویت نزد سرور خواهید داشت.", + "%(severalUsers)shad their invitations withdrawn %(count)s times|other": "%(severalUsers)s دعوت خود را %(count)s مرتبه پس‌گرفتند", + "Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.": "برای اینکه بتوانید بقیه‌ی نشست‌ها را تائید کرده و به آن‌ها امکان مشاهده‌ی پیام‌های رمزشده را بدهید، ابتدا باید این نشست را ارتقاء دهید. بعد از تائیدشدن، به عنوان نشست‌ّای تائید‌شده به سایر کاربران نمایش داده خواهند شد.", + "%(oneUser)srejected their invitation %(count)s times|one": "%(oneUser)s دعوت خود را رد کرد", + "%(oneUser)srejected their invitation %(count)s times|other": "%(oneUser)s دعوت خود را %(count)s مرتبه رد کرد", + "%(severalUsers)srejected their invitations %(count)s times|one": "%(severalUsers)s دعوت‌های خود را رد کردند", + "%(severalUsers)srejected their invitations %(count)s times|other": "%(severalUsers)s دعوت خود را %(count)s مرتبه رد کردند", + "Enter a security phrase only you know, as it’s used to safeguard your data. To be secure, you shouldn’t re-use your account password.": "یک عبارت امنیتی که فقط خودتان می‌دانید را وارد کنید؛ این عبارت از داده‌های شما محافظت می‌کند. برای حفظ امنیت، نباید از گذرواژه‌ی خود در اینجا استفاده کنید.", + "Store your Security Key somewhere safe, like a password manager or a safe, as it’s used to safeguard your encrypted data.": "کلید امنیتی خود را در جایی امن ذخیره کنید، چرا که از آن به عنوان محافظ پیام‌های رمز‌شده‌ی شما استفاده می‌شود.", + "%(oneUser)sleft and rejoined %(count)s times|one": "%(oneUser)s خارج شد و مجددا عضو شد", + "Unable to query secret storage status": "امکان جستجو و کنکاش وضعیت حافظه‌ی مخفی میسر نیست", + "%(oneUser)sleft and rejoined %(count)s times|other": "%(oneUser)s %(count)s مرتبه خارج شد و مجددا عضو شد", + "%(severalUsers)sleft and rejoined %(count)s times|one": "%(severalUsers)s خارج شدند و مجددا عضو شدند", + "If you cancel now, you may lose encrypted messages & data if you lose access to your logins.": "اگر الان لغو کنید، ممکن است پیام‌ها و داده‌های رمزشده‌ی خود را در صورت خارج‌شدن از حساب‌های کاربریتان، از دست دهید.", + "%(severalUsers)sleft and rejoined %(count)s times|other": "%(severalUsers)s %(count)s مرتبه خارج شدند و مجددا عضو شدند", + "%(oneUser)sjoined and left %(count)s times|one": "%(oneUser)s پیوست و خارج شد", + "%(oneUser)sjoined and left %(count)s times|other": "%(oneUser)s %(count)s مرتبه عضو شده و خارج شدند", + "%(severalUsers)sjoined and left %(count)s times|one": "%(severalUsers)s عضو شدند و خارج شدند", + "%(severalUsers)sjoined and left %(count)s times|other": "%(severalUsers)s %(count)s مرتبه عضو شده و خارج شدند", + "%(oneUser)sleft %(count)s times|one": "%(oneUser)s خارج شد", + "%(oneUser)sleft %(count)s times|other": "%(oneUser)s %(count)s مرتبه خارج شده‌است", + "You can also set up Secure Backup & manage your keys in Settings.": "همچنین می‌توانید پشتیبان‌گیری امن را برپا کرده و کلید‌های خود را در تنظیمات مدیریت کنید.", + "%(severalUsers)sleft %(count)s times|one": "%(severalUsers)s خارج شدند", + "%(severalUsers)sleft %(count)s times|other": "%(severalUsers)s %(count)s مرتبه خارج شدند", + "Upgrade your encryption": "رمزنگاری خود را ارتقا دهید", + "Set a Security Phrase": "یک عبارت امنیتی تنظیم کنید", + "Confirm Security Phrase": "عبارت امنیتی را تأیید کنید", + "%(oneUser)sjoined %(count)s times|one": "%(oneUser)s پیوست", + "%(oneUser)sjoined %(count)s times|other": "%(oneUser)s %(count)s مرتبه عضو شدند", + "Save your Security Key": "کلید امنیتی خود را ذخیره کنید", + "Unable to set up secret storage": "تنظیم حافظه‌ی پنهان امکان پذیر نیست", + "Passphrases must match": "عبارات‌های امنیتی باید مطابقت داشته باشند", + "Passphrase must not be empty": "عبارت امنیتی نمی‌تواند خالی باشد", + "%(severalUsers)sjoined %(count)s times|other": "%(severalUsers)s%(count)s مرتبه عضو شده‌اند", + "Unknown error": "خطای ناشناخته", + "This room is not showing flair for any communities": "این اتاق برای هیچ اجتماعی استعداد نشان نمی دهد", + "Showing flair for these communities:": "نمایش استعداد برای این اجتماع‌ها:", + "'%(groupId)s' is not a valid community ID": "«%(groupId)s» یک شناسه معتبر اجتماع نیست", + "Invalid community ID": "شناسه انجمن نامعتبر است", + "There was an error updating the flair for this room. The server may not allow it or a temporary error occurred.": "در به روزرسانی وضعیت این اتاق خطایی روی داد. سرور ممکن است اجازه نداده باشد و یا خطایی موقت روی داده باشد.", + "Error updating flair": "خطا در به روزرسانی", + "Show more": "نمایش بیشتر", + "Set addresses for this room so users can find this room through your homeserver (%(localDomain)s)": "آدرس‌های این اتاق را تنظیم کنید تا کاربران بتوانند این اتاق را از طریق سرور شما پیدا کنند (%(localDomain)s)", + "Local Addresses": "آدرس‌های محلی", + "New published address (e.g. #alias:server)": "آدرس جدید منتشر شده (به عنوان مثال #alias:server)", + "No other published addresses yet, add one below": "آدرس دیگری منتشر نشده است، در زیر اضافه کنید", + "Other published addresses:": "دیگر آدرس‌های منتشر شده:", + "Published addresses can be used by anyone on any server to join your room. To publish an address, it needs to be set as a local address first.": "آدرس‌های منتشرشده را می توان در هر سرور برای پیوستن به اتاق شما استفاده کرد. برای انتشار آدرس، ابتدا باید آن را به عنوان آدرس محلی تنظیم کنید.", + "Published Addresses": "آدرس‌های منتشر شده", + "Local address": "آدرس محلی", + "This room has no local addresses": "این اتاق آدرس محلی ندارد", + "not specified": "مشخص نشده", + "Main address": "آدرس اصلی", + "Error removing address": "خطا در حذف آدرس", + "There was an error removing that address. It may no longer exist or a temporary error occurred.": "هنگام حذف آدرس خطایی روی داد. ممکن است دیگر وجود نداشته باشد یا خطایی موقت روی داده باشد.", + "You don't have permission to delete the address.": "شما اجازه حذف آدرس را ندارید.", + "There was an error creating that address. It may not be allowed by the server or a temporary failure occurred.": "هنگام ایجاد آدرس خطایی روی داد. ممکن است سرور مجاز نباشد و یا اینکه خطایی موقت رخ داده باشد.", + "Error creating address": "خطا در ایجاد آدرس", + "There was an error updating the room's alternative addresses. It may not be allowed by the server or a temporary failure occurred.": "در به روزرسانی آدرس های جایگزین اتاق خطایی روی داد. ممکن است سرور مجاز نباشد و یا اینکه خطایی موقت رخ داده باشد.", + "There was an error updating the room's main address. It may not be allowed by the server or a temporary failure occurred.": "در به روزرسانی آدرس اصلی اتاق خطایی روی داد. ممکن است سرور مجاز نباشد و یا خطای موقتی رخ داده باشد.", + "Error updating main address": "خطا در به روزرسانی آدرس اصلی", + "Delete recording": "حذف پیام ضبط شده", + "Stop the recording": "توقف ضبط", + "Record a voice message": "ضبط پیام صوتی", + "We didn't find a microphone on your device. Please check your settings and try again.": "ما میکروفونی در دستگاه شما پیدا نکردیم. لطفاً تنظیمات خود را بررسی کنید و دوباره امتحان کنید.", + "No microphone found": "میکروفونی یافت نشد", + "We were unable to access your microphone. Please check your browser settings and try again.": "ما نتوانستیم به میکروفون شما دسترسی پیدا کنیم. لطفا تنظیمات مرورگر خود را بررسی کنید و دوباره سعی کنید.", + "Unable to access your microphone": "دسترسی به میکروفن شما امکان پذیر نیست", + "Mark all as read": "همه را به عنوان خوانده شده علامت بزن", + "Jump to first unread message.": "رفتن به اولین پیام خوانده نشده.", + "Invited by %(sender)s": "دعوت شده توسط %(sender)s", + "Revoke invite": "لغو دعوت", + "Admin Tools": "ابزارهای مدیریت", + "Could not revoke the invite. The server may be experiencing a temporary problem or you do not have sufficient permissions to revoke the invite.": "دعوت لغو نشد. ممکن است سرور با یک مشکل موقتی روبرو شده باشد و یا اینکه شما مجوز کافی برای لغو دعوت را نداشته باشید.", + "Failed to revoke invite": "دعوت لغو نشد", + "Show Stickers": "نمایش استیکرها", + "Hide Stickers": "پنهان کردن استیکر‌ها", + "Stickerpack": "استیکر", + "Add some now": "اکنون چندتایی اضافه کنید", + "You don't currently have any stickerpacks enabled": "شما در حال حاضر هیچ بسته برچسب فعالی ندارید", + "Failed to connect to integration manager": "اتصال به سیستم مدیریت ادغام انجام نشد", + "Only room administrators will see this warning": "فقط مدیران اتاق این هشدار را مشاهده خواهند کرد", + "This room is running room version , which this homeserver has marked as unstable.": "این اتاق از نسخه اتاق استفاده می کند، که این سرور آن را به عنوان ناپایدار علامت گذاری کرده است.", + "This room has already been upgraded.": "این اتاق قبلاً ارتقا یافته است.", + "Upgrading this room will shut down the current instance of the room and create an upgraded room with the same name.": "با ارتقا این اتاق نسخه فعلی اتاق خاموش شده و یک اتاق ارتقا یافته به همین نام ایجاد می شود.", + "Unread messages.": "پیام های خوانده نشده.", + "%(count)s unread messages.|one": "1 پیام خوانده نشده", + "%(count)s unread messages.|other": "%(count)s پیام خوانده نشده.", + "%(count)s unread messages including mentions.|one": "1 اشاره خوانده نشده", + "%(count)s unread messages including mentions.|other": "%(count)s پیام‌های خوانده نشده از جمله اشاره‌ها.", + "Room options": "تنظیمات اتاق", + "Leave Room": "ترک اتاق", + "Invite People": "افراد را دعوت کنید", + "Favourited": "مورد علاقه", + "Forget Room": "اتاق را فراموش کن", + "Notification options": "تنظیمات اعلان", + "Mentions & Keywords": "اشاره‌ها و کلمات کلیدی", + "Use default": "استفاده از پیش‌فرض", + "Show less": "نمایش کمتر", + "Public Name": "نام عمومی", + "ID": "شناسه", + "Delete %(count)s sessions|one": "حذف %(count)s نشست", + "Delete %(count)s sessions|other": "حذف %(count)s نشست", + "Delete sessions|one": "حذف نشست", + "Delete sessions|other": "حذف نشست‌ها", + "Click the button below to confirm deleting these sessions.|one": "برای تائید حذف این نشست بر روی دکمه‌ی زیر کلیک کنید.", + "Click the button below to confirm deleting these sessions.|other": "برای تائید حذف این نشست‌ها بر روی دکمه‌ی زیر کلیک کنید.", + "Confirm deleting these sessions": "حذف این نشست‌ها را تائید کنید", + "Confirm deleting these sessions by using Single Sign On to prove your identity.|one": "حذف‌کردن این نشست را با استفاده از احراز هویت یکپارچه که هویت شما را ثابت می‌کند، تائید نمائید.", + "Confirm deleting these sessions by using Single Sign On to prove your identity.|other": "حذف‌کردن این نشست‌ها را با استفاده از احراز هویت یکپارچه که هویت شما را ثابت می‌کند، تائید نمائید.", + "Unable to load session list": "امکان بارگیری و نمایش لیست نشست‌ها ممکن نیست", + "Your homeserver does not support session management.": "سرور شما قابلیت مدیریت نشست‌ها را پشتیبانی نمی‌کند.", + "exists": "وجود دارد", + "Homeserver feature support:": "قابلیت‌های پشتیبانی‌شده سمت سرور:", + "User signing private key:": "کلید امضاء خصوصی کاربر:", + "Self signing private key:": "کلید خصوصی self-sign:", + "not found locally": "به صورت محلی یافت نشد", + "cached locally": "به صورت محلی کش شده‌است", + "Master private key:": "شاه‌کلید خصوصی:", + "not found in storage": "در حافظه یافت نشد", + "in secret storage": "بر روی حافظه نهان", + "Cross-signing private keys:": "کلیدهای خصوصی امضاء متقابل:", + "not found": "یافت نشد", + "in memory": "بر روی حافظه", + "Cross-signing public keys:": "کلیدهای عمومی امضاء متقابل:", + "Go back": "بازگشت", + "You can change this later": "می‌توانید بعدا این را تغییر دهید", + "Invite only, best for yourself or teams": "فقط با دعوتنامه، مناسب برای خودتان یا تیم‌ها یا جمع‌های خصوصی", + "Private": "خصوصی", + "Open space for anyone, best for communities": "محیط باز برای همه، مناسب برای جمع عمومی", + "Public": "عمومی", + "Spaces are a new way to group rooms and people. To join an existing space you'll need an invite.": "محیط‌ها یک روش جدید برای دسته‌بندی کاربران و اتاق‌ها هستند. برای پیوستن به یک محیط موجود، نیاز به یک دعوتنامه دارید.", + "Create a space": "ساختن یک محیط", + "Please enter a name for the space": "لطفا یک نام برای محیط وارد کنید", + "Description": "توضیحات", + "Name": "نام", + "Upload": "بارگذاری", + "Delete": "پاک‌کردن", + "Accept to continue:": "برای ادامه را بپذیرید:", + "Decline (%(counter)s)": "رد کردن (%(counter)s)", + "Your server isn't responding to some requests.": "سرور شما به بعضی درخواست‌ها پاسخ نمی‌دهد.", + "Pin": "سنجاق", + "Folder": "پوشه", + "Headphones": "هدفون", + "Anchor": "لنگر", + "Bell": "زنگ", + "Trumpet": "شیپور", + "Guitar": "گیتار", + "Ball": "توپ", + "Trophy": "کاپ", + "Rocket": "موشک", + "Aeroplane": "هواپیما", + "Bicycle": "دوچرخه", + "Train": "قطار", + "Flag": "پرچم", + "Telephone": "تلفن", + "Hammer": "چکش", + "Key": "کلید", + "Lock": "قفل", + "Scissors": "قیچی", + "Paperclip": "گیره کاغذ", + "Pencil": "مداد", + "Book": "کتاب", + "Light bulb": "لامپ روشن", + "Gift": "هدیه", + "Clock": "ساعت", + "Hourglass": "ساعت‌شنی", + "Umbrella": "چتر", + "Thumbs up": "موافق", + "Santa": "بابانوئل", + "Spanner": "آچار", + "Glasses": "عینک", + "Hat": "کلاه", + "Robot": "ربات", + "Smiley": "لبخند", + "Heart": "قلب", + "Cake": "کیک", + "Pizza": "پیتزا", + "Corn": "ذرت", + "Strawberry": "توت‌فرنگی", + "Apple": "سیب", + "Banana": "موز", + "Fire": "آتش", + "Cloud": "ابر", + "Moon": "ماه", + "Globe": "کره", + "Mushroom": "قارچ", + "Cactus": "کاکتوس", + "Tree": "درخت", + "Flower": "گل", + "Butterfly": "پروانه", + "Octopus": "اختاپوس", + "Fish": "ماهی", + "Turtle": "لاک‌پشت", + "Penguin": "پنگوئن", + "Rooster": "خروس", + "Panda": "پاندا", + "Rabbit": "خرگوش", + "Elephant": "فیل", + "Pig": "خوک", + "Unicorn": "اسب تک‌شاخ", + "Horse": "اسب", + "Lion": "شیر", + "Cat": "گربه", + "Dog": "سگ", + "To be secure, do this in person or use a trusted way to communicate.": "برای حفظ امنیت، خودتان این کار را انجام دهید و یا از یک روش ارتباطی قابل اعتماد استفاده نمائید.", + "They don't match": "مطابقت ندارند", + "They match": "مطابقت دارند", + "Cancelling…": "در حال لغو…", + "Waiting for %(displayName)s to verify…": "منتظر %(displayName)s برای تائید کردن…", + "Waiting for your other session, %(deviceName)s (%(deviceId)s), to verify…": "منتظر نشست دیگر شما، %(deviceName)s (%(deviceId)s) برای تائید کردن…", + "Waiting for your other session to verify…": "منتظر نشست دیگر شما برای تائید‌کردن…", + "Unable to find a supported verification method.": "روش پشتیبانی‌شده‌ای برای تائید پیدا نشد.", + "Verify this user by confirming the following number appears on their screen.": "در صورتی که عدد بعدی بر روی صفحه‌ی کاربر نمایش داده می‌شود، او را تائید نمائید.", + "Verify this session by confirming the following number appears on its screen.": "در صورتی که شماره‌ی بعدی بر روی دستگاه نمایش داده می‌شود، این نشست را تائید نمائید.", + "Verify this user by confirming the following emoji appear on their screen.": "در صورتی که همه‌ی شکلک‌های موجود بر روی صفحه‌ی دستگاه کاربر ظاهر شده‌اند، او را تائید نمائید.", + "Confirm the emoji below are displayed on both sessions, in the same order:": "تائید کنید که شکلک‌های زیر بر روی هر دو دستگاه و به ترتیب نمایش داده می‌شوند:", + "Start": "شروع", + "Compare a unique set of emoji if you don't have a camera on either device": "اگر بر روی دستگاه خود دوربین ندارید، از تطابق شکلک‌های منحصر به فرد استفاده نمائید", + "Compare unique emoji": "شکلک‌های منحصر به فرد را مقایسه کنید", + "Scan this unique code": "این QR-code منحصر به فرد را اسکن کنید", + "or": "یا", + "Verify this session by completing one of the following:": "این نشست را با دنبال‌کردن یکی از فرآیندهای زیر تائید کنید:", + "Got It": "متوجه شدم", + "Secure messages with this user are end-to-end encrypted and not able to be read by third parties.": "پیام‌های رد و بدل شده با این کاربر به صورت سرتاسر رمزشده و هیچ نفر سومی امکان مشاهده و خواندن آن‌ها را ندارد.", + "You've successfully verified this user.": "شما با موفقیت این کاربر را تائید کردید.", + "Verified!": "تائید شد!", + "The other party cancelled the verification.": "طرف مقابل فرآیند تائید را لغو کرد.", + "Play": "اجرا کردن", + "Pause": "متوقف‌کردن", + "Accept": "پذیرفتن", + "Decline": "رد کردن", + "Incoming call": "تماس ورودی", + "Incoming video call": "تماس تصویری ورودی", + "Incoming voice call": "تماس صوتی ورودی", + "Unknown caller": "تماس‌گیرنده‌ی ناشناس", + "Dial pad": "صفحه شماره‌گیری", + "There was an error looking up the phone number": "هنگام یافتن شماره تلفن خطایی رخ داد", + "Unable to look up phone number": "امکان یافتن شماره تلفن میسر نیست", + "%(name)s on hold": "%(name)s در حال تعلیق است", + "Return to call": "بازگشت به تماس", + "Fill Screen": "صفحه را پر کن", + "Voice Call": "تماس صوتی", + "Video Call": "تماس تصویری", + "Connecting": "در حال اتصال", + "%(peerName)s held the call": "%(peerName)s تماس را به حالت تعلیق درآورد", + "You held the call Resume": "شما تماس را به حالت تعلیق نگه داشته‌اید ادامه", + "You held the call Switch": "شما تماس را به حالت تعلیق نگه داشته‌اید تعویض", + "Consulting with %(transferTarget)s. Transfer to %(transferee)s": "با %(transferTarget)s مشورت کنید. انتقال به %(transferee)s", + "unknown person": "فرد ناشناس", + "sends confetti": "انیمیشن بارش کاغذ شادی را ارسال کن", + "sends snowfall": "انیمیشن بارش برف را ارسال کن", + "Sends the given message with snowfall": "این پیام را با انیمیشن بارش برف ارسال کن", + "sends fireworks": "انیمیشن آتش‌بازی را ارسال کن", + "Sends the given message with fireworks": "این پیام را با انیمیشن آتش‌بازی ارسال کن", + "Sends the given message with confetti": "این پیام را با انیمیشن بارش کاغد شادی ارسال کن", + "This is your list of users/servers you have blocked - don't leave the room!": "این لیست کاربران/اتاق‌هایی است که شما آن‌ها را بلاک کرده‌اید - اتاق را ترک نکنید!", + "My Ban List": "لیست تحریم‌های من", + "When rooms are upgraded": "زمانی که اتاق‌ها به‌روزرسانی می‌گردند", + "Encrypted messages in group chats": "پیام‌های رمزشده در اتاق‌ها", + "Encrypted messages in one-to-one chats": "پیام‌های رمزشده در گفتگو‌های خصوصی", + "Messages containing @room": "پیام‌های حاوی شناسه‌ی اتاق", + "Messages containing my username": "پیام‌های حاوی نام کاربری من", + "Downloading logs": "در حال دریافت لاگ‌ها", + "Uploading logs": "در حال بارگذاری لاگ‌ها", + "Show chat effects (animations when receiving e.g. confetti)": "نمایش قابلیت‌های بصری (انیمیشن‌هایی مثل بارش برف یا کاغذ شادی هنگام دریافت پیام)", + "IRC display name width": "عرض نمایش نام‌های IRC", + "Manually verify all remote sessions": "به صورت دستی همه‌ی نشست‌ها را تائید نمائید", + "How fast should messages be downloaded.": "پیام‌ها باید چقدر سریع بارگیری شوند.", + "Enable message search in encrypted rooms": "فعال‌سازی قابلیت جستجو در اتاق‌های رمزشده", + "Show previews/thumbnails for images": "پیش‌نمایش تصاویر را نشان بده", + "Allow fallback call assist server turn.matrix.org when your homeserver does not offer one (your IP address would be shared during a call)": "زمانی که سرور شما پیشنهادی ندارد، از سرور کمکی turn.hivaa.im برای برقراری تماس استفاده کنید (در این صورت آدرس IP شما برای سرور turn.hivaa.im آشکار خواهد شد)", + "Low bandwidth mode": "حالت پهنای باند کم", + "Show hidden events in timeline": "نمایش رخدادهای مخفی در گفتگو‌ها", + "Show shortcuts to recently viewed rooms above the room list": "نمایش میانبر در بالای لیست اتاق‌ها برای مشاهده‌ی اتاق‌هایی که اخیرا باز کرده‌اید", + "Show rooms with unread notifications first": "اتاق‌های با پیام‌های خوانده‌نشده را ابتدا نشان بده", + "Order rooms by name": "مرتب‌کردن اتاق‌ها بر اساس نام", + "Show developer tools": "نمایش ابزار توسعه‌دهندگان", + "Prompt before sending invites to potentially invalid matrix IDs": "قبل از ارسال دعوت‌نامه برای کاربری که شناسه‌ی او احتمالا معتبر نیست، هشدا بده", + "Enable widget screenshots on supported widgets": "فعال‌سازی امکان اسکرین‌شات برای ویجت‌های پشتیبانی‌شده", + "Room Colour": "رنگ اتاق", + "Enable URL previews by default for participants in this room": "امکان پیش‌نمایش URL را به صورت پیش‌فرض برای اعضای این اتاق فعال کن", + "Enable URL previews for this room (only affects you)": "فعال‌سازی پیش‌نمایش URL برای این اتاق (تنها شما را تحت تاثیر قرار می‌دهد)", + "Enable inline URL previews by default": "فعال‌سازی پیش‌نمایش URL به صورت پیش‌فرض", + "Never send encrypted messages to unverified sessions in this room from this session": "هرگز از این نشست، پیام‌های رمزشده برای به نشست‌های تائید نشده در این اتاق ارسال مکن", + "Never send encrypted messages to unverified sessions from this session": "هرگز از این نشست، پیام‌های رمزشده را به نشست‌های تائید نشده ارسال مکن", + "Send analytics data": "ارسال داده‌های تجزیه و تحلیلی", + "Allow Peer-to-Peer for 1:1 calls (if you enable this, the other party might be able to see your IP address)": "اجازه برقراری تماس‌های یک به یک را بده (با فعال‌شدن این قابلیت، ممکن است طرف مقابل بتواند آدرس IP شما را ببیند)", + "System font name": "نام فونت سیستمی", + "Use a system font": "استفاده از یک فونت موجود بر روی سیستم شما", + "Match system theme": "با پوسته‌ی سیستم تطبیق پیدا کن", + "Enable Community Filter Panel": "پنل پالایش فضای کاری را فعال کن", + "Mirror local video feed": "تصویر خودتان را هنگام تماس تصویری برعکس (مثل آینه) نمایش بده", + "Automatically replace plain text Emoji": "متن ساده را به صورت خودکار با شکلک جایگزین کن", + "Use Ctrl + Enter to send a message": "استفاده از Ctrl + Enter برای ارسال پیام", + "Use Command + Enter to send a message": "استفاده از Command + Enter برای ارسال پیام", + "Use Ctrl + F to search": "استفاده از Ctrl + F برای جستجو", + "Use Command + F to search": "استفاده از Command + F برای جستجو", + "Show typing notifications": "نمایش اعلان «در حال نوشتن»", + "Send typing notifications": "ارسال اعلان «در حال نوشتن»", + "Enable big emoji in chat": "نمایش شکلک‌های بزرگ در گفتگوها را فعال کن", + "Space used:": "فضای مصرفی:", + "Indexed messages:": "پیام‌های ایندکس‌شده:", + "Send %(eventType)s events as you in this room": "رویدادهای %(eventType)s هنگامی که داخل این اتاق هستید ارسال شود", + "Indexed rooms:": "اتاق‌های ایندکس‌شده:", + "%(doneRooms)s out of %(totalRooms)s": "%(doneRooms)s از %(totalRooms)s", + "Navigation": "پیمایش", + "Calls": "تماس‌ها", + "Room List": "لیست اتاق‌ها", + "Remain on your screen while running": "بر روی صفحه خود باقی بمانید", + "Autocomplete": "تکمیل خودکار", + "Alt": "Alt", + "Alt Gr": "Alt Gr", + "Shift": "Shift", + "Remain on your screen when viewing another room, when running": "هنگام مشاهده اتاق دیگر، روی صفحه خود باشید", + "Ctrl": "Ctrl", + "Toggle Bold": "بولد‌کردن", + "Toggle Quote": "نقل‌قول کردن", + "Toggle Italics": "ایتالیک‌کردن", + "New line": "خط جدید", + "Navigate recent messages to edit": "پیام‌های اخیر را برای ویرایش پیمایش کنید", + "Jump to start/end of the composer": "به ابتدا/انتهای سازنده پرش کن", + "Navigate composer history": "پیمایش در تاریخچه‌ی سازنده", + "Cancel replying to a message": "پاسخ به پیام را لغو کن", + "Toggle microphone mute": "میکروفون را قطع کنید", + "Toggle video on/off": "ویدئو را وصل/قطع کنید", + "Scroll up/down in the timeline": "تایم‌لاین پیام‌ها را به سمت بالا/پایین پیمایش کنید", + "Dismiss read marker and jump to bottom": "نشانه‌ی خوانده‌شده را بیخیال شو و به انتها پرش کن", + "Jump to oldest unread message": "به قدیمی‌ترین پیام خوانده نشده پرش کن", + "Upload a file": "فایل بارگذاری کنید", + "Search (must be enabled)": "جستجو (باید فعال باشد)", + "Jump to room search": "به قسمت جستجوی اتاق پرش کن", + "Navigate up/down in the room list": "در لیست اتاق‌ها به بالا/پایین بروید", + "Select room from the room list": "از لیست اتاق‌ها انتخاب کنید", + "Collapse room list section": "قسمت لیست اتاق‌ها را جمع کن", + "Expand room list section": "قسمت لیست اتاق‌ها را بسط بده", + "Clear room list filter field": "پاک‌کردن قسمت پالایش‌های لیست اتاق‌ها", + "Previous/next unread room or DM": "اتاق یا گفتگوی خوانده‌نشده قبلی/بعدی", + "Previous/next room or DM": "اتاق یا گفتگوی قبلی/بعدی", + "Toggle the top left menu": "منوی بالا سمت چپ را تغییر دهید", + "Activate selected button": "دکمه انتخاب شده را فعال کنید", + "Toggle right panel": "پانل سمت راست را تغییر دهید", + "Go to Home View": "برو به مشاهده خانه", + "Key request sent.": "درخواست کلید ارسال شد.", + "If your other sessions do not have the key for this message you will not be able to decrypt them.": "اگر بقیه‌ی نشست‌های شما نیز کلید این پیام را نداشته باشند، امکان رمزگشایی و مشاهده‌ی آن برای شما وجود نخواهد داشت.", + "Key share requests are sent to your other sessions automatically. If you rejected or dismissed the key share request on your other sessions, click here to request the keys for this session again.": "درخواست‌های به اشتراک‌گذاری کلید برای بقیه‌ی نشست‌های شما به صورت خودکار ارسال می‌شود. اگر این درخواست‌ها را بر روی سایر نشست‌هایتان رد کرده‌اید، اینجا کلیک کنید تا درخواست به اشتراک‌گذاری کلیدها برای این نشست مجدد ارسال شود.", + "Your key share request has been sent - please check your other sessions for key share requests.": "درخواست به اشتراک‌گذاری کلید ارسال شد - لطفا بقیه‌ی نشست‌های خود را برای درخواست به اشتراک‌گذاری کلید بررسی کنید.", + "This event could not be displayed": "امکان نمایش این رخداد وجود ندارد", + "Edit message": "ویرایش پیام", + "Send as message": "ارسال به عنوان پیام", + "Show avatars in user and room mentions": "نمایش نمایه‌ها هنگام اشاره‌کردن به فرد یا اتاق", + "Jump to the bottom of the timeline when you send a message": "زمانی که پیام ارسال می‌کنید، به صورت خودکار به آخرین پیام پرش کن", + "Show line numbers in code blocks": "شماره‌ی خط‌ها را در بلاک‌های کد نمایش بده", + "Expand code blocks by default": "بلاک‌های کد را به صورت پیش‌فرض کامل نشان بده", + "Enable automatic language detection for syntax highlighting": "فعال‌سازی تشخیص خودکار زبان برای پررنگ‌سازی نحوی", + "Show timestamps in 12 hour format (e.g. 2:30pm)": "زمان را با فرمت ۱۲ ساعته نشان بده (مثلا ۲:۳۰ بعدازظهر)", + "Show read receipts sent by other users": "نشانه‌ی خوانده‌شدن پیام توسط دیگران را نشان بده", + "Show display name changes": "تغییرات نام کاربران را نشان بده", + "Show avatar changes": "تغییرات نمایه را نشان بده", + "Show join/leave messages (invites/kicks/bans unaffected)": "پیام‌های پیوستن/ترک‌کردن را نشان بده (دعوت‌ها، اخراج‌ها و تحریم‌ها بدون اثر خواهند ماند)", + "Show a placeholder for removed messages": "جای خالی پیام‌های پاک‌شده را نشان بده", + "Use a more compact ‘Modern’ layout": "از چیدمان فشرده‌تر و مدرن استفاده کن", + "Show stickers button": "نمایش دکمه‌ی استکیر", + "Enable Emoji suggestions while typing": "پیشنهاد دادن شکلک‌ها هنگام تایپ‌کردن را فعال کن", + "Use custom size": "از اندازه‌ی دلخواه استفاده کنید", + "Font size": "اندازه فونت", + "Show info about bridges in room settings": "اطلاعات پل‌های ارتباطی را در تنظیمات اتاق نمایش بده", + "Enable advanced debugging for the room list": "فعال‌سازی قابلیت رفع‌مشکل پیشرفته برای لیست اتاق‌ها", + "Offline encrypted messaging using dehydrated devices": "ارسال پیام رمزشده به شکل آفلاین با استفاده از دستگاه‌های خاص", + "Show message previews for reactions in all rooms": "پیش‌نمایش احساسات و شکلک‌ها را برای همه اتاق‌ها نشان بده", + "Show message previews for reactions in DMs": "پیش‌نمایش احساسات و شکلک‌ها را برای گفتگوهای خصوصی نشان بده", + "Support adding custom themes": "پشتیبانی از افزودن پوسته‌های ظاهری دلخواه", + "Try out new ways to ignore people (experimental)": "روش‌های جدید برای نادیده‌گرفتن افراد را امتحان کنید (آزمایشی)", + "Multiple integration managers": "چند مدیر پکپارچه‌سازی", + "Render simple counters in room header": "شمارنده‌های ساده‌ای در سرآیند اتاق نمایش بده", + "Group & filter rooms by custom tags (refresh to apply changes)": "دسته‌بندی و پالایش اتاق‌ها با استفاده از تگ‌های دلخواه (برای اعمال تغییرات صفحه را رفرش کنید)", + "Repeats like \"abcabcabc\" are only slightly harder to guess than \"abc\"": "تکرارهایی مانند \"abcabcabc\" تنها مقداری سخت‌تر از \"abc\" قابل حدس‌زدن هستند", + "Add another word or two. Uncommon words are better.": "یک یا دو کلمه دیگر اضافه کنید. کلمات غیرمعمول بهتر هستند.", + "Reversed words aren't much harder to guess": "حدس زدن کلمات معکوس خیلی سخت تر نیست", + "All-uppercase is almost as easy to guess as all-lowercase": "اگر همه‌ی موارد حروف بزرگ باشند، سختی حدس‌زدن آن‌ها با حالتی که فقط از حروف کوچک استفاده شود، تفاوتی نمی‌کند", + "Capitalization doesn't help very much": "استفاده از حروه بزرگ کمک چندانی نمی‌کند", + "Avoid dates and years that are associated with you": "از تاریخ و سالهایی که با شما در ارتباط هستند خودداری کنید", + "Avoid years that are associated with you": "از سالهایی که با شما در ارتباط هستند دوری کنید", + "Avoid recent years": "از سالهای اخیر خودداری کنید", + "Avoid sequences": "از موارد پشت سر هم اجتناب کنید", + "Avoid repeated words and characters": "از تکرار کلمات و کاراکترها خودداری نمائید", + "Use a longer keyboard pattern with more turns": "از الگوی طولانی‌ و پیچیده‌تر استفاده نمائید", + "No need for symbols, digits, or uppercase letters": "نیازی به علامت ، عدد یا حروف بزرگ نیست", + "Use a few words, avoid common phrases": "از چند کلمه استفاده کنید ، از عبارات معمول خودداری نمائید", + "Unknown server error": "خطای ناشناخته از سمت سرور", + "The user's homeserver does not support the version of the room.": "سرور کاربر از نسخه‌ی اتاق پشتیبانی نمی‌کند.", + "The user must be unbanned before they can be invited.": "برای اینکه کاربر بتواند دعوت شود، ابتدا باید رفع تحریم شود.", + "User %(user_id)s may or may not exist": "کاربر %(user_id)s ممکن است وجود داشته باشد یا نداشته باشد", + "User %(user_id)s does not exist": "کاربر %(user_id)s وجود ندارد", + "User %(userId)s is already in the room": "کاربر %(userId)s هم‌اکنون عضو این اتاق است", + "You do not have permission to invite people to this room.": "شما دسترسی دعوت افراد به این اتاق را ندارید.", + "Unrecognised address": "آدرس ناشناخته", + "Error leaving room": "خطا در ترک اتاق", + "This room is used for important messages from the Homeserver, so you cannot leave it.": "این اتاق برای نمایش پیام‌های مهم سرور استفاده می‌شود، لذا امکان ترک آن وجود ندارد.", + "Can't leave Server Notices room": "نمی توان از اتاق اعلامیه های سرور خارج شد", + "Unexpected server error trying to leave the room": "خطای غیرمنتظره روی سرور هنگام تلاش برای ترک اتاق", + "Authentication check failed: incorrect password?": "احراز هویت موفقیت‌آمیز نبود: گذرواژه نادرست است؟", + "Not a valid %(brand)s keyfile": "فایل کلید %(brand)s معتبر نیست", + "Your browser does not support the required cryptography extensions": "مرورگر شما از افزونه‌های رمزنگاری مورد نیاز پشتیبانی نمی‌کند", + "%(name)s (%(userId)s)": "%(name)s (%(userId)s)", + "%(num)s days from now": "%(num)s روز دیگر", + "about a day from now": "حدود یک روز دیگر", + "%(num)s hours from now": "%(num)s ساعت دیگر", + "about an hour from now": "حدود یک ساعت دیگر", + "%(num)s minutes from now": "%(num)s دقیقه دیگر", + "about a minute from now": "حدود یک دقیقه دیگر", + "a few seconds from now": "چند ثانیه دیگر", + "%(num)s days ago": "%(num)s روز قبل", + "about a day ago": "حدود یک روز قبل", + "%(num)s hours ago": "%(num)s ساعت قبل", + "about an hour ago": "حدود یک ساعت قبل", + "about a minute ago": "حدود یک دقیقه قبل", + "%(num)s minutes ago": "%(num)s دقیقه قبل", + "a few seconds ago": "چند ثانیه قبل", + "%(items)s and %(lastItem)s": "%(items)s و %(lastItem)s", + "%(items)s and %(count)s others|one": "%(items)s و یکی دیگر", + "%(items)s and %(count)s others|other": "%(items)s و %(count)s دیگر", + "Unable to connect to Homeserver. Retrying...": "اتصال به سرور میسر نشد. در حال تلاش دوباره...", + "Please contact your service administrator to continue using the service.": "لطفا برای ادامه‌ی استفاده از سرویس، با مدیر سرویس خود تماس بگیرید.", + "This homeserver has exceeded one of its resource limits.": "این سرور از یکی از محدودیت های منابع خود فراتر رفته است.", + "This homeserver has been blocked by its administrator.": "این سرور توسط مدیر آن مسدود شده‌است.", + "This homeserver has hit its Monthly Active User limit.": "این سرور به محدودیت بیشینه‌ی تعداد کاربران فعال ماهانه رسیده‌است.", + "Unexpected error resolving identity server configuration": "خطای غیر منتظره‌ای در حین بررسی پیکربندی سرور هویت‌سنجی رخ داد", + "Unexpected error resolving homeserver configuration": "خطای غیر منتظره‌ای در حین بررسی پیکربندی سرور رخ داد", + "No homeserver URL provided": "هیچ آدرس سروری وارد نشده‌است", + "You can log in, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "شما می توانید وارد شوید ، اما برخی از ویژگی ها تا زمانی که سرور هویت‌سنجی آنلاین نشود ، در دسترس نخواهند بود. اگر مدام این هشدار را می بینید ، پیکربندی خود را بررسی کنید یا با مدیر سرور تماس بگیرید.", + "You can register, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "شما می‌توانید حساب کاربری بسازید، اما برخی قابلیت‌ها تا زمان اتصال مجدد به سرور هویت‌سنجی در دسترس نخواهند بود. اگر شما مدام این هشدار را مشاهده می‌کنید، پیکربندی خود را بررسی کرده و یا با مدیر سرور تماس بگیرید.", + "Cannot reach identity server": "دسترسی به سرور هویت‌سنجی امکان پذیر نیست", + "Ask your %(brand)s admin to check your config for incorrect or duplicate entries.": "از مدیر %(brand)s خود بخواهید تا پیکربندی شما را از جهت ورودی‌های نادرست یا تکراری بررسی کند.", + "Kick, ban, or invite people to this room, and make you leave": "کاربران را به این اتاق دعوت کنید، آن‌ها اخراج و یا تحریم کرده، و حتی به آن‌ها اجازه دهید شما را از اتاق بیرون بیاندازند", + "Kick, ban, or invite people to your active room, and make you leave": "کاربران را به اتاق فعال خود دعوت کنید، آن‌ها اخراج و یا تحریم کرده، و حتی به آن‌ها اجازه دهید شما را از آن بیرون بیاندازند", + "End": "End", + "Enter": "Enter", + "Page Down": "Page Down", + "Page Up": "Page Up", + "Cancel autocomplete": "غیرفعال کردن تکمیل‌کننده خودکار", + "Users": "کاربران", + "Clear personal data": "پاک‌کردن داده‌های شخصی", + "You're signed out": "شما خارج شدید", + "Sign in and regain access to your account.": "وارد شوید و به حساب کاربری خود دسترسی داشته باشید.", + "Forgotten your password?": "گذرواژه‌ی خود را فراموش کردید؟", + "Enter your password to sign in and regain access to your account.": "جهت ورود مجدد به حساب کاربری و دسترسی به منوی کاربری، گذرواژه‌ی خود را وارد نمائید.", + "Regain access to your account and recover encryption keys stored in this session. Without them, you won’t be able to read all of your secure messages in any session.": "مجددا وارد حساب کاربری خود شده و کلیدهای رمزنگاری ذخیره‌شده در این نشست را بازیابی کنید. بدون آن‌ها، قادر به خواندن پیام‌های رمزشده بر روی هیچ نشست دیگری نخواهید بود.", + "Failed to re-authenticate": "احراز هویت مجدد موفیت‌آمیز نبود", + "Incorrect password": "گذرواژه صحیح نیست", + "Without verifying, you won’t have access to all your messages and may appear as untrusted to others.": "بدون تائید نشست، به همه‌ی پیام‌هایتان دسترسی نداشته و ممکن است بقیه به شما اعتماد نکنند.", + "Your new session is now verified. Other users will see it as trusted.": "نشست جدید شما تائید شد. سایر کاربران آن را به عنوان یک نشست قابل اطمینان مشاهده می‌کنند.", + "Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.": "نشست جدید شما تائید شد. اکنون به پیام‌های رمزشده‌ی شما دسترسی داشته و بقیه کاربران، آن را به عنوان یک نشست قابل اعتماد مشاهده می‌کنند.", + "Verify your identity to access encrypted messages and prove your identity to others.": "با تائید هویت خود به پیام‌های رمزشده دسترسی یافته و هویت خود را به دیگران ثابت می‌کنید.", + "Use Security Key": "استفاده از کلید امنیتی", + "Use Security Key or Phrase": "استفاده از کلید یا عبارت امنیتی", + "Decide where your account is hosted": "حساب کاربری شما بر روی کجا ساخته شود", + "Host account on": "ساختن حساب کاربری بر روی", + "Create account": "ساختن حساب کاربری", + "Registration Successful": "ثبت‌نام موفقیت‌آمیز بود", + "Already have an account? Sign in here": "حساب کاربری دارید؟ وارد شوید", + "Registration has been disabled on this homeserver.": "ثبت‌نام بر روی این سرور غیرفعال شده‌است.", + "New? Create account": "کاربر جدید هستید؟ حساب کاربری بسازید", + "Signing In...": "در حال ورود...", + "Syncing...": "در حال همگام‌سازی...", + "Set a new password": "تنظیم گذرواژه‌ی جدید", + "Return to login screen": "بازگشت به صفحه‌ی ورود", + "Your password has been reset.": "گذرواژه‌ی شما با موفقیت تغییر کرد.", + "Sign in instead": "به جای آن وارد شوید", + "Send Reset Email": "ارسال ایمیل تغییر", + "New Password": "گذرواژه جدید", + "New passwords must match each other.": "گذرواژه‌ی جدید باید مطابقت داشته باشند.", + "Please choose a strong password": "لطفا یک گذرواژه‌ی قوی انتخاب کنید", + "Session verified": "نشست تائید شد", + "Verify this login": "این ورود را تائید نمائید", + "Original event source": "منبع اصلی رخداد", + "Decrypted event source": "رمزگشایی منبع رخداد", + "Could not load user profile": "امکان نمایش پروفایل کاربر میسر نیست", + "Community and user menu": "فضای کاری و منوی کاربر", + "User menu": "منوی کاربر", + "Switch theme": "تعویض پوسته", + "Switch to dark mode": "انتخاب حالت تاریک", + "Switch to light mode": "انتخاب حالت روشن", + "User settings": "تنظیمات حساب کاربری", + "Community settings": "تنظیمات فضای کاری", + "All settings": "همه تنظیمات", + "Security & privacy": "امنیت و محرمانگی", + "Notification settings": "تنظیمات اعلان", + "Inviting...": "در حال دعوت...", + "Creating rooms...": "در حال ساختن اتاق...", + "Skip for now": "فعلا بیخیال", + "Room name": "نام اتاق", + "Support": "پشتیبانی", + "Random": "تصادفی", + "Welcome to ": "به خوش‌آمدید", + " invites you": " شما را دعوت کرد", + "Private space": "محیط خصوصی", + "Public space": "محیط عمومی", + "Spaces are a beta feature.": "محیط یک قابلیت بتا است", + "Create room": "ساختن اتاق", + "No results found": "نتیجه‌ای یافت نشد", + "Removing...": "در حال حذف...", + "You don't have permission": "شما دسترسی ندارید", + "Drop file here to upload": "برای بارگذاری فایل آن را کشیده و در این‌جا رها کنید", + "Failed to reject invite": "رد دعوتنامه با شکست همراه شد", + "Room": "اتاق", + "No more results": "نتایج بیشتری یافن نشد", + "Search failed": "جستجو موفیت‌آمیز نبود", + "You seem to be in a call, are you sure you want to quit?": "به نظر می‌رسد شما در میانه‌ی یک تماس هستید، آیا از خروج اطمینان دارید؟", + "You seem to be uploading files, are you sure you want to quit?": "به نظر می‌رسد شما در حال باگذاری فایل هستید، آیا از خروج اطمینان دارید؟", + "Connectivity to the server has been lost.": "اتصال به سرور از دست رفت.", + "Sending": "در حال ارسال", + "Retry all": "همه را دوباره امتحان کنید", + "Delete all": "حذف همه", + "Filter rooms and people": "پالایش اتاق‌ها و کاربران", + "Clear filter": "حذف پالایش", + "Filter all spaces": "پالایش همه محیط‌ها", + "Filter": "پالایش", + "Explore rooms in %(communityName)s": "جستجوی اتاق در فضای کاری %(communityName)s", + "Find a room… (e.g. %(exampleRoom)s)": "یافتن اتاق (برای مثال %(exampleRoom)s)", + "View": "مشاهده", + "Preview": "پیش‌نمایش", + "delete the address.": "آدرس را حذف کنید.", + "The homeserver may be unavailable or overloaded.": "احتمالا سرور در دسترس نباشد و یا بار زیادی روی آن قرار گرفته باشد.", + "You have no visible notifications.": "اعلان قابل مشاهده‌ای ندارید.", + "Communities are changing to Spaces": "فضای کاری به محیط تغییر پیدا کرد", + "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.": "برای دسته‌بندی اتاق‌ها و کاربران، فضای کاری بسازید!", + "Create a new community": "ساختن فضای کاری جدید", + "Your Communities": "فضاهای کاری شما", + "Logout": "خروج", + "Verification requested": "درخواست تائید", + "Old cryptography data detected": "داده‌های رمزنگاری قدیمی شناسایی شد", + "Review terms and conditions": "مرور شرایط و ضوابط", + "Terms and Conditions": "شرایط و ضوابط", + "Signed Out": "از حساب کاربری خارج شدید", + "Are you sure you want to leave the space '%(spaceName)s'?": "آیا از ترک فضای '%(spaceName)s' اطمینان دارید؟", + "This room is not public. You will not be able to rejoin without an invite.": "این اتاق عمومی نیست. پیوستن مجدد بدون دعوتنامه امکان‌پذیر نخواهد بود.", + "This space is not public. You will not be able to rejoin without an invite.": "این فضا عمومی نیست. امکان پیوستن مجدد بدون دعوتنامه امکان‌پذیر نخواهد بود.", + "You are the only person here. If you leave, no one will be able to join in the future, including you.": "شما در این‌جا تنها هستید. اگر اینجا را ترک کنید، دیگر هیچ‌کس حتی خودتان امکان پیوستن مجدد را نخواهید داشت.", + "You do not have permission to create rooms in this community.": "شما دسترسی کافی برای ساختن اتاق در این فضای کاری را ندارید.", + "Cannot create rooms in this community": "امکان ساختن اتاق در این اتاق میسر نیست", + "Failed to reject invitation": "رد دعوتنامه موفقیت‌آمیز نبود", + "Create a Group Chat": "ساختن یک گروه", + "Explore Public Rooms": "جستجوی اتاق‌های عمومی", + "Send a Direct Message": "ارسال یک پیام مستقیم", + "Liberate your communication": "ارتباطات خود را توسعه دهید", + "Welcome to %(appName)s": "به %(appName)s خوش‌آمدید", + "Now, let's help you get started": "همین الان شروع کنید", + "Welcome %(name)s": "%(name)s خوش‌آمدید", + "Add a photo so people know it's you.": "برای اینکه بقیه شما را بشناسند، یک تصویر اضافه کنید.", + "Great, that'll help people know it's you": "احسنت، با این کار شما به سایر افراد کمک می‌کنید که شما را بشناسند", + "Reset": "بازراه‌اندازی", + "Cross-signing is not set up.": "امضاء متقابل تنظیم نشده‌است.", + "Your account has a cross-signing identity in secret storage, but it is not yet trusted by this session.": "حساب کاربری شما یک هویت برای امضاء متقابل در حافظه‌ی نهان دارد، اما این هویت هنوز توسط این نشست تائید نشده‌است.", + "Cross-signing is ready for use.": "امضاء متقابل برای استفاده در دسترس است.", + "Your homeserver does not support cross-signing.": "سرور شما امضاء متقابل را پشتیبانی نمی‌کند.", + "Passwords don't match": "گذرواژه‌ها مطابقت ندارند", + "Do you want to set an email address?": "آیا تمایل به تنظیم یک ادرس ایمیل دارید؟", + "Export E2E room keys": "استخراج (Export) کلیدهای رمزنگاری اتاق‌ها", + "Changing password will currently reset any end-to-end encryption keys on all sessions, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.": "در حال حاضر تغییر گذرواژه منجر به بازنشانی کلید‌های رمزنگاری سرتاسر برای همه‌ی نشست‌ها می‌شود، در نتیجه تاریخچه‌ی پیام‌های رمزشده دیگر قابل مشاهده و خواندن نیستند. لذا حتما ابتدا کلید اتاق‌های خود را Export کرده و بعد از تغییر گذرواژه، مجددا آن‌ها را Import نمائید. این فرآیند در آینده بهبود خواهد یافت.", + "Warning!": "هشدار!", + "Passwords can't be empty": "گذرواژه‌ها نمی‌توانند خالی باشند", + "New passwords don't match": "گذرواژه‌های جدید مطابقت ندارند", + "No display name": "هیچ نامی برای نمایش وجود ندارد", + "Upload new:": "بارگذاری جدید:", + "Channel: ": "کانال:", + "Workspace: ": "فضای کار:", + "This bridge is managed by .": "این پل ارتباطی توسط مدیریت می‌شود.", + "This bridge was provisioned by .": "این پل ارتباطی توسط ارائه شده‌است.", + "Space options": "گزینه‌های انتخابی محیط", + "Manage & explore rooms": "مدیریت و جستجوی اتاق‌ها", + "Add existing room": "اضافه‌کردن اتاق موجود", + "Create": "ایجاد‌کردن", + "Create new room": "ایجاد اتاق جدید", + "Leave space": "ترک محیط", + "Settings": "تنظیمات", + "Invite with email or username": "دعوت با ایمیل یا نام‌کاربری", + "Invite people": "دعوت کاربران", + "Share invite link": "به اشتراک‌گذاری لینک دعوت", + "Failed to copy": "خطا در گرفتن رونوشت", + "Copied!": "رونوشت گرفته شد!", + "Click to copy": "برای گرفتن رونوشت کلیک کنید", + "All rooms": "همه اتاق‌ها", + "Collapse space panel": "جمع‌کردن پنل محیط", + "Expand space panel": "بسط‌دادن پنل محیط", + "Creating...": "در حال ساختن...", + "You can change these anytime.": "شما می‌توانید این را هر زمان که خواستید، تغییر دهید.", + "Add some details to help people recognise it.": "برای کمک به کاربران جهت شناخت محیط، مقداری جزئیات اضافه کنید.", + "Your private space": "محیط خصوصی شما", + "Your public space": "محیط عمومی شما", + "This homeserver does not support communities": "سرور شما از قابلیت فضای کاری پشتیبانی نمی‌کند", + "Upload avatar": "بارگذاری نمایه", + "Long Description (HTML)": "توضیح طولانی (امکان استفاده از تگ‌های HTML نیز میسر است)", + "Everyone": "همه", + "Who can join this community?": "چه کسانی بتوانند به این فضای کاری بپیوندند؟", + "You are a member of this community": "شما یک عضو این فضای کاری هستید", + "You are an administrator of this community": "شما یکی از مدیران این فضای کاری هستید", + "Leave this community": "ترک این فضای کاری", + "Join this community": "پیوستن به این فضای کاری", + "Featured Users:": "کاربران ویژه", + "Featured Rooms:": "اتاق‌های ویژه", + "These rooms are displayed to community members on the community page. Community members can join the rooms by clicking on them.": "این اتاق‌ها به اعضای فضای کاری در صفحه‌ی این فضا نمایش داده می‌شود. اعضا‌ی فضا می‌توانند با کلیک بر روی اتاق‌ها به آن‌ها بپیوندند.", + "Community Settings": "تنظیمات فضای کاری", + "Unable to leave community": "ترک فضای کاری ممکن نیست", + "Leave Community": "ترک فضای کاری", + "You are an administrator of this community. You will not be able to rejoin without an invite from another administrator.": "شما مدیر این فضای کاری هستید. امکان پیوستن مجدد شما به این فضای کاری بدون دعوت یک مدیر دیگر در این فضا امکان‌پذیر نخواهد بود.", + "Unable to join community": "پیوستن به فضای کاری ممکن نیست", + "Unable to accept invite": "تائید دعوت ممکن نیست", + "Failed to update community": "به‌روزرسانی فضای کاری با موفقیت همراه نبود", + "Failed to upload image": "بارگذاری تصویر با موفقیت همراه نبود", + "Add a User": "افزودن کاربر", + "Who would you like to add to this summary?": "تمایل دارید کدام کاربران را به این خلاصه اضافه کنید؟", + "Add users to the community summary": "افزودن کاربران به خلاصه‌ی فضای کاری", + "Add a Room": "افزودن اتاق", + "Add to summary": "افزودن به خلاصه", + "Which rooms would you like to add to this summary?": "تمایل دارید چه اتاق‌هایی را به این خلاصه اضافه کنید؟", + "Add rooms to the community summary": "افزودن اتاق به خلاصه‌ی فضای کاری", + "Create community": "ساختن فضای کاری", + "Communities": "فضاهای کاری", + "Attach files from chat or just drag and drop them anywhere in a room.": "فایل‌ها را از محیط چت ضمیمه کرده و یا آن‌ها را کشیده و در محیط اتاق رها کنید.", + "No files visible in this room": "هیچ فایلی در این اتاق قابل مشاهده نیست", + "You must join the room to see its files": "برای دیدن فایل‌های یک اتاق، باید عضو آن باشید", + "Couldn't load page": "نمایش صفحه امکان‌پذیر نبود", + "Sign in with SSO": "ورود با استفاده از احراز هویت یکپارچه", + "Add an email to be able to reset your password.": "برای داشتن امکان تغییر گذرواژه در صورت فراموش‌کردن آن، لطفا یک آدرس ایمیل وارد نمائید.", + "Register": "ایجاد حساب کاربری", + "Sign in": "ورود به حساب کاربری", + "Sign in with": "سازوکار ورود:", + "Forgot password?": "فراموشی گذرواژه", + "Phone": "شماره تلفن", + "Username": "نام کاربری", + "That phone number doesn't look quite right, please check and try again": "به نظر شماره تلفن صحیح نمی‌باشد، لطفا بررسی کرده و مجددا تلاش فرمائید", + "Enter phone number": "شماره تلفن را وارد کنید", + "Enter email address": "آدرس ایمیل را وارد کنید", + "Enter username": "نام کاربری را وارد کنید", + "Keep going...": "ادامه دهید...", + "Nice, strong password!": "احسنت، گذرواژه‌ی انتخابی قوی است!", + "Enter password": "گذرواژه را وارد کنید", + "Start authentication": "آغاز فرآیند احراز هویت", + "Something went wrong in confirming your identity. Cancel and try again.": "تائید هویت شما با مشکل مواجه شد. لطفا فرآیند را لغو کرده و مجددا اقدام نمائید.", + "Submit": "ارسال", + "Code": "کد", + "Password": "گذرواژه", + "User Status": "وضعیت کاربر", + "This room is public": "این اتاق عمومی است", + "Avatar": "نمایه", + "Join the beta": "اضافه‌شدن به نسخه‌ی بتا", + "Leave the beta": "ترک نسخه‌ی بتا", + "Beta": "بتا", + "Tap for more info": "برای اطلاعات بیشتر کلیک کنید", + "Spaces is a beta feature": "ساختن فضا یک قابلیت بتا است", + "Move right": "به سمت راست ببر", + "Move left": "به سمت چپ ببر", + "Revoke permissions": "دسترسی‌ها را لغو کنید", + "Remove for everyone": "حذف برای همه", + "Delete widget": "حذف ویجت", + "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?": "حذف یک ویجت، باعث حذف‌شدن آن برای همه‌ی کاربران این اتاق می‌شود. آیا از حذف این ویجت اطمینان دارید؟", + "Delete Widget": "حذف ویجت", + "Take a picture": "عکس بگیرید", + "Start audio stream": "آغاز جریان صدا", + "Failed to start livestream": "آغاز livestream با شکست همراه بود", + "Unable to start audio streaming.": "شروع پخش جریان صدا امکان‌پذیر نیست.", + "View Community": "مشاهده‌ی فضای کاری", + "Set a new status...": "تنظیم وضعیت جدید...", + "Set status": "تنظیم وضعیت", + "Update status": "به‌روزرسانی وضعیت", + "Clear status": "پاک‌کردن وضعیت", + "Report Content": "گزارش محتوا", + "Share Message": "به اشتراک‌گذاری پیام", + "Share Permalink": "اشتراک لینک پیام", + "Pin Message": "پین‌کردن پیام", + "Unable to reject invite": "رد کردن دعوت امکان‌پذیر نیست", + "Reject invitation": "ردکردن دعوت", + "Hold": "نگه‌داشتن", + "Resume": "ادامه", + "Appearance": "شکل و ظاهر", + "Share": "اشتراک‌گذاری", + "Revoke": "برگرداندن", + "Complete": "تکمیل", + "Verify the link in your inbox": "لینک موجود در صندوق دریافت خود را تائید کنید", + "Unable to verify email address.": "تائید آدرس ایمیل ممکن نیست", + "Click the link in the email you received to verify and then click continue again.": "برای تائید ادرس ایمیل، بر روی لینکی که برای شما ایمیل شده‌است کلیک کرده و مجددا بر روی ادامه کلیک کنید", + "Your email address hasn't been verified yet": "آدرس ایمیل شما هنوز تائید نشده‌است", + "Unable to share email address": "به اشتراک‌گذاری آدرس ایمیل ممکن نیست", + "Unable to revoke sharing for email address": "لغو اشتراک گذاری برای آدرس ایمیل ممکن نیست", + "Who can access this room?": "چه افرادی بتوانند به این اتاق دسترسی داشته باشند؟", + "Encrypted": "رمزشده", + "Once enabled, encryption cannot be disabled.": "زمانی که رمزنگاری فعال شود، امکان غیرفعال‌کردن آن برای اتاق وجود ندارد.", + "Security & Privacy": "امنیت و محرمانگی", + "Who can read history?": "چه افرادی بتوانند تاریخچه اتاق را مشاهده کنند؟", + "Members only (since they joined)": "فقط اعصاء (از زمانی که به اتاق پیوسته‌اند)", + "Members only (since they were invited)": "فقط اعضاء (از زمانی که دعوت شده‌اند)", + "Members only (since the point in time of selecting this option)": "فقط اعضاء (از زمانی که این تنظیم اعمال می‌شود)", + "Changes to who can read history will only apply to future messages in this room. The visibility of existing history will be unchanged.": "تغییر تنظیمات اینکه چه کاربرانی سابقه‌ی پیام‌ها را مشاهده کنند، تنها برای پیام‌های آتی اتاق اعمال میشود. پیام‌های قبلی متناسب با تنظیمات گذشته نمایش داده می‌شوند.", + "Only people who have been invited": "تنها کاربرانی که دعوت شده‌اند", + "Guests cannot join this room even if explicitly invited.": "کاربران مهمان حتی در صورتی که صراحتا به گروه دعوت شوند، امکان پیوستن به اتاق را نخواهند داشت.", + "Enable encryption?": "رمزنگاری را فعال می‌کنید؟", + "Select the roles required to change various parts of the room": "برای تغییر هر یک از بخش‌های اتاق، خداقل نقش مورد نیاز را انتخاب کنید", + "Permissions": "دسترسی‌ها", + "Roles & Permissions": "نقش‌ها و دسترسی‌ها", + "Muted Users": "کاربران بی‌صدا", + "Privileged Users": "کاربران ممتاز", + "No users have specific privileges in this room": "هیچ کاربری در این اتاق دسترسی خاصی ندارد", + "Notify everyone": "اعلان عمومی به همه", + "Remove messages sent by others": "پاک‌کردن پیام‌های دیگران", + "Ban users": "تحریم کاربران", + "Kick users": "اخراج کاربران", + "Change settings": "تغییر تنظیمات", + "Invite users": "دعوت کاربران", + "Send messages": "ارسال پیام‌ها", + "Default role": "نقش پیش‌فرض", + "An error occurred changing the user's power level. Ensure you have sufficient permissions and try again.": "هنگام تغییر طرح دسترسی کاربر خطایی رخ داد. از داشتن سطح دسترسی کافی برای این کار اطمینان حاصل کرده و مجددا اقدام نمائید.", + "Error changing power level": "تغییر سطح دسترسی با خطا همراه بود", + "Unban": "رفع تحریم", + "Modify widgets": "تغییر ویجت‌ها", + "Enable room encryption": "فعال‌کردن رمزنگاری برای اتاق", + "Upgrade the room": "ارتقاء نسخه اتاق", + "Change topic": "تغییر عنوان", + "Change permissions": "تغییر دسترسی‌ها", + "Change history visibility": "تغییر مشاهده‌پذیری تاریخچه", + "Change main address for the room": "تغییر آدرس اصلی اتاق", + "Change room name": "تغییر نام اتاق", + "Change room avatar": "تغییر نمایه اتاق", + "Browse": "جستجو", + "Set a new custom sound": "تنظیم صدای دلخواه جدید", + "Notification sound": "صدای اعلان", + "Sounds": "صداها", + "Uploaded sound": "صدای بارگذاری‌شده", + "Room Addresses": "آدرس‌های اتاق", + "URL Previews": "پیش‌نمایش URL", + "Bridges": "پل‌ها", + "Open Devtools": "بازکردن ابزار توسعه", + "Developer options": "گزینه‌های توسعه‌دهنده", + "Room version:": "نسخه‌ی اتاق:", + "Room version": "نسخه‌ی اتاق", + "Internal room ID:": "شناسه‌ی داخلی اتاق:", + "Room information": "اطلاعات اتاق", + "this room": "این اتاق", + "Upgrade this room to the recommended room version": "نسخه‌ی این اتاق را به نسخه‌ی توصیه‌شده ارتقاء دهید", + "This room is not accessible by remote Matrix servers": "این اتاق توسط سرورهای ماتریکس در دسترس نیست", + "Voice & Video": "صدا و تصویر", + "Audio Output": "خروجی صدا", + "No Audio Outputs detected": "هیچ خروجی صدایی یافت نشد", + "Request media permissions": "درخواست دسترسی به رسانه", + "Missing media permissions, click the button below to request.": "دسترسی به رسانه از دست رفت، برای درخواست مجدد بر روی دکمه‌ی زیر کلیک نمائید.", + "A session's public name is visible to people you communicate with": "نام عمومی یک نشست برای افرادی که با آن‌ها ارتباط برقرار کرده‌اید، قابل مشاهده است", + "Where you’re logged in": "کجا وارد حساب کاربری خود شده‌اید", + "Learn more about how we use analytics.": "در مورد نحوه‌ی استفاده‌ی ما از داده‌ها بیشتر بدانید", + "Privacy is important to us, so we don't collect any personal or identifiable data for our analytics.": "به دلیل اهمیت حریم خصوصی، ما هیچ‌گونه‌ داده‌ی شخصی‌ و قابل ردگیری را از شما جمع‌آوری نمی‌کنیم.", + "Privacy": "حریم خصوصی", + "Cross-signing": "امضاء متقابل", + "Message search": "جستجوی پیام‌ها", + "Secure Backup": "پشتیبان‌گیری امن", + "You have no ignored users.": "شما هیچ کاربری را نادیده نگرفته‌اید.", + "Session key:": "کلید نشست", + "Session ID:": "شناسه‌ی نشست", + "Import E2E room keys": "واردکردن کلیدهای رمزنگاری اتاق‌ها", + "": "<پشتیبانی نمی‌شود>", + "Unignore": "لغو نادیده‌گرفتن", + "Autocomplete delay (ms)": "تاخیر تکمیل خودکار به میلی ثانیه", + "Timeline": "سیر زمان گفتگو‌ها", + "Room list": "لیست اتاق‌ها", + "Preferences": "ترجیحات", + "Subscribed lists": "لیست‌هایی که در آن‌ها ثبت‌نام کرده‌اید", + "Ignore": "نادیده‌گرفتن", + "Server or user ID to ignore": "شناسه‌ی سرور یا کاربر مورد نظر برای نادیده‌گرفتن", + "Personal ban list": "لیست تحریم شخصی", + "Ignored users": "کاربران نادیده‌گرفته‌شده", + "View rules": "مشاهده قوانین", + "Unsubscribe": "لغو اشتراک", + "You are not subscribed to any lists": "شما در هیچ لیستی ثبت‌نام نکرده‌اید", + "You have not ignored anyone.": "شما هیچ‌کس را نادیده نگرفته‌اید.", + "User rules": "قوانین کاربر", + "Server rules": "قوانین سرور", + "None": "هیچ‌کدام", + "Please try again or view your console for hints.": "لطفا مجددا اقدام کرده و برای کسب اطلاعات بیشتر کنسول مرورگر خود را مشاهده نمائید.", + "Error unsubscribing from list": "لغو اشتراک از لیست با خطا همراه بود", + "Error removing ignored user/server": "حذف کاربر/سرور نادیده‌گرفته‌شده با خطا همراه بود", + "Please verify the room ID or address and try again.": "لطفا شناسه یا آدرس اتاق را تائید کرده و مجددا اقدام نمائید.", + "Error subscribing to list": "ثبت‌نام در لیست با خطا همراه بود", + "Something went wrong. Please try again or view your console for hints.": "مشکلی پیش آمد. لطفا مجددا تلاش کرده و در صورت نیاز، کنسول مرورگر خود را برای کسب اطلاعات بیشتر مشاهده نمائید.", + "Error adding ignored user/server": "افزودن کاربر/سرور به لیست نادیده‌گرفته‌ها با خطا همراه بود", + "Ignored/Blocked": "نادیده گرفته‌شده/بلاک‌شده", + "Labs": "قابلیت‌های بتا", + "Clear cache and reload": "پاک‌کردن حافظه‌ی کش و راه‌اندازی مجدد", + "Copy": "رونوشت", + "Your access token gives full access to your account. Do not share it with anyone.": "توکن دسترسی شما، دسترسی کامل به حساب کاربری شما را میسر می‌سازد. لطفا آن را در اختیار فرد دیگری قرار ندهید.", + "Access Token": "توکن دسترسی", + "Identity Server is": "سرور هویت‌سنجی شما عبارت است از", + "Homeserver is": "سرور ما عبارت است از", + "olm version:": "نسخه‌ی olm", + "Versions": "نسخه‌ها", + "Keyboard Shortcuts": "کلیدهای میانبر", + "FAQ": "سوالات پرتکرار", + "Help & About": "کمک و درباره‌ی‌ ما", + "Submit debug logs": "ارسال لاگ مشکل", + "Bug reporting": "گزارش مشکل", + "Credits": "اعتبارها", + "Legal": "قانونی", + "General": "عمومی", + "Discovery": "کاوش", + "Deactivate account": "غیرفعال‌کردن حساب کاربری", + "Deactivating your account is a permanent action - be careful!": "غیرفعال‌سازی حساب کاربری یک عمل دائمی و غیرقابل بازگشت است - لطفا مراقب باشید!", + "Account management": "مدیریت حساب کاربری", + "You can leave the beta any time from settings or tapping on a beta badge, like the one above.": "شما می‌توانید هر زمان که خواستید، در قسمت تنظیمات و یا با کلیک بر روی علامت بتا از محیط بتا خارج شوید.", + "If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "اگر ترک کنید، %(brand)s بعد از غیرفعال کردن قابلیت محیط بازراه‌اندازی خواهد شد. قابلیت‌های فضای کاری و تگ‌های دلخواه مجددا قابل مشاهده خواهند بود.", + "Custom user status messages": "پیام‌های وضعیت کاربر دلخواه", + "Message Pinning": "پین کردن پیام", + "New spinner design": "طرح اسپینر جدید", + "Communities v2 prototypes. Requires compatible homeserver. Highly experimental - use with caution.": "نسخه‌ی ۲ فضای کاری. نیاز به سرور سازگار دارد. به شدت ازمایشی و ناپایدار است - با احتباط استفاده کنید.", + "Render LaTeX maths in messages": "نمایش لاتکس ریاضیات در پیام‌ها", + "Send and receive voice messages": "ارسال و دریافت پیام‌های صوتی", + "Show options to enable 'Do not disturb' mode": "گزینه‌ها را برای فعال‌کردن حالت 'مزاحم نشوید' نشان بده", + "Your feedback will help make spaces better. The more detail you can go into, the better.": "بازخورد شما به بهبود قابلیت محیط کمک خواهد کرد. هر چقدر وارد جزئیات بیشتری شوید، بهتر خواهد بود.", + "Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.": "نسخه‌ی بتا برای وب، دسکتاپ و اندروید در دسترس است. بعضی از قابلیت‌ها ممکن است بر روی سرور شما در دسترس نباشند.", + "%(brand)s will reload with Spaces enabled. Communities and custom tags will be hidden.": "%(brand)s با فعال‌شدن قابلیت محیط‌ها بازراه‌اندازی خواهد شد. فضای کاری و تگ‌های دلخواه ناپدید خواهند شد.", + "Beta available for web, desktop and Android. Thank you for trying the beta.": "نسخه‌ی بتا برای کلاینت‌های وب، دسکتاپ و اندروید موجود است. از بابت امتحان‌کردن نسخه‌ی بتا سپاس‌گزاریم.", + "Spaces are a new way to group rooms and people.": "محیط‌ها روش جدیدی برای دسته‌بندی اتاق‌ها و کاربران است.", + "Spaces": "محیط‌ها", + "Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags. Requires compatible homeserver for some features.": "نمونه اولیه قابلیت محیط. با نسخه‌های ۱ و ۲ فضای کاری، و تگ‌های دلخواه سازگار نیست. برای برخی از قابلیت‌ها نیاز به سرور سازگار دارد.", + "Change notification settings": "تنظیمات اعلان را تغییر دهید", + "%(senderName)s: %(stickerName)s": "%(senderName)s:%(stickerName)s", + "%(senderName)s: %(reaction)s": "%(senderName)s:%(reaction)s", + "%(senderName)s: %(message)s": "%(senderName)s:%(message)s", + "* %(senderName)s %(emote)s": "* %(senderName)s.%(emote)s", + "%(senderName)s is calling": "%(senderName)s در حال تماس است", + "Waiting for answer": "منتظر پاسخ", + "%(senderName)s started a call": "%(senderName)s تماس را شروع کرد", + "You started a call": "شما یک تماس را شروع کردید", + "Call ended": "تماس پایان یافت", + "%(senderName)s ended the call": "%(senderName)s تماس را پایان داد", + "You ended the call": "شما تماس را پایان دادید", + "Call in progress": "تماس در جریان است", + "%(senderName)s joined the call": "%(senderName)s به تماس پیوست", + "You joined the call": "شما به تماس پیوستید", + "The person who invited you already left the room, or their server is offline.": "شخصی که شما را دعوت کرده است از اتاق خارج شده یا سرور وی در دسترس نیست.", + "The person who invited you already left the room.": "شخصی که شما را دعوت کرده از اتاق خارج شده است.", + "Please contact your homeserver administrator.": "لطفاً با مدیر سرور خود تماس بگیرید.", + "Sorry, your homeserver is too old to participate in this room.": "با عرض پوزش ، سرور شما برای شرکت در این اتاق بیش از حد قدیمی است.", + "There was an error joining the room": "هنگام پیوستن به اتاق خطایی رخ داد", + "New version of %(brand)s is available": "نسخه‌ی جدید %(brand)s وجود است", + "Update %(brand)s": "%(brand)s را به‌روزرسانی کنید", + "Check your devices": "دستگاه های خود را بررسی کنید", + "%(deviceId)s from %(ip)s": "%(deviceId)s از %(ip)s", + "New login. Was this you?": "ورود جدید. آیا شما بودید؟", + "Other users may not trust it": "ممکن است سایر کاربران به آن اعتماد نکنند", + "Safeguard against losing access to encrypted messages & data": "محافظ در برابر از دست‌دادن داده‌ها و پیام‌های رمزشده", + "Set up Secure Backup": "پشتیبان‌گیری امن را انجام دهید", + "Your homeserver has exceeded one of its resource limits.": "سرور شما از یکی از محدودیت‌های منابع خود فراتر رفته است.", + "This homeserver has been blocked by it's administrator.": "این سرور توسط مدیر آن مسدود شده است.", + "Your homeserver has exceeded its user limit.": "سرور شما از حد مجاز کاربر خود فراتر رفته است.", + "Use app": "از برنامه استفاده کنید", + "Element Web is experimental on mobile. For a better experience and the latest features, use our free native app.": "نسخه‌ی المنت وب برای گوشی به شکل آزمایشی است. برای داشتن تجربه‌ی بهتر و آخرین به‌روزرسانی‌ها، از برنامک موبایلی استفاده نمائید.", + "Use app for a better experience": "برای تجربه بهتر از برنامه استفاده کنید", + "Enable": "فعال کن", + "Enable desktop notifications": "فعال‌کردن اعلان‌های دسکتاپ", + "Don't miss a reply": "پاسخی را از دست ندهید", + "Review to ensure your account is safe": "برای کسب اطمینان از امن‌بودن حساب کاربری خود، لطفا بررسی فرمائید", + "You have unverified logins": "شما ورودهای تأیید نشده دارید", + "Yes": "بله", + "Help us improve %(brand)s": "به ما در بهبود %(brand)s کمک کنید", + "Unknown App": "برنامه ناشناخته", + "Share your public space": "محیط عمومی خود را به اشتراک بگذارید", + "Invite to %(spaceName)s": "دعوت به %(spaceName)s", + "A word by itself is easy to guess": "حدس زدن یک کلمه به خودی خود آسان است", + "This is similar to a commonly used password": "این مشابه یکی از گذرواژه‌‌هایی است که معمولاً استفاده می شود", + "This is a very common password": "این یک گذرواژه‌ی بسیار رایج است", + "This is a top-100 common password": "این‌ها ۱۰۰ گذرواژه‌ی پر استفاده هستند", + "This is a top-10 common password": "این‌ها ۱۰ گذرواژه‌ی پس استفاده هستند", + "Dates are often easy to guess": "حدس زدن تاریخ‌ها اغلب آسان است", + "Recent years are easy to guess": "حدس زدن سالهای اخیر آسان است", + "Sequences like abc or 6543 are easy to guess": "موارد متوالی نظیر abc یا 6543 برای حدس‌زدن راحت هستند", + "Language and region": "زبان و جغرافیا", + "Set a new account password...": "تنظیم گذرواژه جدید...", + "Phone numbers": "شماره تلفن", + "Email addresses": "آدرس ایمیل", + "Your password was successfully changed. You will not receive push notifications on other sessions until you log back in to them": "گذرواژه‌ی شما با موفیت تغییر کرد. برای دریافت اعلان بر روی سایر نشست‌ها، لطفا مجددا وارد شوید", + "Success": "موفقیت", + "Flair": "فضای کاری", + "Customise your appearance": "ظاهر پیام‌رسان خود را سفارشی‌سازی کنید", + "Enable experimental, compact IRC style layout": "چیدمان IRC فشرده آزمایشی را فعال کنید", + "Show advanced": "نمایش بخش پیشرفته", + "Hide advanced": "پنهان‌کردن بخش پیشرفته", + "Theme": "پوسته", + "Add theme": "افزودن پوسته", + "Custom theme URL": "آدرس پوسته دلخواه", + "Error downloading theme information.": "بارگیری اطلاعات پوسته با خطا همراه بود.", + "Invalid theme schema.": "ساختار پوسته صحیح نیست.", + "Size must be a number": "سایز باید یک عدد باشد", + "Hey you. You're the best!": "سلام. حال شما خوبه؟", + "Check for update": "بررسی برای به‌روزرسانی جدید", + "Integration Managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "مدیرهای یکپارچه‌سازی، داده‌های مربوط به پیکربندی را دریافت کرده و امکان تغییر ویجت‌ها، ارسال دعوتنامه برای اتاق و تنظیم سطح دسترسی از طرف شما را دارا هستند.", + "Manage integrations": "مدیریت پکپارچه‌سازی‌ها", + "Use an Integration Manager to manage bots, widgets, and sticker packs.": "از یک مدیر پکپارچه‌سازی برای مدیریت بات‌ها، ویجت‌ها و پک‌های استیکر مورد نظرتان استفاده نمائید", + "Change": "تغییر بده", + "Enter a new identity server": "یک سرور هویت‌سنجی جدید وارد کنید", + "Do not use an identity server": "از سرور هویت‌سنجی استفاده نکن" } From 595f8428f5f3552660d926d4d446cace56a8bf2d Mon Sep 17 00:00:00 2001 From: Hivaa Date: Tue, 1 Jun 2021 03:40:44 +0000 Subject: [PATCH 295/999] Translated using Weblate (Persian) Currently translated at 100.0% (2974 of 2974 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/fa/ --- src/i18n/strings/fa.json | 178 ++++++++++++++++++++++++++++++++++----- 1 file changed, 158 insertions(+), 20 deletions(-) diff --git a/src/i18n/strings/fa.json b/src/i18n/strings/fa.json index 99c1252dd9..acea50c83e 100644 --- a/src/i18n/strings/fa.json +++ b/src/i18n/strings/fa.json @@ -862,7 +862,7 @@ "%(creator)s created this DM.": "%(creator)s این گفتگو را ایجاد کرد.", "You’re all caught up": "همه‌ی کارها را انجام دادید", "The server is offline.": "سرور آفلاین است.", - "You're all caught up.": "همه‌ی کارها را انجام دادید", + "You're all caught up.": "همه‌ی کارها را انجام دادید.", "Successfully restored %(sessionCount)s keys": "کلیدهای %(sessionCount)s با موفقیت بازیابی شدند", "Fetching keys from server...": "واکشی کلیدها از سرور ...", "Restoring keys from backup": "بازیابی کلیدها از نسخه پشتیبان", @@ -911,7 +911,7 @@ "Upgrade private room": "ارتقاء اتاق خصوصی", "Automatically invite users": "به طور خودکار کاربران را دعوت کن", "Resend %(unsentCount)s reaction(s)": "بازارسال %(unsentCount)s واکنش", - "Missing session data": "", + "Missing session data": "داده‌های نشست از دست رفته است", "Manually export keys": "کلیدها را به صورت دستی استخراج (Export)کن", "No backup found!": "نسخه پشتیبان یافت نشد!", "Incompatible local cache": "حافظه‌ی محلی ناسازگار", @@ -973,7 +973,7 @@ "Click the button below to confirm your identity.": "برای تأیید هویت خود بر روی دکمه زیر کلیک کنید.", "Confirm to continue": "برای ادامه تأیید کنید", "To continue, use Single Sign On to prove your identity.": "برای ادامه از احراز هویت یکپارچه جهت اثبات هویت خود استفاده نمائید.", - "Your %(brand)s doesn't allow you to use an Integration Manager to do this. Please contact an admin.": "٪ (مارک) s شما اجازه استفاده از مدیر ادغام را برای این کار نمی دهد. لطفا با مدیر تماس بگیرید", + "Your %(brand)s doesn't allow you to use an Integration Manager to do this. Please contact an admin.": "%(brand)s شما اجازه استفاده از سیستم مدیریت ادغام را برای این کار نمی دهد. لطفا با ادمین تماس بگیرید.", "Integrations not allowed": "یکپارچه‌سازی‌ها اجازه داده نشده‌اند", "Enable 'Manage Integrations' in Settings to do this.": "برای انجام این کار 'مدیریت پکپارچه‌سازی‌ها' را در تنظیمات فعال نمائید.", "Integrations are disabled": "پکپارچه‌سازی‌ها غیر فعال هستند", @@ -1016,7 +1016,7 @@ "Value in this room:": "مقدار در این اتاق:", "Value:": "مقدار:", "Save setting values": "ذخیره مقادیر تنظیمات", - "Values at explicit levels in this room": "مقادیر در سطوح مشخص در این اتاق:", + "Values at explicit levels in this room": "مقادیر در سطوح مشخص در این اتاق", "Values at explicit levels": "مقادیر در سطوح مشخص", "Settable at room": "قابل تنظیم در اتاق", "Settable at global": "قابل تنظیم به شکل سراسری", @@ -1304,7 +1304,7 @@ "Continue With Encryption Disabled": "با رمزنگاری غیرفعال ادامه بده", "Incompatible Database": "پایگاه داده ناسازگار", "You've previously used a newer version of %(brand)s with this session. To use this version again with end to end encryption, you will need to sign out and back in again.": "شما قبلاً با این نشست از نسخه جدیدتر %(brand)s استفاده کرده‌اید. برای استفاده مجدد از این نسخه با قابلیت رمزنگاری سرتاسر ، باید از حسابتان خارج شده و دوباره وارد برنامه شوید.", - "To avoid losing your chat history, you must export your room keys before logging out. You will need to go back to the newer version of %(brand)s to do this": "برای جلوگیری از از دست دادن تاریخچه‌ی گفتگوی خود باید قبل از ورود به برنامه ، کلیدهای اتاق خود را استخراج (Export) کنید. برای این کار باید از نسخه جدیدتر %(brand)s استفاده کنید", + "To avoid losing your chat history, you must export your room keys before logging out. You will need to go back to the newer version of %(brand)s to do this": "برای جلوگیری از دست دادن تاریخچه‌ی گفتگوی خود باید قبل از ورود به برنامه ، کلیدهای اتاق خود را استخراج (Export) کنید. برای این کار باید از نسخه جدیدتر %(brand)s استفاده کنید", "Sign out": "خروج از حساب کاربری", "Block anyone not part of %(serverName)s from ever joining this room.": "از عضوشدن کاربرانی در این اتاق که حساب آن‌ها متعلق به سرور %(serverName)s است، جلوگیری کن.", "Make this room public": "این اتاق را عمومی کنید", @@ -1763,7 +1763,7 @@ "You are about to be taken to a third-party site so you can authenticate your account for use with %(integrationsUrl)s. Do you wish to continue?": "شما در آستانه هدایت شدن به یک سایت ثالث هستید بنابراین می توانید حساب خود را برای استفاده با %(integrationsUrl)s احراز هویت کنید. آیا مایل هستید ادامه دهید؟", "Add an Integration": "یکپارچه سازی اضافه کنید", "This room is a continuation of another conversation.": "این اتاق ادامه گفتگوی دیگر است.", - "Click here to see older messages.": "برای دیدن پیام های قدیمی اینجا کلیک کنید", + "Click here to see older messages.": "برای دیدن پیام های قدیمی اینجا کلیک کنید.", "%(senderDisplayName)s changed the room avatar to ": "%(senderDisplayName)s آواتار اتاق را به تغییر داد", "%(senderDisplayName)s removed the room avatar.": "%(senderDisplayName)s آواتار اتاق را حذف کرد.", "%(senderDisplayName)s changed the avatar for %(roomName)s": "%(senderDisplayName)s آواتار خود را در %(roomName)s تغییر داد", @@ -2059,9 +2059,9 @@ "This room has already been upgraded.": "این اتاق قبلاً ارتقا یافته است.", "Upgrading this room will shut down the current instance of the room and create an upgraded room with the same name.": "با ارتقا این اتاق نسخه فعلی اتاق خاموش شده و یک اتاق ارتقا یافته به همین نام ایجاد می شود.", "Unread messages.": "پیام های خوانده نشده.", - "%(count)s unread messages.|one": "1 پیام خوانده نشده", + "%(count)s unread messages.|one": "۱ پیام خوانده نشده.", "%(count)s unread messages.|other": "%(count)s پیام خوانده نشده.", - "%(count)s unread messages including mentions.|one": "1 اشاره خوانده نشده", + "%(count)s unread messages including mentions.|one": "۱ اشاره خوانده نشده.", "%(count)s unread messages including mentions.|other": "%(count)s پیام‌های خوانده نشده از جمله اشاره‌ها.", "Room options": "تنظیمات اتاق", "Leave Room": "ترک اتاق", @@ -2471,7 +2471,7 @@ " invites you": " شما را دعوت کرد", "Private space": "محیط خصوصی", "Public space": "محیط عمومی", - "Spaces are a beta feature.": "محیط یک قابلیت بتا است", + "Spaces are a beta feature.": "فضای کاری یک قابلیت بتا است.", "Create room": "ساختن اتاق", "No results found": "نتیجه‌ای یافت نشد", "Removing...": "در حال حذف...", @@ -2572,8 +2572,8 @@ "You are an administrator of this community": "شما یکی از مدیران این فضای کاری هستید", "Leave this community": "ترک این فضای کاری", "Join this community": "پیوستن به این فضای کاری", - "Featured Users:": "کاربران ویژه", - "Featured Rooms:": "اتاق‌های ویژه", + "Featured Users:": "کاربران ویژه:", + "Featured Rooms:": "اتاق‌های ویژه:", "These rooms are displayed to community members on the community page. Community members can join the rooms by clicking on them.": "این اتاق‌ها به اعضای فضای کاری در صفحه‌ی این فضا نمایش داده می‌شود. اعضا‌ی فضا می‌توانند با کلیک بر روی اتاق‌ها به آن‌ها بپیوندند.", "Community Settings": "تنظیمات فضای کاری", "Unable to leave community": "ترک فضای کاری ممکن نیست", @@ -2600,7 +2600,7 @@ "Add an email to be able to reset your password.": "برای داشتن امکان تغییر گذرواژه در صورت فراموش‌کردن آن، لطفا یک آدرس ایمیل وارد نمائید.", "Register": "ایجاد حساب کاربری", "Sign in": "ورود به حساب کاربری", - "Sign in with": "سازوکار ورود:", + "Sign in with": "نحوه ورود", "Forgot password?": "فراموشی گذرواژه", "Phone": "شماره تلفن", "Username": "نام کاربری", @@ -2653,8 +2653,8 @@ "Revoke": "برگرداندن", "Complete": "تکمیل", "Verify the link in your inbox": "لینک موجود در صندوق دریافت خود را تائید کنید", - "Unable to verify email address.": "تائید آدرس ایمیل ممکن نیست", - "Click the link in the email you received to verify and then click continue again.": "برای تائید ادرس ایمیل، بر روی لینکی که برای شما ایمیل شده‌است کلیک کرده و مجددا بر روی ادامه کلیک کنید", + "Unable to verify email address.": "تائید آدرس ایمیل ممکن نیست.", + "Click the link in the email you received to verify and then click continue again.": "برای تائید ادرس ایمیل، بر روی لینکی که برای شما ایمیل شده‌است کلیک کرده و مجددا بر روی ادامه کلیک کنید.", "Your email address hasn't been verified yet": "آدرس ایمیل شما هنوز تائید نشده‌است", "Unable to share email address": "به اشتراک‌گذاری آدرس ایمیل ممکن نیست", "Unable to revoke sharing for email address": "لغو اشتراک گذاری برای آدرس ایمیل ممکن نیست", @@ -2720,15 +2720,15 @@ "Missing media permissions, click the button below to request.": "دسترسی به رسانه از دست رفت، برای درخواست مجدد بر روی دکمه‌ی زیر کلیک نمائید.", "A session's public name is visible to people you communicate with": "نام عمومی یک نشست برای افرادی که با آن‌ها ارتباط برقرار کرده‌اید، قابل مشاهده است", "Where you’re logged in": "کجا وارد حساب کاربری خود شده‌اید", - "Learn more about how we use analytics.": "در مورد نحوه‌ی استفاده‌ی ما از داده‌ها بیشتر بدانید", + "Learn more about how we use analytics.": "در مورد نحوه‌ی استفاده‌ی ما از داده‌ها بیشتر بدانید.", "Privacy is important to us, so we don't collect any personal or identifiable data for our analytics.": "به دلیل اهمیت حریم خصوصی، ما هیچ‌گونه‌ داده‌ی شخصی‌ و قابل ردگیری را از شما جمع‌آوری نمی‌کنیم.", "Privacy": "حریم خصوصی", "Cross-signing": "امضاء متقابل", "Message search": "جستجوی پیام‌ها", "Secure Backup": "پشتیبان‌گیری امن", "You have no ignored users.": "شما هیچ کاربری را نادیده نگرفته‌اید.", - "Session key:": "کلید نشست", - "Session ID:": "شناسه‌ی نشست", + "Session key:": "کلید نشست:", + "Session ID:": "شناسه‌ی نشست:", "Import E2E room keys": "واردکردن کلیدهای رمزنگاری اتاق‌ها", "": "<پشتیبانی نمی‌شود>", "Unignore": "لغو نادیده‌گرفتن", @@ -2763,7 +2763,7 @@ "Access Token": "توکن دسترسی", "Identity Server is": "سرور هویت‌سنجی شما عبارت است از", "Homeserver is": "سرور ما عبارت است از", - "olm version:": "نسخه‌ی olm", + "olm version:": "نسخه‌ی olm:", "Versions": "نسخه‌ها", "Keyboard Shortcuts": "کلیدهای میانبر", "FAQ": "سوالات پرتکرار", @@ -2866,8 +2866,146 @@ "Check for update": "بررسی برای به‌روزرسانی جدید", "Integration Managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "مدیرهای یکپارچه‌سازی، داده‌های مربوط به پیکربندی را دریافت کرده و امکان تغییر ویجت‌ها، ارسال دعوتنامه برای اتاق و تنظیم سطح دسترسی از طرف شما را دارا هستند.", "Manage integrations": "مدیریت پکپارچه‌سازی‌ها", - "Use an Integration Manager to manage bots, widgets, and sticker packs.": "از یک مدیر پکپارچه‌سازی برای مدیریت بات‌ها، ویجت‌ها و پک‌های استیکر مورد نظرتان استفاده نمائید", + "Use an Integration Manager to manage bots, widgets, and sticker packs.": "از یک مدیر پکپارچه‌سازی برای مدیریت بات‌ها، ویجت‌ها و پک‌های استیکر مورد نظرتان استفاده نمائید.", "Change": "تغییر بده", "Enter a new identity server": "یک سرور هویت‌سنجی جدید وارد کنید", - "Do not use an identity server": "از سرور هویت‌سنجی استفاده نکن" + "Do not use an identity server": "از سرور هویت‌سنجی استفاده نکن", + "If you do, please note that none of your messages will be deleted, but the search experience might be degraded for a few moments whilst the index is recreated": "اگر این کار را انجام می‌دهید، لطفاً توجه داشته باشید که هیچ یک از پیام‌های شما حذف نمی‌شوند ، با این حال چون پیام‌ها مجددا ایندکس می‌شوند، ممکن است برای چند لحظه قابلیت جستجو با مشکل مواجه شود", + "Deleting cross-signing keys is permanent. Anyone you have verified with will see security alerts. You almost certainly don't want to do this, unless you've lost every device you can cross-sign from.": "حذف کلیدهای امضای متقابل دائمی است. هرکسی که او را تائید کرده‌باشید، هشدارهای امنیتی را مشاهده خواهد کرد. به احتمال زیاد نمی‌خواهید این کار را انجام دهید ، مگر هیچ دستگاهی برای امضاء متقابل از طریق آن نداشته باشید.", + "Enter your Security Phrase or to continue.": "برای ادامه، عبارت امنیتی خود را وارد کرده و یا .", + "Click the button below to confirm setting up encryption.": "برای تأیید و فعال‌سازی رمزگذاری ، روی دکمه زیر کلیک کنید.", + "Unable to access secret storage. Please verify that you entered the correct Security Phrase.": "دسترسی به حافظه نهان امکان‌پذیر نیست. لطفاً تأیید کنید که عبارت امنیتی صحیح را وارد کرده‌اید.", + "If you reset everything, you will restart with no trusted sessions, no trusted users, and might not be able to see past messages.": "اگر همه موارد را بازراه‌اندازی (reset) کنید، دیگر هیچ نشست تائید شده‌ای و هیچ کاربر تائيد‌ شده‌ای نخواهید داشت و ممکن است نتوانید پیام‌های گذشته‌ی خود را مشاهده نمائید.", + "Backup could not be decrypted with this Security Key: please verify that you entered the correct Security Key.": "نسخه پشتیبان با این کلید امنیتی رمزگشایی نمی شود: لطفاً بررسی کنید که کلید امنیتی درست را وارد کرده اید.", + "Backup could not be decrypted with this Security Phrase: please verify that you entered the correct Security Phrase.": "نسخه پشتیبان با این عبارت امنیتی رمزگشایی نمی‌شود: لطفاً بررسی کنید که عبارت امنیتی درست را وارد کرده اید.", + "Warning: you should only set up key backup from a trusted computer.": "هشدا: پشتیبان گیری از کلید را فقط از یک رایانه مطمئن انجام دهید.", + "Access your secure message history and set up secure messaging by entering your Security Phrase.": "با وارد کردن عبارت امنیتی خود به سابقه پیام‌های رمز شدتان دسترسی پیدا کرده و پیام امن ارسال کنید.", + "Only do this if you have no other device to complete verification with.": "این کار را فقط درصورتی انجام دهید که دستگاه دیگری برای تکمیل فرآیند تأیید ندارید.", + "Forgotten or lost all recovery methods? Reset all": "همه روش‌های بازیابی را فراموش کرده یا از دست داده‌اید؟ بازراه‌اندازی (reset) همه", + "If you've forgotten your Security Phrase you can use your Security Key or set up new recovery options": "اگر عبارت امنیتی خود را فراموش کرده اید، می توانید از کلید امنیتی خود استفاده کنید یا تنظیمات پشتیانی‌گیری را مجددا انجام دهید", + "The widget will verify your user ID, but won't be able to perform actions for you:": "ابزارک شناسه‌ی کاربری شما را تائید خواهد کرد، اما نمی‌تواند این کارها را برای شما انجام دهد:", + "This looks like a valid Security Key!": "به نظر می رسد این یک کلید امنیتی معتبر است!", + "Warning: You should only set up key backup from a trusted computer.": "هشدار: پشتیان کلید تنها باید در یک رایانه‌ی مطمئن انجام شود.", + "Allow this widget to verify your identity": "به این ابزارک اجازه دهید هویت شما را تأیید کند", + "Approve": "تایید", + "Some files are too large to be uploaded. The file size limit is %(limit)s.": "برخی از فایل‌ها برای بارگذاری بیش از حد بزرگ هستند. محدودیت اندازه فایل %(limit)s است.", + "Access your secure message history and set up secure messaging by entering your Security Key.": "با وارد کردن کلید امنیتی خود به تاریخچه‌ی پیام‌‌های رمز شده خود دسترسی پیدا کرده و پیام امن ارسال کنید.", + "These files are too large to upload. The file size limit is %(limit)s.": "این فایل‌ها برای بارگذاری بیش از حد بزرگ هستند. محدودیت اندازه فایل %(limit)s است.", + "If you've forgotten your Security Key you can ": "اگر کلید امنیتی خود را فراموش کرده‌اید ، می توانید ", + "This file is too large to upload. The file size limit is %(limit)s but this file is %(sizeOfThisFile)s.": "این فایل برای بارگذاری بسیار بزرگ است. محدودیت اندازه‌ی فایل برابر %(limit)s است اما حجم این فایل %(sizeOfThisFile)s.", + "Ask this user to verify their session, or manually verify it below.": "از این کاربر بخواهید نشست خود را تأیید کرده و یا آن را به صورت دستی تأیید کنید.", + "Away": "بعدا", + "%(name)s (%(userId)s) signed in to a new session without verifying it:": "%(name)s (%(userId)s) وارد یک نشست جدید شد بدون اینکه آن را تائید کند:", + "This homeserver would like to make sure you are not a robot.": "این سرور می خواهد مطمئن شود که شما یک ربات نیستید.", + "Confirm your identity by entering your account password below.": "با وارد کردن رمز ورود حساب خود در زیر ، هویت خود را تأیید کنید.", + "Missing captcha public key in homeserver configuration. Please report this to your homeserver administrator.": "کلید عمومی captcha در پیکربندی سرور خانگی وجود ندارد. لطفاً این را به ادمین سرور خود گزارش دهید.", + "Verify your other session using one of the options below.": "نست دیگر خود را با استفاده از یکی از راهکارهای زیر تأیید کنید.", + "You signed in to a new session without verifying it:": "شما وارد یک نشست جدید شده‌اید بدون اینکه آن را تائید کنید:", + "Please review and accept all of the homeserver's policies": "لطفاً کلیه خط مشی‌های سرور را مرور و قبول کنید", + "Please review and accept the policies of this homeserver:": "لطفاً خط مشی‌های این سرور را مرور و قبول کنید:", + "Next": "بعدی", + "A confirmation email has been sent to %(emailAddress)s": "یک ایمیل تأیید به %(emailAddress)s ارسال شد", + "Document": "سند", + "Summary": "خلاصه", + "Service": "سرویس", + "Open the link in the email to continue registration.": "برای ادامه ثبت نام ، پیوند موجود در ایمیل را باز کنید.", + "Token incorrect": "کد نامعتبر است", + "To continue you need to accept the terms of this service.": "برای ادامه باید شرایط این سرویس را بپذیرید.", + "Use bots, bridges, widgets and sticker packs": "از بات‌ها، پل‌ها ارتباطی، ابزارک‌ها و بسته‌های استیکر استفاده کنید", + "A text message has been sent to %(msisdn)s": "کد فعال‌سازی به %(msisdn)s ارسال شد", + "Your browser likely removed this data when running low on disk space.": "هنگام کمبود فضای دیسک ، مرورگر شما این داده ها را حذف می کند.", + "Use an email address to recover your account": "برای بازیابی حساب خود از آدرس ایمیل استفاده کنید", + "Some session data, including encrypted message keys, is missing. Sign out and sign in to fix this, restoring keys from backup.": "برخی از داده‌های نشست ، از جمله کلیدهای رمزنگاری پیام‌ها موجود نیست. برای برطرف کردن این مشکل از برنامه خارج شده و مجددا وارد شوید و از کلیدها را از نسخه‌ی پشتیبان بازیابی نمائيد.", + "Enter email address (required on this homeserver)": "آدرس ایمیل را وارد کنید (در این سرور اجباری است)", + "Other users can invite you to rooms using your contact details": "سایر کاربران می توانند شما را با استفاده از اطلاعات تماستان به اتاق ها دعوت کنند", + "Enter phone number (required on this homeserver)": "شماره تلفن را وارد کنید (در این سرور اجباری است)", + "Use lowercase letters, numbers, dashes and underscores only": "فقط از حروف کوچک، اعداد، خط تیره و زیر خط استفاده کنید", + "Use email to optionally be discoverable by existing contacts.": "از ایمیل استفاده کنید تا به طور اختیاری توسط مخاطبین موجود قابل کشف باشید.", + "Use email or phone to optionally be discoverable by existing contacts.": "از ایمیل یا تلفن استفاده کنید تا به طور اختیاری توسط مخاطبین موجود قابل کشف باشید.", + "To help us prevent this in future, please send us logs.": "برای کمک به ما در جلوگیری از این امر در آینده ، لطفا لاگ‌ها را برای ما ارسال کنید.", + "You must register to use this functionality": "برای استفاده از این قابلیت باید ثبت نام کنید", + "

      HTML for your community's page

      \n

      \n Use the long description to introduce new members to the community, or distribute\n some important links\n

      \n

      \n You can even add images with Matrix URLs \n

      \n": "

      html مرتبط با اجتماع شما

      \n

      \n برای معرفی اجتماع به اعضای جدید، یا ذکر نکات مهم،\n از توضیحات مفصل استفاده کنیدلینک\n

      \n

      \n شما حتی می‌توانید تصاویر را با استفاده از url ماتریکس به این صفحه اضافه کنید \n

      \n", + "Saving...": "در حال ذخیره‌سازی...", + "Edit settings relating to your space.": "تنظیمات مربوط به فضای کاری خود را ویرایش کنید.", + "This will allow you to reset your password and receive notifications.": "با این کار می‌توانید گذرواژه خود را تغییر داده و اعلان‌ها را دریافت کنید.", + "Please check your email and click on the link it contains. Once this is done, click continue.": "لطفاً ایمیل خود را بررسی کرده و روی لینکی که برایتان ارسال شده، کلیک کنید. پس از انجام این کار، روی ادامه کلیک کنید.", + "Verification Pending": "در انتظار تائید", + "Clearing your browser's storage may fix the problem, but will sign you out and cause any encrypted chat history to become unreadable.": "پاک کردن فضای ذخیره‌سازی مرورگر ممکن است این مشکل را برطرف کند ، اما شما را از برنامه خارج کرده و باعث می‌شود هرگونه سابقه گفتگوی رمزشده غیرقابل خواندن باشد.", + "If you have previously used a more recent version of %(brand)s, your session may be incompatible with this version. Close this window and return to the more recent version.": "اگر در گذشته از نسخه جدیدتر %(brand)s استفاده کرده‌اید ، نشست شما ممکن است با این نسخه ناسازگار باشد. این پنجره را بسته و به نسخه جدیدتر برگردید.", + "We encountered an error trying to restore your previous session.": "هنگام تلاش برای بازیابی نشست قبلی شما، با خطایی روبرو شدیم.", + "Refresh": "رفرش", + "Failed to add the following rooms to the summary of %(groupId)s:": "افزدون اتاق‌های زیر به خلاصه %(groupId)s انجام نشد:", + "You most likely do not want to reset your event index store": "به احتمال زیاد نمی‌خواهید مخزن فهرست رویدادهای خود را حذف کنید", + "Failed to remove the room from the summary of %(groupId)s": "اتاق از خلاصه %(groupId)s حذف نشد", + "Use your preferred Matrix homeserver if you have one, or host your own.": "از یک سرور مبتنی بر پروتکل ماتریکس که ترجیح می‌دهید استفاده کرده، و یا از سرور شخصی خودتان استفاده کنید.", + "The room '%(roomName)s' could not be removed from the summary.": "اتاق «%(roomName)s» از خلاصه حذف نمی شود.", + "Failed to add the following users to the summary of %(groupId)s:": "افزودن کاربران زیر به خلاصه %(groupId)s انجام نشد:", + "We call the places where you can host your account ‘homeservers’.": "ما به زیرساختی که شما می‌توانید بر روی آن حساب کاربری ایجاد کنید، \"سرور\" می‌گوییم.", + "Failed to remove a user from the summary of %(groupId)s": "حذف کاربر از خلاصه %(groupId)s انجام نشد", + "The user '%(displayName)s' could not be removed from the summary.": "کاربر «%(displayName)s» را نمی توان از خلاصه حذف کرد.", + "Leave %(groupName)s?": "%(groupName)s را ترک می‌کنید؟", + "Want more than a community? Get your own server": "بیش از یک اجتماع می خواهید؟سرور خود را دریافت کنید", + "Matrix.org is the biggest public homeserver in the world, so it’s a good place for many.": "Matrix.org بزرگترین سرور عمومی در جهان است ، بنابراین مکان خوبی برای بسیاری از افراد جهت برقراری ارتباط به شمار می‌رود.", + "Changes made to your community name and avatar might not be seen by other users for up to 30 minutes.": "تغییراتی که در نام و آواتار در اجتماع شما ایجاد شده است ممکن است توسط کاربران دیگر حداکثر تا ۳۰ دقیقه دیده شود.", + "Recent changes that have not yet been received": "تغییرات اخیری که هنوز دریافت نشده‌اند", + "The server is not configured to indicate what the problem is (CORS).": "سرور طوری پیکربندی نشده تا نشان دهد مشکل چیست (CORS).", + "%(inviter)s has invited you to join this community": "%(inviter)s از شما برای پیوستن به این اجتماع دعوت کرده است", + "A connection error occurred while trying to contact the server.": "هنگام تلاش برای اتصال به سرور خطایی رخ داده است.", + "Your community hasn't got a Long Description, a HTML page to show to community members.
      Click here to open settings and give it one!": "اجتماع شما دارای توصیف طولانی (صفحه HTML برای نشان دادن به اعضای انجمن) نیست.
      برای باز کردن تنظیمات اینجا کلیک کنید و یک توصیف به آن اضافه کنید!", + "Your area is experiencing difficulties connecting to the internet.": "منطقه شما در اتصال به اینترنت با مشکل روبرو است.", + "A browser extension is preventing the request.": "پلاگینی در مرورگر مانع از ارسال درخواست می‌گردد.", + "Your firewall or anti-virus is blocking the request.": "دیوار آتش یا آنتی‌ویروس شما مانع از ارسال درخواست می‌شود.", + "To continue using the %(homeserverDomain)s homeserver you must review and agree to our terms and conditions.": "برای ادامه استفاده از سرور %(homeserverDomain)s باید شرایط و ضوابط ما را بررسی کرده و موافقت کنید.", + "The server (%(serverName)s) took too long to respond.": "زمان پاسخگویی سرور (%(serverName)s) بسیار طولانی شده‌است.", + "Data from an older version of %(brand)s has been detected. This will have caused end-to-end cryptography to malfunction in the older version. End-to-end encrypted messages exchanged recently whilst using the older version may not be decryptable in this version. This may also cause messages exchanged with this version to fail. If you experience problems, log out and back in again. To retain message history, export and re-import your keys.": "داده هایی از نسخه قدیمی %(brand)s شناسایی شده است. این امر باعث اختلال در رمزنگاری سرتاسر در نسخه قدیمی شده است. پیام های رمزگذاری شده سرتاسر که اخیراً رد و بدل شده اند ممکن است با استفاده از نسخه قدیمی رمزگشایی نشوند. همچنین ممکن است پیام های رد و بدل شده با این نسخه با مشکل مواجه شود. اگر مشکلی رخ داد، از سیستم خارج شوید و مجددا وارد شوید. برای حفظ سابقه پیام، کلیدهای خود را خروجی گرفته و دوباره وارد کنید.", + "Your server isn't responding to some of your requests. Below are some of the most likely reasons.": "سرور شما به برخی از درخواست‌ها پاسخ نمی‌دهد. در ادامه برخی از دلایل محتمل آن ذکر شده است.", + "You'll upgrade this room from to .": "این اتاق را از به ارتقا خواهید داد.", + "Upgrading a room is an advanced action and is usually recommended when a room is unstable due to bugs, missing features or security vulnerabilities.": "به‌روزرسانی اتاق اقدامی پیشرفته بوده و معمولاً در صورتی توصیه می‌شود که اتاق به دلیل اشکالات، فقدان قابلیت‌ها یا آسیب پذیری‌های امنیتی، پایدار و قابل استفاده نباشد.", + "Did you know: you can use communities to filter your %(brand)s experience!": "آیا می دانید: برای فیلتر کردن تجربیات خود در %(brand)s می توانید از اجتماع‌ها استفاده کنید!", + "This usually only affects how the room is processed on the server. If you're having problems with your %(brand)s, please report a bug.": "این معمولاً فقط بر نحوه پردازش اتاق در سرور تأثیر می‌گذارد. اگر با %(brand)s خود مشکلی دارید، لطفاً اشکال را گزارش کنید.", + "To set up a filter, drag a community avatar over to the filter panel on the far left hand side of the screen. You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.": "برای تنظیم فیلتر، آواتار یک اجتماع را به صفحه فیلتر در سمت چپ صفحه بکشید. همواره می‌توانید با کلیک برر روی آواتار در صفحه فیلتر، فقط اتاق ها و افراد مرتبط با آن انجمن را مشاهده کنید.", + "This usually only affects how the room is processed on the server. If you're having problems with your %(brand)s, please report a bug.": "این معمولاً فقط بر نحوه پردازش اتاق در سرور تأثیر می‌گذارد. اگر با %(brand)s خود مشکلی دارید ، لطفاً یک اشکال گزارش دهید.", + "Put a link back to the old room at the start of the new room so people can see old messages": "در ابتدای اتاق جدید پیوندی به اتاق قدیمی قرار دهید تا افراد بتوانند پیام‌های موجود در اتاق قدیمی را ببینند", + "Stop users from speaking in the old version of the room, and post a message advising users to move to the new room": "از گفتگوی کاربران در نسخه قدیمی اتاق جلوگیری کرده و با ارسال پیامی به کاربران توصیه کنید به اتاق جدید منتقل شوند", + "Update any local room aliases to point to the new room": "برای اشاره به اتاق جدید، نام‌های مستعار (aliases) اتاق محلی را به‌روز کنید", + "Create a new room with the same name, description and avatar": "یک اتاق جدید با همان نام ، توضیحات و نمایه ایجاد کنید", + "Upgrading this room requires closing down the current instance of the room and creating a new room in its place. To give room members the best possible experience, we will:": "ارتقاء این اتاق نیازمند بستن نسخه‌ی فعلی و ساختن درجای یک اتاق جدید است. برای داشتن بهترین تجربه‌ی کاربری ممکن، ما:", + "The room upgrade could not be completed": "متاسفانه فرآیند ارتقاء اتاق به پایان نرسید", + "Reporting this message will send its unique 'event ID' to the administrator of your homeserver. If messages in this room are encrypted, your homeserver administrator will not be able to read the message text or view any files or images.": "گزارش این پیام شناسه‌ی منحصر به فرد رخداد آن را برای مدیر سرور ارسال می‌کند. اگر پیام‌های این اتاق رمزشده باشند، مدیر سرور شما امکان خواندن متن آن پیام یا مشاهده‌ی عکس یا فایل‌های دیگر را نخواهد داشت.", + "Just a heads up, if you don't add an email and forget your password, you could permanently lose access to your account.": "حواستان را جمع کنید، اگر ایمیلی اضافه نکرده و گذرواژه‌ی خود را فراموش کنید ، ممکن است دسترسی به حساب کاربری خود را برای همیشه از دست دهید.", + "Doesn't look like a valid email address": "به نظر نمی‌رسد یک آدرس ایمیل معتبر باشد", + "%(brand)s failed to get the protocol list from the homeserver. The homeserver may be too old to support third party networks.": "%(brand)s نتوانست لیست پروتکل را از سرور میزبان دریافت کند. ممکن است سرور برای پشتیبانی از شبکه های شخص ثالث خیلی قدیمی باشد.", + "If they don't match, the security of your communication may be compromised.": "اگر آنها مطابقت نداشته‌باشند ، ممکن است امنیت ارتباطات شما به خطر افتاده باشد.", + "If you didn’t sign in to this session, your account may be compromised.": "اگر وارد این نشست نشده‌اید ، ممکن است حساب کاربری شما به خطر افتاد باشد.", + "%(brand)s failed to get the public room list.": "%(brand)s نتوانست لیست اتاق‌های عمومی را دریافت کند.", + "Use this session to verify your new one, granting it access to encrypted messages:": "از این نشست برای تائید نشست‌های جدید خود و اعطای دسترسی به پیام‌های رمزشده استفاده کنید:", + "Delete the room address %(alias)s and remove %(name)s from the directory?": "اتاق با آدرس %(alias)s حذف شده و نام %(name)s از فهرست اتاق‌ها نیز پاک شود؟", + "We recommend you change your password and Security Key in Settings immediately": "ما به شما توصیه می‌کنیم بلافاصله گذرواژه و کلید امنیتی خود را در تنظیمات تغییر دهید", + "The internet connection either session is using": "اتصال اینترنت و یا نشست در حال استفاده است", + "%(brand)s does not know how to join a room on this network": "%(brand)s نمی‌تواند به اتاقی در این شبکه بپیوندد", + "Data on this screen is shared with %(widgetDomain)s": "داده‌های این صفحه با %(widgetDomain)s به اشتراک گذاشته می‌شود", + "Your homeserver doesn't seem to support this feature.": "به نظر نمی‌رسد که سرور شما از این قابلیت پشتیبانی کند.", + "Unable to look up room ID from server": "جستجوی شناسه اتاق از سرور انجام نشد", + "Not currently indexing messages for any room.": "در حال حاضر ایندکس پیام ها برای هیچ اتاقی انجام نمی‌شود.", + "Session ID": "شناسه‌ی نشست", + "Confirm this user's session by comparing the following with their User Settings:": "این نشست کاربر را از طریق مقایسه‌ی این با تنظیمات کاربری تائيد کنید:", + "Confirm by comparing the following with the User Settings in your other session:": "از طریق مقایسه‌ی این با تنظیمات کاربری در نشست‌های دیگرتان، تائيد کنید:", + "Are you sure you want to sign out?": "آیا مطمئن هستید که می خواهید از برنامه خارج شوید؟", + "You'll lose access to your encrypted messages": "دسترسی به پیام‌های رمزشده‌ی خود را از دست خواهید داد", + "Encrypted messages are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.": "پیام‌های رمزشده با رمزنگاری سرتاسر ایمن می‌شوند. فقط شما و طرف گیرنده(ها) کلیدهای خواندن این پیام ها را در اختیار دارید.", + "%(brand)s now uses 3-5x less memory, by only loading information about other users when needed. Please wait whilst we resynchronise with the server!": "هم‌اکنون %(brand)s از طریق بارگیری و نمایش اطلاعات کاربران تنها در زمان‌هایی که نیاز است، حدود ۳ تا ۵ مرتبه حافظه‌ی کمتری استفاده می‌کند. لطفا تا همگام‌سازی با سرور منتظر بمانید!", + "If the other version of %(brand)s is still open in another tab, please close it as using %(brand)s on the same host with both lazy loading enabled and disabled simultaneously will cause issues.": "اگر نسخه دیگری از %(brand)s هنوز در تب‌های دیگر باز است، لطفاً آن را ببندید زیرا استفاده از %(brand)s با قابلیت بارگیری تکه‌تکه‌ی فعال روی یکی و غیرفعال روی دیگری، باعث ایجاد مشکل می شود.", + "You've previously used %(brand)s on %(host)s with lazy loading of members enabled. In this version lazy loading is disabled. As the local cache is not compatible between these two settings, %(brand)s needs to resync your account.": "شما از %(brand)s بر روی %(host)s با قابلیت بارگیری اعضا به شکل تکه‌تکه استفاده می‌کنید. در این نسخه قابلیت بارگیری تکه‌تکه غیرفعال است. از آن‌جایی که حافظه‌ی کش مورد استفاده برای این دو پیکربندی با هم سازگار نیست، %(brand)s نیاز به همگام‌سازی مجدد حساب کاربری شما دارد.", + "%(brand)s encountered an error during upload of:": "%(brand)s در حین بارگذاری این دچار مشکل شد:", + "Transfer": "منتقل کردن", + "Invited people will be able to read old messages.": "افراد دعوت‌شده خواهند توانست پیام‌های قدیمی را بخوانند.", + "Invite someone using their name, username (like ) or share this room.": "با استفاده از نام یا نام کاربری (مانند ) از افراد دعوت کرده و یا این اتاق را به اشتراک بگذارید.", + "Invite someone using their name, email address, username (like ) or share this room.": "با استفاده از نام، آدرس ایمیل، نام کاربری (مانند ) از فردی دعوت کرده و یا این اتاق را به اشتراک بگذارید.", + "Invite someone using their name, email address, username (like ) or share this space.": "با استفاده از نام ، آدرس ایمیل ، نام کاربری (مانند ) کسی را دعوت کرده یا این فضای کاری را به اشتراک بگذارید.", + "Invite someone using their name, username (like ) or share this space.": "با استفاده از نام یا نام کاربری (مانند ) از افراد دعوت کرده و یا این فضای کاری را به اشتراک بگذارید.", + "Go": "برو", + "This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click here": "این کار آنها را به %(communityName)s دعوت نمی‌کند. برای دعوت افراد به %(communityName)s،اینجا کلیک کنید", + "Start a conversation with someone using their name or username (like ).": "با استفاده از نام یا نام کاربری (مانند )، گفتگوی جدیدی را با دیگران شروع کنید.", + "Start a conversation with someone using their name, email address or username (like ).": "با استفاده از نام، آدرس ایمیل و یا نام کاربری (مانند )، یک گفتگوی جدید را شروع کنید.", + "May include members not in %(communityName)s": "ممکن شامل اعضایی که در %(communityName)s نیستند نیز شود" } From 7de605259eb1a838b7a794f3168534bc06ca4d79 Mon Sep 17 00:00:00 2001 From: Hivaa Date: Tue, 1 Jun 2021 03:47:35 +0000 Subject: [PATCH 296/999] Translated using Weblate (Persian) Currently translated at 100.0% (2974 of 2974 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/fa/ --- src/i18n/strings/fa.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/fa.json b/src/i18n/strings/fa.json index acea50c83e..09780481d8 100644 --- a/src/i18n/strings/fa.json +++ b/src/i18n/strings/fa.json @@ -1455,8 +1455,8 @@ "Room settings": "تنظیمات اتاق", "Share room": "به اشتراک گذاری اتاق", "Show files": "نمایش پرونده ها", - "%(count)s people|one": "%(count)s نفر", - "%(count)s people|other": "%(count)s نفر", + "%(count)s people|one": "نفر %(count)s", + "%(count)s people|other": "نفر %(count)s", "About": "درباره", "Not encrypted": "رمزگذاری نشده", "Add widgets, bridges & bots": "افزودن ابزارک‌ها، پل‌ها و ربات‌ها", From 1ea73ae9ae4c4d1316d9b8f5bbae713a7131bdaf Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 1 Jun 2021 10:18:57 +0100 Subject: [PATCH 297/999] Fix all DMs wrongly appearing in room list when `m.direct` is changed --- src/stores/room-list/RoomListStore.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/stores/room-list/RoomListStore.ts b/src/stores/room-list/RoomListStore.ts index a23401e4c9..b5961f1ac3 100644 --- a/src/stores/room-list/RoomListStore.ts +++ b/src/stores/room-list/RoomListStore.ts @@ -426,8 +426,10 @@ export class RoomListStoreClass extends AsyncStoreWithClient { return; // don't do anything on rooms that aren't visible } - if (cause === RoomUpdateCause.NewRoom && !this.prefilterConditions.every(c => c.isVisible(room))) { - return; // don't do anything on new rooms which ought not to be shown + if ((cause === RoomUpdateCause.NewRoom || cause === RoomUpdateCause.PossibleTagChange) && + !this.prefilterConditions.every(c => c.isVisible(room)) + ) { + return; // don't do anything on new/moved rooms which ought not to be shown } const shouldUpdate = await this.algorithm.handleRoomUpdate(room, cause); From 826efeaeaa54ac6f5cbb1f3f3eb66790b8b065dd Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 1 Jun 2021 10:22:24 +0100 Subject: [PATCH 298/999] Update way of checking for registration disabled Spec says 403 + M_FORBIDDEN --- src/components/structures/auth/Registration.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/auth/Registration.tsx b/src/components/structures/auth/Registration.tsx index 96fb9bdc82..fce7450b8b 100644 --- a/src/components/structures/auth/Registration.tsx +++ b/src/components/structures/auth/Registration.tsx @@ -223,7 +223,7 @@ export default class Registration extends React.Component { this.setState({ flows: e.data.flows, }); - } else if (e.httpStatus === 403 && e.errcode === "M_UNKNOWN") { + } else if (e.httpStatus === 403 || e.errcode === "M_FORBIDDEN") { // At this point registration is pretty much disabled, but before we do that let's // quickly check to see if the server supports SSO instead. If it does, we'll send // the user off to the login page to figure their account out. From b2f01b843861ab46f1dac95e00eafa060da04200 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 1 Jun 2021 10:48:53 +0100 Subject: [PATCH 299/999] Respect newlines in space topics --- res/css/structures/_SpaceRoomView.scss | 1 + src/components/structures/SpaceRoomView.tsx | 10 +++++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/res/css/structures/_SpaceRoomView.scss b/res/css/structures/_SpaceRoomView.scss index 503fe72414..4bc4af467c 100644 --- a/res/css/structures/_SpaceRoomView.scss +++ b/res/css/structures/_SpaceRoomView.scss @@ -328,6 +328,7 @@ $SpaceRoomViewInnerWidth: 428px; font-size: $font-15px; margin-top: 12px; margin-bottom: 16px; + white-space: pre; } > hr { diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx index ea4c75e25c..6ae04ef5e5 100644 --- a/src/components/structures/SpaceRoomView.tsx +++ b/src/components/structures/SpaceRoomView.tsx @@ -417,9 +417,13 @@ const SpaceLanding = ({ space }) => { { inviteButton } { settingsButton }
      -
      - -
      + + {(topic, ref) => ( +
      + { topic } +
      + )} +

      From e525d046c79b9f8143b690c87cea2d4217eaedaa Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 1 Jun 2021 10:49:19 +0100 Subject: [PATCH 300/999] remove outdated TODO --- src/components/structures/SpaceRoomView.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx index 6ae04ef5e5..06d2bda16e 100644 --- a/src/components/structures/SpaceRoomView.tsx +++ b/src/components/structures/SpaceRoomView.tsx @@ -441,7 +441,6 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => { const [error, setError] = useState(""); const numFields = 3; const placeholders = [_t("General"), _t("Random"), _t("Support")]; - // TODO vary default prefills for "Just Me" spaces const [roomNames, setRoomName] = useStateArray(numFields, [_t("General"), _t("Random"), ""]); const fields = new Array(numFields).fill(0).map((_, i) => { const name = "roomName" + i; From f11a7083aeb719b52ec43d8c89911125bd995972 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 1 Jun 2021 11:01:09 +0100 Subject: [PATCH 301/999] Switch to stable endpoint/fields for MSC2858 --- src/Login.ts | 3 ++- src/components/structures/auth/Registration.tsx | 2 +- src/components/views/elements/SSOButtons.tsx | 2 +- test/components/structures/auth/Login-test.js | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Login.ts b/src/Login.ts index d584df7dfe..89f7eb4b82 100644 --- a/src/Login.ts +++ b/src/Login.ts @@ -48,7 +48,8 @@ export interface IIdentityProvider { export interface ISSOFlow { type: "m.login.sso" | "m.login.cas"; - "org.matrix.msc2858.identity_providers": IIdentityProvider[]; // Unstable prefix for MSC2858 + // eslint-disable-next-line camelcase + identity_providers: IIdentityProvider[]; } export type LoginFlow = ISSOFlow | IPasswordFlow; diff --git a/src/components/structures/auth/Registration.tsx b/src/components/structures/auth/Registration.tsx index 96fb9bdc82..d905db9d44 100644 --- a/src/components/structures/auth/Registration.tsx +++ b/src/components/structures/auth/Registration.tsx @@ -467,7 +467,7 @@ export default class Registration extends React.Component { let ssoSection; if (this.state.ssoFlow) { let continueWithSection; - const providers = this.state.ssoFlow["org.matrix.msc2858.identity_providers"] || []; + const providers = this.state.ssoFlow.identity_providers || []; // when there is only a single (or 0) providers we show a wide button with `Continue with X` text if (providers.length > 1) { // i18n: ssoButtons is a placeholder to help translators understand context diff --git a/src/components/views/elements/SSOButtons.tsx b/src/components/views/elements/SSOButtons.tsx index a9eb04d4ec..a531abdf83 100644 --- a/src/components/views/elements/SSOButtons.tsx +++ b/src/components/views/elements/SSOButtons.tsx @@ -112,7 +112,7 @@ interface IProps { const MAX_PER_ROW = 6; const SSOButtons: React.FC = ({matrixClient, flow, loginType, fragmentAfterLogin, primary}) => { - const providers = flow["org.matrix.msc2858.identity_providers"] || []; + const providers = flow.identity_providers || []; if (providers.length < 2) { return
      Date: Tue, 1 Jun 2021 11:04:45 +0100 Subject: [PATCH 302/999] Remove brand prefix too --- src/Login.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Login.ts b/src/Login.ts index 89f7eb4b82..7caab22d88 100644 --- a/src/Login.ts +++ b/src/Login.ts @@ -31,12 +31,12 @@ interface IPasswordFlow { } export enum IdentityProviderBrand { - Gitlab = "org.matrix.gitlab", - Github = "org.matrix.github", - Apple = "org.matrix.apple", - Google = "org.matrix.google", - Facebook = "org.matrix.facebook", - Twitter = "org.matrix.twitter", + Gitlab = "gitlab", + Github = "github", + Apple = "apple", + Google = "google", + Facebook = "facebook", + Twitter = "twitter", } export interface IIdentityProvider { From 6e74ab0cf53a691ea8d34fa133b00725e9defbdb Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 1 Jun 2021 11:11:04 +0100 Subject: [PATCH 303/999] Fix the ability to remove avatar from a space via settings --- src/components/views/dialogs/SpaceSettingsDialog.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/components/views/dialogs/SpaceSettingsDialog.tsx b/src/components/views/dialogs/SpaceSettingsDialog.tsx index 7453ff1d8b..a135b6bc16 100644 --- a/src/components/views/dialogs/SpaceSettingsDialog.tsx +++ b/src/components/views/dialogs/SpaceSettingsDialog.tsx @@ -73,9 +73,13 @@ const SpaceSettingsDialog: React.FC = ({ matrixClient: cli, space, onFin const promises = []; if (avatarChanged) { - promises.push(cli.sendStateEvent(space.roomId, EventType.RoomAvatar, { - url: await cli.uploadContent(newAvatar), - }, "")); + if (newAvatar) { + promises.push(cli.sendStateEvent(space.roomId, EventType.RoomAvatar, { + url: await cli.uploadContent(newAvatar), + }, "")); + } else { + promises.push(cli.sendStateEvent(space.roomId, EventType.RoomAvatar, {}, "")); + } } if (nameChanged) { From ebd7cd6212905862912784add85fa936709a8b65 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Tue, 1 Jun 2021 11:21:59 +0100 Subject: [PATCH 304/999] Add CSS containement rules for shorter reflow operations --- res/css/_common.scss | 1 + res/css/structures/_ContextualMenu.scss | 1 + res/css/structures/_LeftPanel.scss | 2 ++ res/css/structures/_RightPanel.scss | 1 + res/css/structures/_RoomView.scss | 2 ++ res/css/structures/_ScrollPanel.scss | 3 +++ res/css/views/avatars/_DecoratedRoomAvatar.scss | 1 + res/css/views/rooms/_RoomSublist.scss | 1 + res/css/views/rooms/_RoomTile.scss | 4 ++++ 9 files changed, 16 insertions(+) diff --git a/res/css/_common.scss b/res/css/_common.scss index a05ec7eadd..b128a82442 100644 --- a/res/css/_common.scss +++ b/res/css/_common.scss @@ -291,6 +291,7 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus { .mx_Dialog_staticWrapper .mx_Dialog { z-index: 4010; + contain: content; } .mx_Dialog_background { diff --git a/res/css/structures/_ContextualMenu.scss b/res/css/structures/_ContextualMenu.scss index 4b33427a87..d7f2cb76e8 100644 --- a/res/css/structures/_ContextualMenu.scss +++ b/res/css/structures/_ContextualMenu.scss @@ -38,6 +38,7 @@ limitations under the License. position: absolute; font-size: $font-14px; z-index: 5001; + contain: content; } .mx_ContextualMenu_right { diff --git a/res/css/structures/_LeftPanel.scss b/res/css/structures/_LeftPanel.scss index 7c3cd1c513..c7dd678c07 100644 --- a/res/css/structures/_LeftPanel.scss +++ b/res/css/structures/_LeftPanel.scss @@ -25,6 +25,7 @@ $roomListCollapsedWidth: 68px; // Create a row-based flexbox for the GroupFilterPanel and the room list display: flex; + contain: content; .mx_LeftPanel_GroupFilterPanelContainer { flex-grow: 0; @@ -70,6 +71,7 @@ $roomListCollapsedWidth: 68px; // aligned correctly. This is also a row-based flexbox. display: flex; align-items: center; + contain: content; &.mx_IndicatorScrollbar_leftOverflow { mask-image: linear-gradient(90deg, transparent, black 5%); diff --git a/res/css/structures/_RightPanel.scss b/res/css/structures/_RightPanel.scss index 5515fe4060..48f19c1cd5 100644 --- a/res/css/structures/_RightPanel.scss +++ b/res/css/structures/_RightPanel.scss @@ -25,6 +25,7 @@ limitations under the License. padding: 4px 0; box-sizing: border-box; height: 100%; + contain: strict; .mx_RoomView_MessageList { padding: 14px 18px; // top and bottom is 4px smaller to balance with the padding set above diff --git a/res/css/structures/_RoomView.scss b/res/css/structures/_RoomView.scss index cdbe47178d..b019fe7e01 100644 --- a/res/css/structures/_RoomView.scss +++ b/res/css/structures/_RoomView.scss @@ -152,6 +152,7 @@ limitations under the License. flex: 1; display: flex; flex-direction: column; + contain: content; } .mx_RoomView_statusArea { @@ -221,6 +222,7 @@ limitations under the License. .mx_RoomView_MessageList li { clear: both; + contain: content; } li.mx_RoomView_myReadMarker_container { diff --git a/res/css/structures/_ScrollPanel.scss b/res/css/structures/_ScrollPanel.scss index a4e501b339..7b75c69e86 100644 --- a/res/css/structures/_ScrollPanel.scss +++ b/res/css/structures/_ScrollPanel.scss @@ -21,5 +21,8 @@ limitations under the License. display: flex; flex-direction: column; justify-content: flex-end; + + content-visibility: auto; + contain-intrinsic-size: 50px; } } diff --git a/res/css/views/avatars/_DecoratedRoomAvatar.scss b/res/css/views/avatars/_DecoratedRoomAvatar.scss index 2631cbfb40..257b512579 100644 --- a/res/css/views/avatars/_DecoratedRoomAvatar.scss +++ b/res/css/views/avatars/_DecoratedRoomAvatar.scss @@ -16,6 +16,7 @@ limitations under the License. .mx_DecoratedRoomAvatar, .mx_ExtraTile { position: relative; + contain: content; &.mx_DecoratedRoomAvatar_cutout .mx_BaseAvatar { mask-image: url('$(res)/img/element-icons/roomlist/decorated-avatar-mask.svg'); diff --git a/res/css/views/rooms/_RoomSublist.scss b/res/css/views/rooms/_RoomSublist.scss index bae469ab87..146b3edf71 100644 --- a/res/css/views/rooms/_RoomSublist.scss +++ b/res/css/views/rooms/_RoomSublist.scss @@ -198,6 +198,7 @@ limitations under the License. // as the box model should be top aligned. Happens in both FF and Chromium display: flex; flex-direction: column; + align-self: stretch; mask-image: linear-gradient(0deg, transparent, black 4px); } diff --git a/res/css/views/rooms/_RoomTile.scss b/res/css/views/rooms/_RoomTile.scss index 72d29dfd4c..421fbfe39b 100644 --- a/res/css/views/rooms/_RoomTile.scss +++ b/res/css/views/rooms/_RoomTile.scss @@ -19,6 +19,10 @@ limitations under the License. margin-bottom: 4px; padding: 4px; + contain: strict; + height: 40px; + box-sizing: border-box; + // The tile is also a flexbox row itself display: flex; From 308ac505a8be7046c2b58649fb9f6b6a103771ca Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Tue, 1 Jun 2021 14:13:46 +0100 Subject: [PATCH 305/999] Migrate AutoHideScrollbar to TypeScript Also changed the way the React.RefObject is collected --- ...HideScrollbar.js => AutoHideScrollbar.tsx} | 41 ++++++++++--------- .../dialogs/AddExistingToSpaceDialog.tsx | 2 +- 2 files changed, 22 insertions(+), 21 deletions(-) rename src/components/structures/{AutoHideScrollbar.js => AutoHideScrollbar.tsx} (61%) diff --git a/src/components/structures/AutoHideScrollbar.js b/src/components/structures/AutoHideScrollbar.tsx similarity index 61% rename from src/components/structures/AutoHideScrollbar.js rename to src/components/structures/AutoHideScrollbar.tsx index a2dcff2731..4a577a8dbd 100644 --- a/src/components/structures/AutoHideScrollbar.js +++ b/src/components/structures/AutoHideScrollbar.tsx @@ -17,42 +17,43 @@ limitations under the License. import React from "react"; -export default class AutoHideScrollbar extends React.Component { - constructor(props) { - super(props); - this._collectContainerRef = this._collectContainerRef.bind(this); - } +interface IProps { + className?: string; + onScroll?: () => void; + onWheel?: () => void; + style?: React.CSSProperties + tabIndex?: number, + wrappedRef?: (ref: HTMLDivElement) => void; +} + +export default class AutoHideScrollbar extends React.Component { + private containerRef: React.RefObject = React.createRef(); componentDidMount() { - if (this.containerRef && this.props.onScroll) { + if (this.containerRef.current && this.props.onScroll) { // Using the passive option to not block the main thread // https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#improving_scrolling_performance_with_passive_listeners - this.containerRef.addEventListener("scroll", this.props.onScroll, { passive: true }); + this.containerRef.current.addEventListener("scroll", this.props.onScroll, { passive: true }); + } + + if (this.props.wrappedRef) { + this.props.wrappedRef(this.containerRef.current); } } componentWillUnmount() { - if (this.containerRef && this.props.onScroll) { - this.containerRef.removeEventListener("scroll", this.props.onScroll); - } - } - - _collectContainerRef(ref) { - if (ref && !this.containerRef) { - this.containerRef = ref; - } - if (this.props.wrappedRef) { - this.props.wrappedRef(ref); + if (this.containerRef.current && this.props.onScroll) { + this.containerRef.current.removeEventListener("scroll", this.props.onScroll); } } getScrollTop() { - return this.containerRef.scrollTop; + return this.containerRef.current.scrollTop; } render() { return (
      = ({ autoComplete={true} autoFocus={true} /> - + { rooms.length > 0 ? (

      { _t("Rooms") }

      From 73ca6b2ad044523b073f2339cb8d584af45efc78 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Tue, 1 Jun 2021 14:14:02 +0100 Subject: [PATCH 306/999] Add passive flag to Tooltip scroll event listener --- src/components/views/elements/Tooltip.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/components/views/elements/Tooltip.tsx b/src/components/views/elements/Tooltip.tsx index 7e9ce9745c..0202c6b02f 100644 --- a/src/components/views/elements/Tooltip.tsx +++ b/src/components/views/elements/Tooltip.tsx @@ -70,7 +70,10 @@ export default class Tooltip extends React.Component { this.tooltipContainer = document.createElement("div"); this.tooltipContainer.className = "mx_Tooltip_wrapper"; document.body.appendChild(this.tooltipContainer); - window.addEventListener('scroll', this.renderTooltip, true); + window.addEventListener('scroll', this.renderTooltip, { + passive: true, + capture: true, + }); this.parent = ReactDOM.findDOMNode(this).parentNode as Element; @@ -85,7 +88,9 @@ export default class Tooltip extends React.Component { public componentWillUnmount() { ReactDOM.unmountComponentAtNode(this.tooltipContainer); document.body.removeChild(this.tooltipContainer); - window.removeEventListener('scroll', this.renderTooltip, true); + window.removeEventListener('scroll', this.renderTooltip, { + capture: true, + }); } private updatePosition(style: CSSProperties) { From 591314141bb10d19028c3379357587571abe43bd Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Tue, 1 Jun 2021 14:15:42 +0100 Subject: [PATCH 307/999] Add methods visibility for AutoHideScrollbar --- src/components/structures/AutoHideScrollbar.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/structures/AutoHideScrollbar.tsx b/src/components/structures/AutoHideScrollbar.tsx index 4a577a8dbd..66f998b616 100644 --- a/src/components/structures/AutoHideScrollbar.tsx +++ b/src/components/structures/AutoHideScrollbar.tsx @@ -29,7 +29,7 @@ interface IProps { export default class AutoHideScrollbar extends React.Component { private containerRef: React.RefObject = React.createRef(); - componentDidMount() { + public componentDidMount() { if (this.containerRef.current && this.props.onScroll) { // Using the passive option to not block the main thread // https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#improving_scrolling_performance_with_passive_listeners @@ -41,17 +41,17 @@ export default class AutoHideScrollbar extends React.Component { } } - componentWillUnmount() { + public componentWillUnmount() { if (this.containerRef.current && this.props.onScroll) { this.containerRef.current.removeEventListener("scroll", this.props.onScroll); } } - getScrollTop() { + public getScrollTop(): number { return this.containerRef.current.scrollTop; } - render() { + public render() { return (
      Date: Tue, 1 Jun 2021 15:00:59 +0100 Subject: [PATCH 308/999] add comment --- src/components/structures/auth/Registration.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/structures/auth/Registration.tsx b/src/components/structures/auth/Registration.tsx index fce7450b8b..f9361647d7 100644 --- a/src/components/structures/auth/Registration.tsx +++ b/src/components/structures/auth/Registration.tsx @@ -224,6 +224,7 @@ export default class Registration extends React.Component { flows: e.data.flows, }); } else if (e.httpStatus === 403 || e.errcode === "M_FORBIDDEN") { + // Check for 403 or M_FORBIDDEN, Synapse used to send 403 M_UNKNOWN but now sends 403 M_FORBIDDEN. // At this point registration is pretty much disabled, but before we do that let's // quickly check to see if the server supports SSO instead. If it does, we'll send // the user off to the login page to figure their account out. From fc18e21ae1f147c19949923b327b72b767a772ca Mon Sep 17 00:00:00 2001 From: libexus Date: Fri, 28 May 2021 15:58:39 +0000 Subject: [PATCH 309/999] Translated using Weblate (German) Currently translated at 99.3% (2956 of 2974 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/de/ --- src/i18n/strings/de_DE.json | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index e3893cb382..dcc5343af4 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -980,7 +980,7 @@ "Enable Emoji suggestions while typing": "Emojivorschläge während Eingabe", "Show a placeholder for removed messages": "Platzhalter für gelöschte Nachrichten", "Show join/leave messages (invites/kicks/bans unaffected)": "Betreten oder Verlassen von Benutzern (ausgen. Einladungen/Rauswürfe/Banne)", - "Show avatar changes": "Avataränderungen anzeigen", + "Show avatar changes": "Avataränderungen", "Show display name changes": "Änderungen von Anzeigenamen", "Send typing notifications": "Tippbenachrichtigungen senden", "Show avatars in user and room mentions": "Avatare in Benutzer- und Raumerwähnungen", @@ -1200,11 +1200,11 @@ "Scissors": "Schere", "Upgrade to your own domain": "Upgrade zu deiner eigenen Domain", "Accept all %(invitedRooms)s invites": "Akzeptiere alle %(invitedRooms)s Einladungen", - "Change room avatar": "Ändere Raumbild", - "Change room name": "Ändere Raumname", - "Change main address for the room": "Ändere Hauptadresse für den Raum", + "Change room avatar": "Raumbild ändern", + "Change room name": "Raumname ändern", + "Change main address for the room": "Hauptadresse ändern", "Change history visibility": "Sichtbarkeit des Verlaufs ändern", - "Change permissions": "Ändere Berechtigungen", + "Change permissions": "Berechtigungen ändern", "Change topic": "Thema ändern", "Modify widgets": "Widgets bearbeiten", "Default role": "Standard-Rolle", @@ -2378,7 +2378,7 @@ "A connection error occurred while trying to contact the server.": "Beim Versuch, den Server zu kontaktieren, ist ein Verbindungsfehler aufgetreten.", "You might have configured them in a client other than %(brand)s. You cannot tune them in %(brand)s but they still apply.": "Du hast sie ggf. in einem anderen Client als %(brand)s konfiguriert. Du kannst sie nicht in %(brand)s verändern, aber sie werden trotzdem angewandt.", "Master private key:": "Privater Hauptschlüssel:", - "Set the name of a font installed on your system & %(brand)s will attempt to use it.": "Setze den Schriftnamen auf eine in deinem System installierte Schriftart & %(brand)s wird versuchen, sie zu verwenden.", + "Set the name of a font installed on your system & %(brand)s will attempt to use it.": "Setze den Schriftnamen auf eine in deinem System installierte Schriftart und %(brand)s wird versuchen, sie zu verwenden.", "Custom Tag": "Benutzerdefinierter Tag", "You’re already signed in and good to go here, but you can also grab the latest versions of the app on all platforms at element.io/get-started.": "Du bist bereits eingeloggt und kannst loslegen. Allerdings kannst du auch die neuesten Versionen der App für alle Plattformen unter element.io/get-started herunterladen.", "You're all caught up.": "Alles gesichtet.", @@ -2521,7 +2521,7 @@ "Ignored attempt to disable encryption": "Versuch, die Verschlüsselung zu deaktivieren, wurde ignoriert", "Failed to save your profile": "Speichern des Profils fehlgeschlagen", "The operation could not be completed": "Die Operation konnte nicht abgeschlossen werden", - "Remove messages sent by others": "Nachrichten von anderen entfernen", + "Remove messages sent by others": "Nachrichten von anderen löschen", "Starting camera...": "Starte Kamera...", "Call connecting...": "Verbinde den Anruf...", "Calling...": "Rufe an...", @@ -2699,21 +2699,21 @@ "Switzerland": "Schweiz", "Sweden": "Schweden", "Swaziland": "Swasiland", - "Svalbard & Jan Mayen": "Spitzbergen & Jan Mayen", + "Svalbard & Jan Mayen": "Spitzbergen und Jan Mayen", "Suriname": "Surinam", "Sudan": "Sudan", "St. Vincent & Grenadines": "St. Vincent und die Grenadinen", - "St. Pierre & Miquelon": "St. Pierre & Miquelon", + "St. Pierre & Miquelon": "St. Pierre und Miquelon", "St. Martin": "St. Martin", "St. Lucia": "St. Lucia", - "St. Kitts & Nevis": "St. Kitts & Nevis", + "St. Kitts & Nevis": "St. Kitts und Nevis", "St. Helena": "St. Helena", "St. Barthélemy": "St. Barthélemy", "Sri Lanka": "Sri Lanka", "Spain": "Spanien", "South Sudan": "Südsudan", "South Korea": "Südkorea", - "South Georgia & South Sandwich Islands": "Südgeorgien & Südliche Sandwichinseln", + "South Georgia & South Sandwich Islands": "Südgeorgien und Südliche Sandwichinseln", "South Africa": "Südafrika", "Somalia": "Somalia", "Solomon Islands": "Salomonen", @@ -2815,7 +2815,7 @@ "Hungary": "Ungarn", "Hong Kong": "Hongkong", "Honduras": "Honduras", - "Heard & McDonald Islands": "Heard & McDonald-Inseln", + "Heard & McDonald Islands": "Heard und McDonald-Inseln", "Haiti": "Haiti", "Guyana": "Guyana", "Guinea-Bissau": "Guinea-Bissau", From 20a03de6ccf6b33d97faaa21540c4981314eb14c Mon Sep 17 00:00:00 2001 From: Tirifto Date: Sat, 29 May 2021 18:06:51 +0000 Subject: [PATCH 310/999] Translated using Weblate (Esperanto) Currently translated at 99.9% (2972 of 2974 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/eo/ --- src/i18n/strings/eo.json | 95 ++++++++++++++++++++++++++++++++-------- 1 file changed, 76 insertions(+), 19 deletions(-) diff --git a/src/i18n/strings/eo.json b/src/i18n/strings/eo.json index a1979c6699..fc8c00fd19 100644 --- a/src/i18n/strings/eo.json +++ b/src/i18n/strings/eo.json @@ -324,7 +324,7 @@ "Add rooms to this community": "Aldoni ĉambrojn al ĉi tiu komunumo", "An email has been sent to %(emailAddress)s": "Retletero sendiĝis al %(emailAddress)s", "Please check your email to continue registration.": "Bonvolu kontroli vian retpoŝton por daŭrigi la registriĝon.", - "Token incorrect": "Malĝusta ĵetono", + "Token incorrect": "Malĝusta peco", "A text message has been sent to %(msisdn)s": "Tekstmesaĝo sendiĝîs al %(msisdn)s", "Please enter the code it contains:": "Bonvolu enigi la enhavatan kodon:", "Start authentication": "Komenci aŭtentikigon", @@ -769,7 +769,7 @@ "Failed to invite users to the room:": "Malsukcesis inviti uzantojn al la ĉambro:", "Opens the Developer Tools dialog": "Maflermas evoluigistan interagujon", "This homeserver has hit its Monthly Active User limit.": "Tiu ĉi hejmservilo atingis sian monatan limon de aktivaj uzantoj.", - "This homeserver has exceeded one of its resource limits.": "Tiu ĉi hejmservilo superis je unu el siaj risurcaj limoj.", + "This homeserver has exceeded one of its resource limits.": "Tiu ĉi hejmservilo superis je unu el siaj rimedaj limoj.", "Unable to connect to Homeserver. Retrying...": "Ne povas konektiĝi al hejmservilo. Reprovante…", "You do not have permission to invite people to this room.": "Vi ne havas permeson inviti personojn al la ĉambro.", "User %(user_id)s does not exist": "Uzanto %(user_id)s ne ekzistas", @@ -1569,7 +1569,7 @@ "Block users on other matrix homeservers from joining this room (This setting cannot be changed later!)": "Bloki aliĝojn al ĉi tiu ĉambro de uzantoj el aliaj Matrix-serviloj (Ĉi tiun agordon ne eblas poste ŝanĝi!)", "Please fill why you're reporting.": "Bonvolu skribi, kial vi raportas.", "Report Content to Your Homeserver Administrator": "Raporti enhavon al la administrantode via hejmservilo", - "Reporting this message will send its unique 'event ID' to the administrator of your homeserver. If messages in this room are encrypted, your homeserver administrator will not be able to read the message text or view any files or images.": "Per raporto de ĉi tiu mesaĝo vi sendos ĝian unikan « eventan identigilon » al la administranto de via hejmservilo. Se mesaĝoj en ĉi tiu ĉambro estas ĉifrataj, la administranto de via hejmservilo ne povos legi la tekston de la mesaĝo, nek rigardi dosierojn aŭ bildojn.", + "Reporting this message will send its unique 'event ID' to the administrator of your homeserver. If messages in this room are encrypted, your homeserver administrator will not be able to read the message text or view any files or images.": "Per raporto de ĉi tiu mesaĝo vi sendos ĝian unikan «identigilon de okazo» al la administranto de via hejmservilo. Se mesaĝoj en ĉi tiu ĉambro estas ĉifrataj, la administranto de via hejmservilo ne povos legi la tekston de la mesaĝo, nek rigardi dosierojn aŭ bildojn.", "Send report": "Sendi raporton", "Command Help": "Helpo pri komando", "To continue you need to accept the terms of this service.": "Por pluigi, vi devas akcepti la uzokondiĉojn de ĉi tiu servo.", @@ -1653,7 +1653,7 @@ "Unencrypted": "Neĉifrita", "Send a reply…": "Sendi respondon…", "Send a message…": "Sendi mesaĝon…", - "Direct Messages": "Rektaj ĉambroj", + "Direct Messages": "Individuaj ĉambroj", " wants to chat": " volas babili", "Start chatting": "Ekbabili", "Reject & Ignore user": "Rifuzi kaj malatenti uzanton", @@ -1665,7 +1665,7 @@ "Start Verification": "Komenci kontrolon", "Trusted": "Fidata", "Not trusted": "Nefidata", - "Direct message": "Rekta ĉambro", + "Direct message": "Individua ĉambro", "Security": "Sekureco", "Reactions": "Reagoj", "More options": "Pliaj elektebloj", @@ -1919,7 +1919,7 @@ "Failed to find the following users": "Malsukcesis trovi la jenajn uzantojn", "The following users might not exist or are invalid, and cannot be invited: %(csvNames)s": "La jenaj uzantoj eble ne ekzistas aŭ ne validas, kaj ne povas invitiĝi: %(csvNames)s", "Recent Conversations": "Freŝaj interparoloj", - "Recently Direct Messaged": "Freŝaj rektaj ĉambroj", + "Recently Direct Messaged": "Freŝe uzitaj individuaj ĉambroj", "Go": "Iri", "Your account is not secure": "Via konto ne estas sekura", "Your password": "Via pasvorto", @@ -2312,7 +2312,7 @@ "Customise your appearance": "Adaptu vian aspekton", "Appearance Settings only affect this %(brand)s session.": "Agordoj de aspekto nur efikos sur ĉi tiun salutaĵon de %(brand)s.", "Add users and servers you want to ignore here. Use asterisks to have %(brand)s match any characters. For example, @bot:* would ignore all users that have the name 'bot' on any server.": "Aldonu uzantojn kaj servilojn, kiujn vi volas malatenti, ĉi tien. Uzu steletojn por ke %(brand)s atendu iujn ajn signojn. Ekzemple, @bot:* malatentigus ĉiujn uzantojn, kiuj havas la nomon «bot» sur ĉiu ajn servilo.", - "Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.": "La administranto de via servilo malŝaltis implicitan tutvojan ĉifradon en privataj kaj rektaj ĉambroj.", + "Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.": "La administranto de via servilo malŝaltis implicitan tutvojan ĉifradon en privataj kaj individuaj ĉambroj.", "Make this room low priority": "Doni al la ĉambro malaltan prioritaton", "Low priority rooms show up at the bottom of your room list in a dedicated section at the bottom of your room list": "Ĉambroj kun malalta prioritato montriĝas en aparta sekcio, en la suba parto de via ĉambrobreto,", "The authenticity of this encrypted message can't be guaranteed on this device.": "La aŭtentikeco de ĉi tiu ĉifrita mesaĝo ne povas esti garantiita sur ĉi tiu aparato.", @@ -2391,7 +2391,7 @@ "The person who invited you already left the room.": "La persono, kiu vin invitis, jam foriris de la ĉambro.", "The person who invited you already left the room, or their server is offline.": "Aŭ la persono, kiu vin invitis, jam foriris de la ĉambro, aŭ ĝia servilo estas eksterreta.", "Change notification settings": "Ŝanĝi agordojn pri sciigoj", - "Show message previews for reactions in DMs": "Montri antaŭrigardojn al mesaĝoj ĉe reagoj en rektaj ĉambroj", + "Show message previews for reactions in DMs": "Montri antaŭrigardojn al mesaĝoj ĉe reagoj en individuaj ĉambroj", "Show message previews for reactions in all rooms": "Montri antaŭrigardojn al mesaĝoj ĉe reagoj en ĉiuj ĉambroj", "Your server isn't responding to some requests.": "Via servilo ne respondas al iuj petoj.", "Server isn't responding": "Servilo ne respondas", @@ -2729,10 +2729,10 @@ "Send images as you in your active room": "Sendi bildojn kiel vi en via aktiva ĉambro", "Send images as you in this room": "Sendi bildojn kiel vi en ĉi tiu ĉambro", "The %(capability)s capability": "La kapablo %(capability)s", - "See %(eventType)s events posted to your active room": "Vidi eventojn de speco %(eventType)s afiŝitajn al via aktiva ĉambro", - "Send %(eventType)s events as you in your active room": "Sendi eventojn de speco %(eventType)s kiel vi en via aktiva ĉambro", - "See %(eventType)s events posted to this room": "Vidi eventojn de speco %(eventType)s afiŝitajn al ĉi tiu ĉambro", - "Send %(eventType)s events as you in this room": "Sendi eventojn de speco %(eventType)s kiel vi en ĉi tiu ĉambro", + "See %(eventType)s events posted to your active room": "Vidi okazojn de speco %(eventType)s afiŝitajn al via aktiva ĉambro", + "Send %(eventType)s events as you in your active room": "Sendi okazojn de speco %(eventType)s kiel vi en via aktiva ĉambro", + "See %(eventType)s events posted to this room": "Vidi okazojn de speco %(eventType)s afiŝitajn al ĉi tiu ĉambro", + "Send %(eventType)s events as you in this room": "Sendi okazojn de speco %(eventType)s kiel vi en ĉi tiu ĉambro", "See messages posted to your active room": "Vidi mesaĝojn senditajn al via aktiva ĉambro", "See messages posted to this room": "Vidi mesaĝojn senditajn al ĉi tiu ĉambro", "Send messages as you in your active room": "Sendi mesaĝojn kiel vi en via aktiva ĉambro", @@ -3000,14 +3000,14 @@ "Send text messages as you in this room": "Sendi tekstajn mesaĝojn kiel vi en ĉi tiu ĉambro", "Change which room, message, or user you're viewing": "Ŝanĝu, kiun ĉambron, mesaĝon, aŭ uzanton vi rigardas", "%(senderName)s has updated the widget layout": "%(senderName)s ĝisdatigis la aranĝon de la fenestrajoj", - "Converts the DM to a room": "Igas la ĉambron nerekta", - "Converts the room to a DM": "Igas la ĉambron rekta", + "Converts the DM to a room": "Malindividuigas la ĉambron", + "Converts the room to a DM": "Individuigas la ĉambron", "Your homeserver rejected your log in attempt. This could be due to things just taking too long. Please try again. If this continues, please contact your homeserver administrator.": "Via hejmservilo rifuzis vian saluton. Eble tio okazis, ĉar ĝi simple daŭris tro longe. Bonvolu reprovi. Se tio daŭros, bonvolu kontakti la administranton de via hejmservilo.", "Your homeserver was unreachable and was not able to log you in. Please try again. If this continues, please contact your homeserver administrator.": "Via hejmservilo estis neatingebla kaj ne povis vin salutigi. Bonvolu reprovi. Se tio daŭros, bonvolu kontakti la administranton de via hejmservilo.", "Try again": "Reprovu", "We asked the browser to remember which homeserver you use to let you sign in, but unfortunately your browser has forgotten it. Go to the sign in page and try again.": "Ni petis la foliumilon memori, kiun hejmservilon vi uzas por saluti, sed domaĝe, via foliumilo forgesis. Iru al la saluta paĝo kaj reprovu.", "We couldn't log you in": "Ni ne povis salutigi vin", - "%(creator)s created this DM.": "%(creator)s kreis ĉi tiun rektan ĉambron.", + "%(creator)s created this DM.": "%(creator)s kreis ĉi tiun individuan ĉambron.", "Invalid URL": "Nevalida URL", "Unable to validate homeserver": "Ne povas validigi hejmservilon", "Just a heads up, if you don't add an email and forget your password, you could permanently lose access to your account.": "Averte, se vi ne aldonos retpoŝtadreson kaj poste forgesos vian pasvorton, vi eble por ĉiam perdos aliron al via konto.", @@ -3156,8 +3156,8 @@ "Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags. Requires compatible homeserver for some features.": "Pratipo de Aroj. Malkonforma kun Komunumoj, Komunumoj v2, kaj Propraj etikedoj. Bezonas konforman hejmservilon por iuj funkcioj.", "Verify this login to access your encrypted messages and prove to others that this login is really you.": "Kontrolu ĉi tiun saluton por aliri viajn ĉifritajn mesaĝojn, kaj pruvi al aliuloj, ke la salutanto vere estas vi.", "Verify with another session": "Knotroli per alia salutaĵo", - "Original event source": "Originala fonto de evento", - "Decrypted event source": "Malĉifrita fonto de evento", + "Original event source": "Originala fonto de okazo", + "Decrypted event source": "Malĉifrita fonto de okazo", "We'll create rooms for each of them. You can add more later too, including already existing ones.": "Por ĉiu el ili ni kreos ĉambron. Vi povos aldoni pliajn pli poste, inkluzive jam ekzistantajn.", "What projects are you working on?": "Kiujn projektojn vi prilaboras?", "Let's create a room for each of them. You can add more later too, including already existing ones.": "Ni kreu ĉambron por ĉiu el ili. Vi povas aldoni pliajn poste, inkluzive jam ekzistantajn.", @@ -3223,7 +3223,7 @@ "Confirm abort of host creation": "Konfirmu nuligon de kreado de gastiganto", "Feeling experimental? Labs are the best way to get things early, test out new features and help shape them before they actually launch. Learn more.": "Ĉu vi eksperimentemas? Laboratorioj estas la plej bona maniero frue akiri kaj testi novajn funkciojn, kaj helpi ilin formi antaŭ ilia plena ekuzo. Eksciu plion.", "Your access token gives full access to your account. Do not share it with anyone.": "Via alirpeco donas plenan aliron al via konto. Donu ĝin al neniu.", - "We couldn't create your DM.": "Ni ne povis krei vian rektan ĉambron.", + "We couldn't create your DM.": "Ni ne povis krei vian individuan ĉambron.", "You may contact me if you have any follow up questions": "Vi povas min kontakti okaze de pliaj demandoj", "To leave the beta, visit your settings.": "Por foriri de la prova versio, iru al viaj agordoj.", "Your platform and username will be noted to help us use your feedback as much as we can.": "Via platformo kaj uzantonomo helpos al ni pli bone uzi viajn prikomentojn.", @@ -3263,5 +3263,62 @@ "If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "Se vi foriros, %(brand)s estos enlegita sen subteno de Aroj. Komunumoj kaj propraj etikedoj ree estos videblaj.", "Spaces are a new way to group rooms and people.": "Aroj prezentas novan manieron grupigi ĉambrojn kaj homojn.", "See when people join, leave, or are invited to your active room": "Vidu kiam oni aliĝas, foriras, aŭ invitiĝas al via aktiva ĉambro", - "See when people join, leave, or are invited to this room": "Vidu kiam oni aliĝas, foriras, aŭ invitiĝas al la ĉambro" + "See when people join, leave, or are invited to this room": "Vidu kiam oni aliĝas, foriras, aŭ invitiĝas al la ĉambro", + "This homeserver has been blocked by it's administrator.": "Tiu ĉi hejmservilo estas blokita de sia administranto.", + "This homeserver has been blocked by its administrator.": "Tiu ĉi hejmservilo estas blokita de sia administranto.", + "Modal Widget": "Reĝima fenestraĵo", + "Your message wasn't sent because this homeserver has been blocked by it's administrator. Please contact your service administrator to continue using the service.": "Via mesaĝo ne sendiĝis, ĉar ĉi tiu hejmservilo estas blokita de ĝia administranto. Bonvolu kontakti la administranton de via servo por daŭre uzadi la servon.", + "Element Web is experimental on mobile. For a better experience and the latest features, use our free native app.": "Elemento por la reto estas eksperimenta sur telefono. Por pli bona sperto kaj freŝaj funkcioj, uzu nian senpagan malfremdan aplikaĵon.", + "Kick, ban, or invite people to your active room, and make you leave": "Forpeli, forbari, aŭ inviti homojn al via aktiva ĉambro, kaj foririgi vin", + "Kick, ban, or invite people to this room, and make you leave": "Forpeli, forbari, aŭ inviti personojn al la ĉambro, kaj foririgi vin", + "Consult first": "Unue konsulti", + "Continuing temporarily allows the %(hostSignupBrand)s setup process to access your account to fetch verified email addresses. This data is not stored.": "Provizora daŭrigo permesas al la agorda procedo de %(hostSignupBrand)s aliri vian konton por preni kontrolitajn retpoŝtadresojn. Tiuj ĉi datumoj de konserviĝos.", + "Access Token": "Alirpeco", + "Message search initialisation failed": "Malsukcesis komenci serĉadon de mesaĝoj", + "Consulting with %(transferTarget)s. Transfer to %(transferee)s": "Konsultante kun %(transferTarget)s. Transdono al %(transferee)s", + "sends space invaders": "sendas imiton de ludo « Space Invaders »", + "Beta available for web, desktop and Android. Thank you for trying the beta.": "Prova versio disponeblas por reto, labortablo, kaj Androido. Dankon pro via provo.", + "Enter your Security Phrase a second time to confirm it.": "Enigu vian Sekurecan frazon duafoje por ĝin konfirmi.", + "Space Autocomplete": "Memaga finfaro de aro", + "Without verifying, you won’t have access to all your messages and may appear as untrusted to others.": "Sen kontrolo, vi ne povos aliri al ĉiuj viaj mesaĝoj, kaj aliuloj vin povos vidi nefidata.", + "Verify your identity to access encrypted messages and prove your identity to others.": "Kontrolu vian identecon por aliri ĉifritajn mesaĝojn kaj pruvi vian identecon al aliuloj.", + "Use another login": "Uzi alian saluton", + "Please choose a strong password": "Bonvolu elekti fortan pasvorton", + "You can add more later too, including already existing ones.": "Vi povas aldoni pliajn poste, inkluzive tiujn, kiuj jam ekzistas.", + "Let's create a room for each of them.": "Kreu ni ĉambron por ĉiu el ili.", + "What are some things you want to discuss in %(spaceName)s?": "Pri kio volus vi diskuti en %(spaceName)s?", + "This is an experimental feature. For now, new users receiving an invite will have to open the invite on to actually join.": "Ĉi tio estas prova funkcio. Uzantoj, kiuj nun ricevos inviton, devos ĝin malfermi per por efektive aliĝi.", + "Go to my space": "Iri al mia aro", + "Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.": "Elektu aldonotajn ĉambrojn aŭ interparolojn. Ĉi tiu aro estas nur por vi, neniu estos informita. Vi povas aldoni pliajn pli poste.", + "What do you want to organise?": "Kion vi volas organizi?", + "Skip for now": "Preterpasi ĉi-foje", + "To join %(spaceName)s, turn on the Spaces beta": "Por aliĝi al %(spaceName)s, ŝaltu la provan version de Aroj", + "To view %(spaceName)s, turn on the Spaces beta": "Por vidi %(spaceName)s, ŝaltu la provan version de Aroj", + "Spaces are a beta feature.": "Aroj estas prova funkcio.", + "Search names and descriptions": "Serĉi nomojn kaj priskribojn", + "Select a room below first": "Unue elektu ĉambron de sube", + "You can select all or individual messages to retry or delete": "Vi povas elekti ĉiujn aŭ unuopajn mesaĝojn, por reprovi aŭ forigi", + "Sending": "Sendante", + "Retry all": "Reprovi ĉiujn", + "Delete all": "Forigi ĉiujn", + "Some of your messages have not been sent": "Kelkaj viaj mesaĝoj ne sendiĝis", + "Filter all spaces": "Filtri ĉiujn arojn", + "Communities are changing to Spaces": "Komunumoj iĝas Aroj", + "Verification requested": "Kontrolpeto", + "You are the only person here. If you leave, no one will be able to join in the future, including you.": "Vi estas la nura persono tie ĉi. Se vi foriros, neniu alia plu povos aliĝi, inkluzive vin mem.", + "Avatar": "Profilbildo", + "Join the beta": "Aliĝi al provado", + "Leave the beta": "Ĉesi provadon", + "Beta": "Prova", + "Tap for more info": "Klaku por pliaj informoj", + "Spaces is a beta feature": "Aroj estas prova funkcio", + "If you reset everything, you will restart with no trusted sessions, no trusted users, and might not be able to see past messages.": "Se vi restarigos ĉion, vi rekomencos sen fidataj salutaĵoj, uzantoj, kaj eble ne povos vidi antaŭajn mesaĝojn.", + "Only do this if you have no other device to complete verification with.": "Faru tion ĉi nur se vi ne havas alian aparaton, per kiu vi kontrolus ceterajn.", + "Forgotten or lost all recovery methods? Reset all": "Ĉu vi forgesis aŭ perdis ĉiujn manierojn de rehavo? Restarigu ĉion", + "Reset everything": "Restarigi ĉion", + "Verify other login": "Kontroli alian saluton", + "Reset event store": "Restarigi deponejon de okazoj", + "If you do, please note that none of your messages will be deleted, but the search experience might be degraded for a few moments whilst the index is recreated": "Se vi tamen tion faras, sciu ke neniu el viaj mesaĝoj foriĝos, sed via sperto pri serĉado povas malboniĝi momente, dum la indekso estas refarata", + "You most likely do not want to reset your event index store": "Plej probable, vi ne volas restarigi vian deponejon de indeksoj de okazoj", + "Reset event store?": "Ĉu restarigi deponejon de okazoj?" } From 92119afbfcae825ef4cc916b0dd8c853ff9ad15a Mon Sep 17 00:00:00 2001 From: Hivaa Date: Tue, 1 Jun 2021 06:24:03 +0000 Subject: [PATCH 311/999] Translated using Weblate (Persian) Currently translated at 100.0% (2974 of 2974 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/fa/ --- src/i18n/strings/fa.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/strings/fa.json b/src/i18n/strings/fa.json index 09780481d8..46dde79945 100644 --- a/src/i18n/strings/fa.json +++ b/src/i18n/strings/fa.json @@ -73,7 +73,7 @@ "(HTTP status %(httpStatus)s)": "(HTTP وضعیت %(httpStatus)s)", "Failed to forget room %(errCode)s": "فراموش کردن اتاق با خطا مواجه شد %(errCode)s", "Wednesday": "چهارشنبه", - "Quote": "گفتآورد", + "Quote": "نقل قول", "Send": "ارسال", "Error": "خطا", "Send logs": "ارسال گزارش‌ها", From be55864c5ed374fb3a4964594469a7824e775caa Mon Sep 17 00:00:00 2001 From: Trendyne Date: Mon, 31 May 2021 12:22:27 +0000 Subject: [PATCH 312/999] Translated using Weblate (Icelandic) Currently translated at 22.1% (660 of 2974 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/is/ --- src/i18n/strings/is.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/is.json b/src/i18n/strings/is.json index 3695075cc5..35f5342b30 100644 --- a/src/i18n/strings/is.json +++ b/src/i18n/strings/is.json @@ -719,5 +719,6 @@ "Show less": "Sýna minna", "%(count)s messages deleted.|one": "%(count)s skilaboð eytt.", "%(count)s messages deleted.|other": "%(count)s skilaboðum eytt.", - "Message deleted on %(date)s": "Skilaboð eytt á %(date)s" + "Message deleted on %(date)s": "Skilaboð eytt á %(date)s", + "Message edits": "Skilaboðs breytingar" } From 40619b83e0ac59e3ce859937f0fdde621778c337 Mon Sep 17 00:00:00 2001 From: iaiz Date: Fri, 28 May 2021 12:20:47 +0000 Subject: [PATCH 313/999] Translated using Weblate (Spanish) Currently translated at 100.0% (2974 of 2974 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/es/ --- src/i18n/strings/es.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/es.json b/src/i18n/strings/es.json index 8db9e9a6e0..60f5d06bec 100644 --- a/src/i18n/strings/es.json +++ b/src/i18n/strings/es.json @@ -3310,5 +3310,10 @@ "Go to my space": "Ir a mi espacio", "sends space invaders": "enviar space invaders", "Sends the given message with a space themed effect": "Envía un mensaje con efectos espaciales", - "If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "Si sales, %(brand)s volverá a cargarse con los espacios desactivados. Las comunidades y las etiquetas personalizadas serán visibles de nuevo." + "If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "Si sales, %(brand)s volverá a cargarse con los espacios desactivados. Las comunidades y las etiquetas personalizadas serán visibles de nuevo.", + "Allow Peer-to-Peer for 1:1 calls (if you enable this, the other party might be able to see your IP address)": "Permitir conexión directa (peer-to-peer) en las llamadas individuales (si lo activas, la otra parte podría ver tu dirección IP)", + "See when people join, leave, or are invited to your active room": "Ver cuando alguien se una, salga o se le invite a tu sala activa", + "Kick, ban, or invite people to this room, and make you leave": "Expulsar, vetar o invitar personas a esta sala, y hacerte salir de ella", + "Kick, ban, or invite people to your active room, and make you leave": "Expulsar, vetar o invitar a gente a tu sala activa, o hacerte salir", + "See when people join, leave, or are invited to this room": "Ver cuando alguien se une, sale o se le invita a la sala" } From 352ca1d270b586b9df3e76d3a7da38b81f020878 Mon Sep 17 00:00:00 2001 From: Percy Date: Fri, 28 May 2021 09:48:31 +0000 Subject: [PATCH 314/999] Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (2974 of 2974 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/zh_Hant/ --- src/i18n/strings/zh_Hant.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json index 4092ed067b..5c27fb3878 100644 --- a/src/i18n/strings/zh_Hant.json +++ b/src/i18n/strings/zh_Hant.json @@ -169,9 +169,9 @@ "No Webcams detected": "未偵測到網路攝影機", "No media permissions": "沒有媒體權限", "You may need to manually permit %(brand)s to access your microphone/webcam": "您可能需要手動允許 %(brand)s 存取您的麥克風/網路攝影機", - "Are you sure you want to leave the room '%(roomName)s'?": "您確定您要想要離開房間 '%(roomName)s' 嗎?", + "Are you sure you want to leave the room '%(roomName)s'?": "你確定你要想要離開房間 '%(roomName)s' 嗎?", "Bans user with given id": "阻擋指定 ID 的使用者", - "Can't connect to homeserver - please check your connectivity, ensure your homeserver's SSL certificate is trusted, and that a browser extension is not blocking requests.": "無法連線到家伺服器 - 請檢查您的連線,確保您的家伺服器的 SSL 憑證可被信任,而瀏覽器擴充套件也沒有阻擋請求。", + "Can't connect to homeserver - please check your connectivity, ensure your homeserver's SSL certificate is trusted, and that a browser extension is not blocking requests.": "無法連線到家伺服器 - 請檢查你的連線,確保你的家伺服器的 SSL 憑證可被信任,而瀏覽器擴充套件也沒有阻擋請求。", "%(senderName)s changed their profile picture.": "%(senderName)s 已經變更了他的基本資料圖片。", "%(senderName)s changed the power level of %(powerLevelDiffText)s.": "%(senderName)s 變更了 %(powerLevelDiffText)s 權限等級。", "%(senderDisplayName)s changed the room name to %(roomName)s.": "%(senderDisplayName)s 將聊天室名稱變更為 %(roomName)s。", @@ -838,7 +838,7 @@ "An error ocurred whilst trying to remove the widget from the room": "嘗試從聊天室移除小工具時發生錯誤", "System Alerts": "系統警告", "Only room administrators will see this warning": "僅聊天室管理員會看到此警告", - "Please contact your service administrator to continue using the service.": "請聯絡您的服務管理員以繼續使用服務。", + "Please contact your service administrator to continue using the service.": "請聯絡你的服務管理員以繼續使用服務。", "This homeserver has hit its Monthly Active User limit.": "這個主伺服器已經到達其每月活躍使用者限制。", "This homeserver has exceeded one of its resource limits.": "此主伺服器已經超過其中一項資源限制。", "Upgrade Room Version": "更新聊天室版本", @@ -1160,7 +1160,7 @@ "Change": "變更", "Couldn't load page": "無法載入頁面", "This homeserver does not support communities": "此家伺服器不支援社群", - "A verification email will be sent to your inbox to confirm setting your new password.": "一封驗證用的電子郵件已經傳送到您的收件匣以確認您設定了新密碼。", + "A verification email will be sent to your inbox to confirm setting your new password.": "一封驗證用的電子郵件已經傳送到你的收件匣以確認你設定了新密碼。", "Your password has been reset.": "您的密碼已重設。", "This homeserver does not support login using email address.": "此家伺服器不支援使用電子郵件地址登入。", "Registration has been disabled on this homeserver.": "註冊已在此家伺服器上停用。", From e69c57ae94d6a5459855a584a0fd79e658932a14 Mon Sep 17 00:00:00 2001 From: random Date: Fri, 28 May 2021 13:52:20 +0000 Subject: [PATCH 315/999] Translated using Weblate (Italian) Currently translated at 100.0% (2974 of 2974 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/it/ --- src/i18n/strings/it.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/it.json b/src/i18n/strings/it.json index d109837bb1..585ee8ba3a 100644 --- a/src/i18n/strings/it.json +++ b/src/i18n/strings/it.json @@ -3367,5 +3367,13 @@ "Send and receive voice messages": "Invia e ricevi messaggi vocali", "Your feedback will help make spaces better. The more detail you can go into, the better.": "La tua opinione aiuterà a migliorare gli spazi. Più dettagli dai, meglio è.", "If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "Se esci, %(brand)s si ricaricherà con gli spazi disattivati. Le comunità e le etichette personalizzate saranno di nuovo visibili.", - "Message search initialisation failed": "Inizializzazione ricerca messaggi fallita" + "Message search initialisation failed": "Inizializzazione ricerca messaggi fallita", + "Space Autocomplete": "Autocompletamento spazio", + "Go to my space": "Vai nel mio spazio", + "sends space invaders": "invia space invaders", + "Sends the given message with a space themed effect": "Invia il messaggio con un effetto a tema spaziale", + "Kick, ban, or invite people to your active room, and make you leave": "Buttare fuori, bandire o invitare persone nella tua stanza attiva e farti uscire", + "See when people join, leave, or are invited to this room": "Vedere quando le persone entrano, escono o sono invitate in questa stanza", + "Kick, ban, or invite people to this room, and make you leave": "Buttare fuori, bandire o invitare persone in questa stanza e farti uscire", + "See when people join, leave, or are invited to your active room": "Vedere quando le persone entrano, escono o sono invitate nella tua stanza attiva" } From 31163df863b8be24fc683e86c75aa438958c0883 Mon Sep 17 00:00:00 2001 From: Percy Date: Sat, 29 May 2021 09:20:08 +0000 Subject: [PATCH 316/999] Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (2974 of 2974 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/zh_Hans/ --- src/i18n/strings/zh_Hans.json | 1157 ++++++++++++++++++++------------- 1 file changed, 709 insertions(+), 448 deletions(-) diff --git a/src/i18n/strings/zh_Hans.json b/src/i18n/strings/zh_Hans.json index 6afe74dbee..af02a40587 100644 --- a/src/i18n/strings/zh_Hans.json +++ b/src/i18n/strings/zh_Hans.json @@ -51,7 +51,7 @@ "Invalid Email Address": "邮箱地址格式错误", "Invalid file%(extra)s": "无效文件%(extra)s", "Return to login screen": "返回登录页面", - "%(brand)s does not have permission to send you notifications - please check your browser settings": "%(brand)s 没有通知发送权限 - 请检查您的浏览器设置", + "%(brand)s does not have permission to send you notifications - please check your browser settings": "%(brand)s 没有通知发送权限 - 请检查你的浏览器设置", "%(brand)s was not given permission to send notifications - please try again": "%(brand)s 没有通知发送权限 - 请重试", "%(brand)s version:": "%(brand)s 版本:", "Room %(roomId)s not visible": "聊天室 %(roomId)s 已隐藏", @@ -65,7 +65,7 @@ "%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.": "%(senderName)s 向 %(targetDisplayName)s 发了加入聊天室的邀请。", "Server error": "服务器错误", "Server may be unavailable, overloaded, or search timed out :(": "服务器可能不可用、超载,或者搜索超时 :(", - "Server may be unavailable, overloaded, or you hit a bug.": "当前服务器可能处于不可用或过载状态,或者您遇到了一个 bug。", + "Server may be unavailable, overloaded, or you hit a bug.": "当前服务器可能处于不可用或过载状态,或者你遇到了一个 bug。", "Server unavailable, overloaded, or something else went wrong.": "服务器可能不可用、超载,或者其他东西出错了.", "Session ID": "会话 ID", "%(senderName)s set a profile picture.": "%(senderName)s 设置了头像。", @@ -272,7 +272,7 @@ "Upload file": "上传文件", "Usage": "用法", "Who can read history?": "谁可以阅读历史消息?", - "You are not in this room.": "您不在此聊天室中。", + "You are not in this room.": "你不在此聊天室中。", "You have no visible notifications": "没有可见的通知", "Not a valid %(brand)s keyfile": "不是有效的 %(brand)s 密钥文件", "%(targetName)s accepted an invitation.": "%(targetName)s 已接受邀请。", @@ -310,11 +310,11 @@ "(no answer)": "(无回复)", "Who can access this room?": "谁有权访问此聊天室?", "You are already in a call.": "您正在通话。", - "You do not have permission to do that in this room.": "您没有进行此操作的权限。", + "You do not have permission to do that in this room.": "你没有进行此操作的权限。", "You cannot place VoIP calls in this browser.": "无法在此浏览器中发起 VoIP 通话。", - "You do not have permission to post to this room": "您没有在此聊天室发送消息的权限", - "You seem to be in a call, are you sure you want to quit?": "您似乎正在进行通话,确定要退出吗?", - "You seem to be uploading files, are you sure you want to quit?": "您似乎正在上传文件,确定要退出吗?", + "You do not have permission to post to this room": "你没有在此聊天室发送消息的权限", + "You seem to be in a call, are you sure you want to quit?": "你似乎正在进行通话,确定要退出吗?", + "You seem to be uploading files, are you sure you want to quit?": "你似乎正在上传文件,确定要退出吗?", "Upload an avatar:": "上传头像:", "An error occurred: %(error_string)s": "发生了一个错误: %(error_string)s", "There are no visible files in this room": "此聊天室中没有可见的文件", @@ -327,13 +327,13 @@ "%(senderDisplayName)s removed the room avatar.": "%(senderDisplayName)s 移除了聊天室头像。", "Something went wrong!": "出了点问题!", "If you already have a Matrix account you can log in instead.": "若您已经拥有 Matrix 帐号,您也可以 登录。", - "Do you want to set an email address?": "您想要设置一个邮箱地址吗?", + "Do you want to set an email address?": "你想要设置一个邮箱地址吗?", "Upload new:": "上传新的:", "Username invalid: %(errMessage)s": "用户名无效: %(errMessage)s", "Verification Pending": "验证等待中", "(unknown failure: %(reason)s)": "(未知错误:%(reason)s)", "%(senderName)s withdrew %(targetName)s's invitation.": "%(senderName)s 收回了 %(targetName)s 的邀请。", - "You cannot place a call with yourself.": "您无法向自己发起通话。", + "You cannot place a call with yourself.": "你无法向自己发起通话。", "You have disabled URL previews by default.": "你已经默认禁用链接预览。", "You have enabled URL previews by default.": "你已经默认启用链接预览。", "Set a display name:": "设置昵称:", @@ -385,10 +385,10 @@ "%(widgetName)s widget removed by %(senderName)s": "%(senderName)s 移除了 %(widgetName)s 挂件", "%(widgetName)s widget modified by %(senderName)s": "%(senderName)s 修改了 %(widgetName)s 挂件", "Unpin Message": "取消置顶消息", - "Add rooms to this community": "添加聊天室到此社区", + "Add rooms to this community": "添加聊天室到此社群", "Call Failed": "呼叫失败", - "Invite new community members": "邀请新社区成员", - "Invite to Community": "邀请到社区", + "Invite new community members": "邀请新社群成员", + "Invite to Community": "邀请到社群", "Ignored user": "已忽略的用户", "You are now ignoring %(userId)s": "你忽略了 %(userId)s", "Unignored user": "未忽略的用户", @@ -450,28 +450,28 @@ "collapse": "折叠", "expand": "展开", "email address": "邮箱地址", - "You have entered an invalid address.": "您输入了无效的地址。", + "You have entered an invalid address.": "你输入了无效的地址。", "Leave": "退出", "Description": "描述", "Warning": "警告", "Room Notification": "聊天室通知", - "The platform you're on": "您使用的平台是", + "The platform you're on": "你使用的平台是", "The version of %(brand)s": "%(brand)s 版本", - "Your language of choice": "您选择的语言是", - "Whether or not you're using the Richtext mode of the Rich Text Editor": "您是否正在使用富文本编辑器的富文本模式", - "Your homeserver's URL": "您的主服务器的链接", + "Your language of choice": "你选择的语言是", + "Whether or not you're using the Richtext mode of the Rich Text Editor": "你是否正在使用富文本编辑器的富文本模式", + "Your homeserver's URL": "你的主服务器的链接", "The information being sent to us to help make %(brand)s better includes:": "正在给我们发送信息以帮助 %(brand)s:", - "Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "此页面中含有可用于识别您身份的信息,比如聊天室、用户或群组 ID,这些数据会在发送到服务器前被移除。", + "Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "此页面中含有可用于识别你身份的信息,比如聊天室、用户或群组 ID,这些数据会在发送到服务器前被移除。", "%(weekDayName)s %(time)s": "%(weekDayName)s %(time)s", "%(weekDayName)s, %(monthName)s %(day)s %(time)s": "%(monthName)s %(day)s %(time)s, %(weekDayName)s", "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s": "%(fullYear)s %(monthName)s %(day)s, %(weekDayName)s", "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s": "%(fullYear)s %(monthName)s %(day)s %(time)s, %(weekDayName)s", - "Who would you like to add to this community?": "您想把谁添加至此社区中?", - "Warning: any person you add to a community will be publicly visible to anyone who knows the community ID": "警告:您添加的一切用户都将会对一切知道此社区的 ID 的人公开", - "Which rooms would you like to add to this community?": "您想把哪个聊天室添加至此社区中?", - "Add rooms to the community": "添加聊天室到社区", - "Add to community": "添加到社区", - "Failed to invite users to community": "邀请用户到社区失败", + "Who would you like to add to this community?": "你想把谁添加至此社群中?", + "Warning: any person you add to a community will be publicly visible to anyone who knows the community ID": "警告:你添加的一切用户都将会对一切知道此社群的 ID 的人公开", + "Which rooms would you like to add to this community?": "你想把哪个聊天室添加至此社群中?", + "Add rooms to the community": "添加聊天室到社群", + "Add to community": "添加到社群", + "Failed to invite users to community": "邀请用户到社群失败", "Enable inline URL previews by default": "默认启用链接预览", "Disinvite this user?": "是否不再邀请此用户?", "Kick this user?": "是否移除此用户?", @@ -485,24 +485,24 @@ "Members only (since the point in time of selecting this option)": "仅成员(从选中此选项时开始)", "Members only (since they were invited)": "只有成员(从他们被邀请开始)", "Members only (since they joined)": "只有成员(从他们加入开始)", - "Invalid community ID": "无效的社区 ID", - "Create Community": "创建社区", - "Community Name": "社区名称", - "Community ID": "社区 ID", + "Invalid community ID": "无效的社群 ID", + "Create Community": "创建社群", + "Community Name": "社群名称", + "Community ID": "社群 ID", "example": "示例", "Add a Room": "添加聊天室", "Add a User": "添加用户", "Unable to accept invite": "无法接受邀请", "Unable to reject invite": "无法拒绝邀请", - "Leave Community": "退出社区", - "Community Settings": "社区设置", - "Community %(groupId)s not found": "找不到社区 %(groupId)s", - "Your Communities": "我的社区", + "Leave Community": "退出社群", + "Community Settings": "社群设置", + "Community %(groupId)s not found": "找不到社群 %(groupId)s", + "Your Communities": "我的社群", "Failed to set direct chat tag": "无法设定私聊标签", "Failed to remove tag %(tagName)s from room": "移除聊天室标签 %(tagName)s 失败", "Failed to add tag %(tagName)s to room": "无法为聊天室新增标签 %(tagName)s", "Submit debug logs": "提交调试日志", - "Show these rooms to non-members on the community page and room list?": "在社区页面与聊天室列表上对非社区成员显示这些聊天室?", + "Show these rooms to non-members on the community page and room list?": "在社群页面与聊天室列表上对非社群成员显示这些聊天室?", "Failed to invite users to %(groupId)s": "邀请用户到 %(groupId)s 失败", "Failed to invite the following users to %(groupId)s:": "邀请下列用户到 %(groupId)s 失败:", "Failed to add the following rooms to %(groupId)s:": "添加以下聊天室到 %(groupId)s 失败:", @@ -511,10 +511,10 @@ "To use it, just wait for autocomplete results to load and tab through them.": "若要使用自动补全,只要等待自动补全结果加载完成,按 Tab 键切换即可。", "%(oldDisplayName)s changed their display name to %(displayName)s.": "%(oldDisplayName)s 将他们的昵称修改成了 %(displayName)s 。", "Stickerpack": "贴图集", - "You don't currently have any stickerpacks enabled": "您目前没有启用任何贴图集", + "You don't currently have any stickerpacks enabled": "你目前没有启用任何贴图集", "Key request sent.": "已发送密钥共享请求。", - "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.": "如果您是房间中最后一位有权限的用户,在您降低自己的权限等级后将无法撤回此修改,因为你将无法重新获得权限。", - "You will not be able to undo this change as you are promoting the user to have the same power level as yourself.": "您将无法撤回此修改,因为您正在将此用户的滥权等级提升至与你相同。", + "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.": "如果你是房间中最后一位有权限的用户,在你降低自己的权限等级后将无法撤回此修改,因为你将无法重新获得权限。", + "You will not be able to undo this change as you are promoting the user to have the same power level as yourself.": "你将无法撤回此修改,因为你正在将此用户的滥权等级提升至与你相同。", "Unmute": "取消静音", "%(userName)s (power %(powerLevelNumber)s)": "%(userName)s(滥权等级 %(powerLevelNumber)s)", "Hide Stickers": "隐藏贴图", @@ -528,23 +528,23 @@ "Offline for %(duration)s": "已离线 %(duration)s", "Unknown for %(duration)s": "未知状态已持续 %(duration)s", "Seen by %(displayName)s (%(userName)s) at %(dateTime)s": "%(displayName)s (%(userName)s) 在 %(dateTime)s 看到这里", - "'%(groupId)s' is not a valid community ID": "“%(groupId)s” 不是有效的社区 ID", + "'%(groupId)s' is not a valid community ID": "“%(groupId)s” 不是有效的社群 ID", "Flair": "个性徽章", "Code": "代码", - "Remove from community": "从社区中移除", - "Disinvite this user from community?": "是否不再邀请此用户加入本社区?", - "Remove this user from community?": "是否要从社区中移除此用户?", + "Remove from community": "从社群中移除", + "Disinvite this user from community?": "是否不再邀请此用户加入本社群?", + "Remove this user from community?": "是否要从社群中移除此用户?", "Failed to withdraw invitation": "撤回邀请失败", "Failed to remove user from community": "移除用户失败", - "Filter community members": "过滤社区成员", + "Filter community members": "过滤社群成员", "Are you sure you want to remove '%(roomName)s' from %(groupId)s?": "你确定要从 %(groupId)s 中移除 %(roomName)s 吗?", - "Removing a room from the community will also remove it from the community page.": "从社区中移除房间时,同时也会将其从社区页面中移除。", - "Failed to remove room from community": "从社区中移除聊天室失败", + "Removing a room from the community will also remove it from the community page.": "从社群中移除房间时,同时也会将其从社群页面中移除。", + "Failed to remove room from community": "从社群中移除聊天室失败", "Failed to remove '%(roomName)s' from %(groupId)s": "从 %(groupId)s 中移除 “%(roomName)s” 失败", - "Only visible to community members": "仅对社区成员可见", - "Filter community rooms": "过滤社区聊天室", - "You're not currently a member of any communities.": "您目前不是任何一个社区的成员。", - "Communities": "社区", + "Only visible to community members": "仅对社群成员可见", + "Filter community rooms": "过滤社群聊天室", + "You're not currently a member of any communities.": "你目前不是任何一个社群的成员。", + "Communities": "社群", "%(nameList)s %(transitionList)s": "%(nameList)s %(transitionList)s", "%(severalUsers)sjoined %(count)s times|other": "%(severalUsers)s 已加入 %(count)s 次", "%(severalUsers)sjoined %(count)s times|one": "%(severalUsers)s 已加入", @@ -571,13 +571,13 @@ "%(oneUser)shad their invitation withdrawn %(count)s times|other": "%(oneUser)s 撤回了他们的邀请共 %(count)s 次", "%(oneUser)shad their invitation withdrawn %(count)s times|one": "%(oneUser)s 撤回了他们的邀请", "In reply to ": "回复给 ", - "Community IDs cannot be empty.": "社区 ID 不能为空。", - "Community IDs may only contain characters a-z, 0-9, or '=_-./'": "社区 ID 只能包含 a-z、0-9 或 “=_-./” 等字符", - "Something went wrong whilst creating your community": "创建社区时出现问题", - "If you have previously used a more recent version of %(brand)s, your session may be incompatible with this version. Close this window and return to the more recent version.": "如果您之前使用过较新版本的 %(brand)s,则您的会话可能与当前版本不兼容。请关闭此窗口并使用最新版本。", - "Showing flair for these communities:": "显示这些社区的个性徽章:", - "This room is not showing flair for any communities": "此聊天室没有显示任何社区的个性徽章", - "New community ID (e.g. +foo:%(localDomain)s)": "新社区 ID(例子:+foo:%(localDomain)s)", + "Community IDs cannot be empty.": "社群 ID 不能为空。", + "Community IDs may only contain characters a-z, 0-9, or '=_-./'": "社群 ID 只能包含 a-z、0-9 或 “=_-./” 等字符", + "Something went wrong whilst creating your community": "创建社群时出现问题", + "If you have previously used a more recent version of %(brand)s, your session may be incompatible with this version. Close this window and return to the more recent version.": "如果你之前使用过较新版本的 %(brand)s,则你的会话可能与当前版本不兼容。请关闭此窗口并使用最新版本。", + "Showing flair for these communities:": "显示这些社群的个性徽章:", + "This room is not showing flair for any communities": "此聊天室没有显示任何社群的个性徽章", + "New community ID (e.g. +foo:%(localDomain)s)": "新社群 ID(例子:+foo:%(localDomain)s)", "URL previews are enabled by default for participants in this room.": "此聊天室默认启用链接预览。", "URL previews are disabled by default for participants in this room.": "此聊天室默认禁用链接预览。", "%(senderDisplayName)s changed the room avatar to ": "%(senderDisplayName)s 将聊天室的头像更改为 ", @@ -585,56 +585,56 @@ "Matrix ID": "Matrix ID", "Matrix Room ID": "Matrix 聊天室 ID", "

      HTML for your community's page

      \n

      \n Use the long description to introduce new members to the community, or distribute\n some important links\n

      \n

      \n You can even use 'img' tags\n

      \n": "", - "Add rooms to the community summary": "将聊天室添加到社区简介中", - "Which rooms would you like to add to this summary?": "您想要将哪个聊天室添加到社区简介?", + "Add rooms to the community summary": "将聊天室添加到社群简介中", + "Which rooms would you like to add to this summary?": "你想要将哪个聊天室添加到社群简介?", "Add to summary": "添加到简介", "Failed to add the following rooms to the summary of %(groupId)s:": "添加以下聊天室到 %(groupId)s 的简介中时失败:", "Failed to remove the room from the summary of %(groupId)s": "从 %(groupId)s 的简介中移除此聊天室时失败", - "The room '%(roomName)s' could not be removed from the summary.": "聊天室 “%(roomName)s” 无法从社区简介中移除。", - "Failed to update community": "更新社区简介失败", - "Unable to leave community": "无法退出社区", + "The room '%(roomName)s' could not be removed from the summary.": "聊天室 “%(roomName)s” 无法从社群简介中移除。", + "Failed to update community": "更新社群简介失败", + "Unable to leave community": "无法退出社群", "Leave %(groupName)s?": "退出 %(groupName)s?", "Featured Rooms:": "核心聊天室:", "Featured Users:": "核心用户:", - "Join this community": "加入此社区", - "%(inviter)s has invited you to join this community": "%(inviter)s 邀请您加入此社区", + "Join this community": "加入此社群", + "%(inviter)s has invited you to join this community": "%(inviter)s 邀请你加入此社群", "Failed to add the following users to the summary of %(groupId)s:": "将下列用户添加至 %(groupId)s 的简介中时失败:", "Failed to remove a user from the summary of %(groupId)s": "从 %(groupId)s 的简介中移除用户时失败", - "You are an administrator of this community": "你是此社区的管理员", - "You are a member of this community": "你是此社区的成员", - "Who can join this community?": "谁可以加入此社区?", + "You are an administrator of this community": "你是此社群的管理员", + "You are a member of this community": "你是此社群的成员", + "Who can join this community?": "谁可以加入此社群?", "Everyone": "所有人", - "Leave this community": "退出此社区", + "Leave this community": "退出此社群", "Long Description (HTML)": "长描述(HTML)", "Old cryptography data detected": "检测到旧的加密数据", - "Data from an older version of %(brand)s has been detected. This will have caused end-to-end cryptography to malfunction in the older version. End-to-end encrypted messages exchanged recently whilst using the older version may not be decryptable in this version. This may also cause messages exchanged with this version to fail. If you experience problems, log out and back in again. To retain message history, export and re-import your keys.": "已检测到旧版%(brand)s的数据,这将导致端到端加密在旧版本中发生故障。在此版本中,使用旧版本交换的端对端加密消息可能无法解密。这也可能导致与此版本交换的消息失败。如果您遇到问题,请退出并重新登录。要保留历史消息,请先导出并在重新登录后导入您的密钥。", - "Did you know: you can use communities to filter your %(brand)s experience!": "你知道吗:你可以将社区用作过滤器以增强你的 %(brand)s 使用体验!", - "Create a new community": "创建新社区", - "Error whilst fetching joined communities": "获取已加入社区列表时出现错误", - "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.": "创建社区,将用户与聊天室整合在一起!搭建自定义社区主页以在 Matrix 宇宙之中标出您的私人空间。", + "Data from an older version of %(brand)s has been detected. This will have caused end-to-end cryptography to malfunction in the older version. End-to-end encrypted messages exchanged recently whilst using the older version may not be decryptable in this version. This may also cause messages exchanged with this version to fail. If you experience problems, log out and back in again. To retain message history, export and re-import your keys.": "已检测到旧版%(brand)s的数据,这将导致端到端加密在旧版本中发生故障。在此版本中,使用旧版本交换的端对端加密消息可能无法解密。这也可能导致与此版本交换的消息失败。如果你遇到问题,请退出并重新登录。要保留历史消息,请先导出并在重新登录后导入你的密钥。", + "Did you know: you can use communities to filter your %(brand)s experience!": "你知道吗:你可以将社群用作过滤器以增强你的 %(brand)s 使用体验!", + "Create a new community": "创建新社群", + "Error whilst fetching joined communities": "获取已加入社群列表时出现错误", + "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.": "创建社群,将用户与聊天室整合在一起!搭建自定义社群主页以在 Matrix 宇宙之中标出你的私人空间。", "%(count)s of your messages have not been sent.|one": "您的消息尚未发送。", "Uploading %(filename)s and %(count)s others|other": "正在上传 %(filename)s 与其他 %(count)s 个文件", "Uploading %(filename)s and %(count)s others|zero": "正在上传 %(filename)s", "Uploading %(filename)s and %(count)s others|one": "正在上传 %(filename)s 与其他 %(count)s 个文件", "Privacy is important to us, so we don't collect any personal or identifiable data for our analytics.": "隐私对我们而言重要至极,所以我们不会在分析统计服务中收集任何个人信息或者可用于识别身份的数据。", "Learn more about how we use analytics.": "进一步了解我们如何使用分析统计服务。", - "Please note you are logging into the %(hs)s server, not matrix.org.": "请注意,您正在登录 %(hs)s,而非 matrix.org。", + "Please note you are logging into the %(hs)s server, not matrix.org.": "请注意,你正在登录 %(hs)s,而非 matrix.org。", "This homeserver doesn't offer any login flows which are supported by this client.": "此主服务器不兼容本客户端支持的任何登录方式。", "Opens the Developer Tools dialog": "打开开发者工具窗口", "Notify the whole room": "通知聊天室全体成员", - "This process allows you to export the keys for messages you have received in encrypted rooms to a local file. You will then be able to import the file into another Matrix client in the future, so that client will also be able to decrypt these messages.": "此操作允许您将加密聊天室中收到的消息的密钥导出为本地文件。您可以将文件导入其他 Matrix 客户端,以便让别的客户端在未收到密钥的情况下解密这些消息。", - "The exported file will allow anyone who can read it to decrypt any encrypted messages that you can see, so you should be careful to keep it secure. To help with this, you should enter a passphrase below, which will be used to encrypt the exported data. It will only be possible to import the data by using the same passphrase.": "导出的文件将允许任何可以读取它的人解密任何他们可以看到的加密消息,因此,您应该小心对待,以确保其安全。为解决此问题,您应当在下面输入密语以加密导出的数据。只有输入相同的密语才能导入数据。", + "This process allows you to export the keys for messages you have received in encrypted rooms to a local file. You will then be able to import the file into another Matrix client in the future, so that client will also be able to decrypt these messages.": "此操作允许你将加密聊天室中收到的消息的密钥导出为本地文件。你可以将文件导入其他 Matrix 客户端,以便让别的客户端在未收到密钥的情况下解密这些消息。", + "The exported file will allow anyone who can read it to decrypt any encrypted messages that you can see, so you should be careful to keep it secure. To help with this, you should enter a passphrase below, which will be used to encrypt the exported data. It will only be possible to import the data by using the same passphrase.": "导出的文件将允许任何可以读取它的人解密任何他们可以看到的加密消息,因此,你应此小心对待,以确保其安全。为解决此问题,你应当在下面输入密语以加密导出的数据。只有输入相同的密语才能导入数据。", "The export file will be protected with a passphrase. You should enter the passphrase here, to decrypt the file.": "导出文件受密语保护。必须输入密语以解密此文件。", - "This process allows you to import encryption keys that you had previously exported from another Matrix client. You will then be able to decrypt any messages that the other client could decrypt.": "此操作允许您导入之前从另一个 Matrix 客户端中导出的加密密钥文件。导入完成后,您将能够解密那个客户端可以解密的加密消息。", + "This process allows you to import encryption keys that you had previously exported from another Matrix client. You will then be able to decrypt any messages that the other client could decrypt.": "此操作允许你导入之前从另一个 Matrix 客户端中导出的加密密钥文件。导入完成后,你将能够解密那个客户端可以解密的加密消息。", "Ignores a user, hiding their messages from you": "忽略用户,隐藏他们发送的消息", "Stops ignoring a user, showing their messages going forward": "解除忽略用户,显示他们的消息", - "If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "如果你在 GitHub 提交了一个 bug,调试日志可以帮助我们追踪这个问题。 调试日志包含应用程序使用数据,也就包括您的用户名、您访问的房间或社区的 ID 或别名,以及其他用户的用户名,但不包括聊天记录。", + "If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "如果你在 GitHub 提交了一个 bug,调试日志可以帮助我们追踪这个问题。 调试日志包含应用程序使用数据,也就包括你的用户名、你访问的房间或社群的 ID 或别名,以及其他用户的用户名,但不包括聊天记录。", "Tried to load a specific point in this room's timeline, but was unable to find it.": "尝试加载此聊天室的时间线的特定时间点,但是无法找到。", "%(count)s Resend all or cancel all now. You can also select individual messages to resend or cancel.|one": "现在 重新发送消息取消发送 。", "%(count)s Resend all or cancel all now. You can also select individual messages to resend or cancel.|other": "現在 重新发送消息取消发送 。你也可以单独选择消息以重新发送或取消。", "Visibility in Room List": "是否在聊天室目录中可见", - "Something went wrong when trying to get your communities.": "获取你加入的社区时发生错误。", - "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?": "删除挂件时将为聊天室中的所有成员删除。您确定要删除此挂件吗?", + "Something went wrong when trying to get your communities.": "获取你加入的社群时发生错误。", + "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?": "删除挂件时将为聊天室中的所有成员删除。你确定要删除此挂件吗?", "Fetching third party location failed": "获取第三方位置失败", "Send Account Data": "发送账号数据", "All notifications are currently disabled for all targets.": "目前所有通知都已禁用。", @@ -647,7 +647,7 @@ "Update": "更新", "What's New": "更新内容", "On": "打开", - "Changelog": "变更日志", + "Changelog": "更改日志", "Waiting for response from server": "正在等待服务器响应", "Send Custom Event": "发送自定义事件", "Advanced notification settings": "通知高级设置", @@ -679,7 +679,7 @@ "Collecting app version information": "正在收集应用版本信息", "Keywords": "关键词", "Enable notifications for this account": "对此账号启用通知", - "Invite to this community": "邀请加入此社区", + "Invite to this community": "邀请加入此社群", "Messages containing keywords": "包含 关键词 的消息", "Room not found": "找不到聊天室", "Tuesday": "星期二", @@ -730,7 +730,7 @@ "Back": "返回", "Reply": "回复", "Show message in desktop notification": "在桌面通知中显示信息", - "Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "调试日志包含使用数据(包括您的用户名、您访问过的聊天室/群组的 ID 或别名,以及其他用户的用户名),不含聊天消息。", + "Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "调试日志包含使用数据(包括你的用户名、你访问过的聊天室/群组的 ID 或别名,以及其他用户的用户名),不含聊天消息。", "Unhide Preview": "取消隐藏预览", "Unable to join network": "无法加入网络", "Sorry, your browser is not able to run %(brand)s.": "抱歉,您的浏览器 无法 运行 %(brand)s.", @@ -749,8 +749,8 @@ "Event Type": "事件类型", "Download this file": "下载该文件", "Pin Message": "置顶消息", - "Failed to change settings": "变更设置失败", - "View Community": "查看社区", + "Failed to change settings": "更改设置失败", + "View Community": "查看社群", "Event sent!": "事件已发送!", "View Source": "查看源码", "Event Content": "事件内容", @@ -761,13 +761,13 @@ "You need to be able to invite users to do that.": "你需要有邀请用户的权限才能进行此操作。", "Missing roomId.": "找不到此聊天室 ID 所对应的聊天室。", "e.g. ": "例如:", - "Your device resolution": "您设备的分辨率", + "Your device resolution": "你的设备分辨率", "Always show encryption icons": "总是显示加密标志", - "You are about to be taken to a third-party site so you can authenticate your account for use with %(integrationsUrl)s. Do you wish to continue?": "您将被带到一个第三方网站以便验证您的账号来使用 %(integrationsUrl)s 提供的集成。您希望继续吗?", - "The visibility of '%(roomName)s' in %(groupId)s could not be updated.": "无法更新聊天室 %(roomName)s 在社区 “%(groupId)s” 中的可见性。", + "You are about to be taken to a third-party site so you can authenticate your account for use with %(integrationsUrl)s. Do you wish to continue?": "你将被带到一个第三方网站以便验证你的账号来使用 %(integrationsUrl)s 提供的集成。你希望继续吗?", + "The visibility of '%(roomName)s' in %(groupId)s could not be updated.": "无法更新聊天室 %(roomName)s 在社群 “%(groupId)s” 中的可见性。", "Minimize apps": "最小化应用程序", "Popout widget": "在弹出式窗口中打开挂件", - "Unable to load event that was replied to, it either does not exist or you do not have permission to view it.": "无法加载被回复的事件,它可能不存在,也可能是您没有权限查看它。", + "Unable to load event that was replied to, it either does not exist or you do not have permission to view it.": "无法加载被回复的事件,它可能不存在,也可能是你没有权限查看它。", "And %(count)s more...|other": "和 %(count)s 个其他…", "Try using one of the following valid address types: %(validTypesList)s.": "请尝试使用以下的有效邮箱地址格式中的一种:%(validTypesList)s", "e.g. %(exampleValue)s": "例如:%(exampleValue)s", @@ -775,11 +775,11 @@ "A call is already in progress!": "您已在通话中!", "Send analytics data": "发送统计数据", "Enable widget screenshots on supported widgets": "对支持的挂件启用挂件截图", - "Demote yourself?": "是否降低您自己的权限?", + "Demote yourself?": "是否降低你自己的权限?", "Demote": "降权", "A call is currently being placed!": "正在发起通话!", "Permission Required": "需要权限", - "You do not have permission to start a conference call in this room": "您没有在此聊天室发起通话会议的权限", + "You do not have permission to start a conference call in this room": "你没有在此聊天室发起通话会议的权限", "This event could not be displayed": "无法显示此事件", "Share Link to User": "分享链接给其他用户", "Share room": "分享聊天室", @@ -790,53 +790,53 @@ "The email field must not be blank.": "必须输入电子邮箱。", "The phone number field must not be blank.": "必须输入电话号码。", "The password field must not be blank.": "必须输入密码。", - "Display your community flair in rooms configured to show it.": "在启用“显示徽章”的聊天室中显示本社区的个性徽章。", + "Display your community flair in rooms configured to show it.": "在启用“显示徽章”的聊天室中显示本社群的个性徽章。", "Failed to remove widget": "移除小挂件失败", "An error ocurred whilst trying to remove the widget from the room": "尝试从聊天室中移除小部件时发生了错误", - "Are you sure you wish to remove (delete) this event? Note that if you delete a room name or topic change, it could undo the change.": "您确定要移除(删除)此事件吗?注意,如果删除了聊天室名称或话题的修改事件,就会撤销此更改。", - "This will make your account permanently unusable. You will not be able to log in, and no one will be able to re-register the same user ID. This will cause your account to leave all rooms it is participating in, and it will remove your account details from your identity server. This action is irreversible.": "这将使您的账号永远不再可用。您将不能登录,或使用相同的用户 ID 重新注册。您的账号将退出所有已加入的聊天室,身份服务器上的账号信息也会被删除。此操作是不可逆的。", - "Deactivating your account does not by default cause us to forget messages you have sent. If you would like us to forget your messages, please tick the box below.": "默认情况下,停用您的账号不会忘记您发送的消息 。如果您希望我们忘记您发送的消息,请勾选下面的选择框。", - "Message visibility in Matrix is similar to email. Our forgetting your messages means that messages you have sent will not be shared with any new or unregistered users, but registered users who already have access to these messages will still have access to their copy.": "Matrix 中的(历史)信息可见性类似于电子邮件。我们忘记您的消息意味着您发送的消息将不会被发至新注册或未注册的用户,但是已收到您的消息的注册用户依旧可以看到他们的副本。", + "Are you sure you wish to remove (delete) this event? Note that if you delete a room name or topic change, it could undo the change.": "你确定要移除(删除)此事件吗?注意,如果删除了聊天室名称或话题的修改事件,就会撤销此更改。", + "This will make your account permanently unusable. You will not be able to log in, and no one will be able to re-register the same user ID. This will cause your account to leave all rooms it is participating in, and it will remove your account details from your identity server. This action is irreversible.": "这将使你的账号永远不再可用。你将不能登录,或使用相同的用户 ID 重新注册。你的账号将退出所有已加入的聊天室,身份服务器上的账号信息也会被删除。此操作是不可逆的。", + "Deactivating your account does not by default cause us to forget messages you have sent. If you would like us to forget your messages, please tick the box below.": "默认情况下,停用你的账号不会忘记你发送的消息 。如果你希望我们忘记你发送的消息,请勾选下面的选择框。", + "Message visibility in Matrix is similar to email. Our forgetting your messages means that messages you have sent will not be shared with any new or unregistered users, but registered users who already have access to these messages will still have access to their copy.": "Matrix 中的(历史)信息可见性类似于电子邮件。我们忘记你的消息意味着你发送的消息将不会被发至新注册或未注册的用户,但是已收到你的消息的注册用户依旧可以看到他们的副本。", "Please forget all messages I have sent when my account is deactivated (Warning: this will cause future users to see an incomplete view of conversations)": "请在停用我的账号的同时忘记我发送的所有消息(警告:这将导致未来的用户看到的对话记录不完整)", - "To continue, please enter your password:": "请输入您的密码以继续:", + "To continue, please enter your password:": "请输入你的密码以继续:", "Clear Storage and Sign Out": "清除数据并退出登录", "Send Logs": "发送日志", "Refresh": "刷新", - "Unable to join community": "无法加入社区", + "Unable to join community": "无法加入社群", "The user '%(displayName)s' could not be removed from the summary.": "无法将用户“%(displayName)s”从简介中移除。", - "Who would you like to add to this summary?": "您想将谁添加到简介中?", - "Add users to the community summary": "添加用户至社区简介", + "Who would you like to add to this summary?": "你想将谁添加到简介中?", + "Add users to the community summary": "添加用户至社群简介", "Collapse Reply Thread": "收起回复", "Share Message": "分享消息", "COPY": "复制", "Share Room Message": "分享聊天室消息", - "Share Community": "分享社区", + "Share Community": "分享社群", "Share User": "分享用户", "Share Room": "分享聊天室", - "Clearing your browser's storage may fix the problem, but will sign you out and cause any encrypted chat history to become unreadable.": "清除本页储存在您浏览器上的数据或许能修复此问题,但也会导致您退出登录并无法读取任何已加密的聊天记录。", - "We encountered an error trying to restore your previous session.": "我们在尝试恢复您先前的会话时遇到了错误。", + "Clearing your browser's storage may fix the problem, but will sign you out and cause any encrypted chat history to become unreadable.": "清除本页储存在你浏览器上的数据或许能修复此问题,但也会导致你退出登录并无法读取任何已加密的聊天记录。", + "We encountered an error trying to restore your previous session.": "我们在尝试恢复你先前的会话时遇到了错误。", "Link to most recent message": "最新消息的链接", "Link to selected message": "选中消息的链接", - "Changes made to your community name and avatar might not be seen by other users for up to 30 minutes.": "至多半个小时内,其他用户可能看不到您社区的 名称头像 的变化。", - "These rooms are displayed to community members on the community page. Community members can join the rooms by clicking on them.": "这些聊天室对社区成员可见。社区成员可通过点击来加入它们。", - "Your community hasn't got a Long Description, a HTML page to show to community members.
      Click here to open settings and give it one!": "你的社区没有一个很长的描述,一个HTML页面可以展示给社区成员。
      点击这里即可打开设置添加详细介绍!", + "Changes made to your community name and avatar might not be seen by other users for up to 30 minutes.": "至多半个小时内,其他用户可能看不到你社群的 名称头像 的变化。", + "These rooms are displayed to community members on the community page. Community members can join the rooms by clicking on them.": "这些聊天室对社群成员可见。社群成员可通过点击来加入它们。", + "Your community hasn't got a Long Description, a HTML page to show to community members.
      Click here to open settings and give it one!": "你的社群没有一个很长的描述,一个HTML页面可以展示给社群成员。
      点击这里即可打开设置添加详细介绍!", "Failed to load %(groupId)s": "%(groupId)s 加载失败", - "This room is not public. You will not be able to rejoin without an invite.": "此聊天室不是公开聊天室。如果没有成员邀请,您将无法重新加入。", + "This room is not public. You will not be able to rejoin without an invite.": "此聊天室不是公开聊天室。如果没有成员邀请,你将无法重新加入。", "Can't leave Server Notices room": "无法退出服务器公告聊天室", - "This room is used for important messages from the Homeserver, so you cannot leave it.": "此聊天室是用于发布来自主服务器的重要讯息的,所以您不能退出它。", + "This room is used for important messages from the Homeserver, so you cannot leave it.": "此聊天室是用于发布来自主服务器的重要讯息的,所以你不能退出它。", "Terms and Conditions": "条款与要求", - "To continue using the %(homeserverDomain)s homeserver you must review and agree to our terms and conditions.": "若要继续使用主服务器 %(homeserverDomain)s,您必须浏览并同意我们的条款与要求。", + "To continue using the %(homeserverDomain)s homeserver you must review and agree to our terms and conditions.": "若要继续使用主服务器 %(homeserverDomain)s,你必须浏览并同意我们的条款与要求。", "Review terms and conditions": "浏览条款与要求", - "To set up a filter, drag a community avatar over to the filter panel on the far left hand side of the screen. You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.": "若要设置社区过滤器,请将社区头像拖到屏幕最左侧的社区过滤器面板上。单击社区过滤器面板中的社区头像即可过滤出与该社区相关联的房间和人员。", - "You can't send any messages until you review and agree to our terms and conditions.": "在您查看并同意 我们的条款与要求 之前,您不能发送任何消息。", + "To set up a filter, drag a community avatar over to the filter panel on the far left hand side of the screen. You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.": "若要设置社群过滤器,请将社群头像拖到屏幕最左侧的社群过滤器面板上。单击社群过滤器面板中的社群头像即可过滤出与此社群相关联的房间和人员。", + "You can't send any messages until you review and agree to our terms and conditions.": "在你查看并同意 我们的条款与要求 之前,你不能发送任何消息。", "Clear filter": "清除过滤器", - "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "尝试加载此聊天室时间轴上的某处,但您没有查看相关消息的权限。", + "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "尝试加载此聊天室时间轴上的某处,但你没有查看相关消息的权限。", "No Audio Outputs detected": "未检测到可用的音频输出方式", "Audio Output": "音频输出", "An email has been sent to %(emailAddress)s. Once you've followed the link it contains, click below.": "已向 %(emailAddress)s 发送了一封电子邮件。点开邮件中的链接后,请点击下面。", "Forces the current outbound group session in an encrypted room to be discarded": "强制丢弃加密聊天室中的当前出站群组会话", "Unable to connect to Homeserver. Retrying...": "无法连接至主服务器。正在重试…", - "Sorry, your homeserver is too old to participate in this room.": "抱歉,因您的主服务器的程序版本过旧,无法加入此聊天室。", + "Sorry, your homeserver is too old to participate in this room.": "抱歉,因你的主服务器的程序版本过旧,无法加入此聊天室。", "Mirror local video feed": "镜像翻转本地视频源", "This room has been replaced and is no longer active.": "此聊天室已被取代,且不再活跃。", "The conversation continues here.": "对话在这里继续。", @@ -854,14 +854,14 @@ "Legal": "法律信息", "This homeserver has hit its Monthly Active User limit.": "此主服务器已达到其每月活跃用户限制。", "This homeserver has exceeded one of its resource limits.": "本服务器已达到其使用量限制之一。", - "Please contact your service administrator to continue using this service.": "请 联系您的服务管理员 以继续使用本服务。", - "Your message wasn't sent because this homeserver has exceeded a resource limit. Please contact your service administrator to continue using the service.": "您的消息未被发送,因为本主服务器已达到其使用量限制之一。请 联系您的服务管理员 以继续使用本服务。", - "Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. Please contact your service administrator to continue using the service.": "您的消息未被发送,因为本主服务器已达到其每月活跃用户限制。请 联系您的服务管理员 以继续使用本服务。", - "Please contact your service administrator to continue using the service.": "请 联系您的服务管理员 以继续使用本服务。", - "Please contact your homeserver administrator.": "请 联系您的主服务器管理员。", - "%(senderName)s set the main address for this room to %(address)s.": "%(senderName)s 将此聊天室的主地址设为了 %(address)s。", - "%(senderName)s removed the main address for this room.": "%(senderName)s 移除了此聊天室的主地址。", - "Unable to load! Check your network connectivity and try again.": "无法加载!请检查您的网络连接并重试。", + "Please contact your service administrator to continue using this service.": "请 联系你的服务管理员 以继续使用本服务。", + "Your message wasn't sent because this homeserver has exceeded a resource limit. Please contact your service administrator to continue using the service.": "你的消息未被发送,因为本主服务器已达到其使用量限制之一。请 联系你的服务管理员 以继续使用本服务。", + "Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. Please contact your service administrator to continue using the service.": "你的消息未被发送,因为本主服务器已达到其每月活跃用户限制。请 联系你的服务管理员 以继续使用本服务。", + "Please contact your service administrator to continue using the service.": "请 联系你的服务管理员 以继续使用本服务。", + "Please contact your homeserver administrator.": "请 联系你的主服务器管理员。", + "%(senderName)s set the main address for this room to %(address)s.": "%(senderName)s 将此聊天室的主要地址设为了 %(address)s。", + "%(senderName)s removed the main address for this room.": "%(senderName)s 移除了此聊天室的主要地址。", + "Unable to load! Check your network connectivity and try again.": "无法加载!请检查你的网络连接并重试。", "User %(user_id)s does not exist": "用户 %(user_id)s 不存在", "There was an error joining the room": "加入聊天室时发生错误", "Custom user status messages": "自定义用户状态信息", @@ -887,7 +887,7 @@ "Download": "下载", "Retry": "重试", "Go to Settings": "打开设置", - "You do not have permission to invite people to this room.": "您没有权限将其他用户邀请至本聊天室。", + "You do not have permission to invite people to this room.": "你没有权限将其他用户邀请至本聊天室。", "Unknown server error": "未知服务器错误", "Failed to invite users to the room:": "邀请失败:", "No need for symbols, digits, or uppercase letters": "不一定要有符号、数字或大写字母", @@ -895,12 +895,12 @@ "Avoid repeated words and characters": "避免重复词语与字符", "Avoid sequences": "避免递增或递减的序列", "Avoid recent years": "避免年份", - "Avoid years that are associated with you": "避免与您相关联的年份", - "Avoid dates and years that are associated with you": "避免与您相关联的日期与年份", + "Avoid years that are associated with you": "避免与你相关联的年份", + "Avoid dates and years that are associated with you": "避免与你相关联的日期与年份", "Capitalization doesn't help very much": "大写字母并没有很大的作用", "All-uppercase is almost as easy to guess as all-lowercase": "全大写的密码通常比全小写的更容易猜测", "Reversed words aren't much harder to guess": "把单词倒过来不会比原来的难猜很多", - "Whether or not you're logged in (we don't record your username)": "您是否已经登入(我们不会记录您的用户名)", + "Whether or not you're logged in (we don't record your username)": "你是否已经登入(我们不会记录你的用户名)", "The file '%(fileName)s' exceeds this homeserver's size limit for uploads": "文件 %(fileName)s 超过主服务器的文件大小限制", "Upgrades a room to a new version": "将聊天室升级到新版本", "Gets or sets the room topic": "获取或设置聊天室话题", @@ -952,7 +952,7 @@ "Show avatars in user and room mentions": "在用户和聊天室提及中显示头像", "Enable big emoji in chat": "在聊天中启用大型表情符号", "Send typing notifications": "发送正在输入通知", - "Enable Community Filter Panel": "启用社区筛选器面板", + "Enable Community Filter Panel": "启用社群筛选器面板", "Allow Peer-to-Peer for 1:1 calls": "允许一对一通话使用 P2P", "Prompt before sending invites to potentially invalid matrix IDs": "在发送邀请之前提示可能无效的 Matrix ID", "Messages containing my username": "包含我的用户名的消息", @@ -960,7 +960,7 @@ "Encrypted messages in group chats": "群聊中的加密消息", "The other party cancelled the verification.": "另一方取消了验证。", "Verified!": "已验证!", - "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.": "此用户的安全消息是端到端加密的,不能被第三方读取。", "Got It": "收到", "Verify this user by confirming the following emoji appear on their screen.": "通过在其屏幕上显示以下表情符号来验证此用户。", @@ -1031,10 +1031,10 @@ "Pin": "别针", "Yes": "是", "No": "拒绝", - "We've sent you an email to verify your address. Please follow the instructions there and then click the button below.": "我们已向您发送了一封电子邮件,以验证您的地址。 请按照里面的说明操作,然后单击下面的按钮。", + "We've sent you an email to verify your address. Please follow the instructions there and then click the button below.": "我们已向你发送了一封电子邮件,以验证你的地址。 请按照里面的说明操作,然后单击下面的按钮。", "Email Address": "电子邮箱地址", - "Are you sure? You will lose your encrypted messages if your keys are not backed up properly.": "您确定吗?如果密钥没有正确地备份您将失去您的加密消息。", - "Encrypted messages are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.": "加密消息已使用端对端加密保护。只有您和拥有密钥的收件人可以阅读这些消息。", + "Are you sure? You will lose your encrypted messages if your keys are not backed up properly.": "你确定吗?如果密钥没有正确地备份你将失去你的加密消息。", + "Encrypted messages are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.": "加密消息已使用端对端加密保护。只有你和拥有密钥的收件人可以阅读这些消息。", "Unable to load key backup status": "无法载入密钥备份状态", "Restore from Backup": "从备份恢复", "Back up your keys before signing out to avoid losing them.": "在登出账号之前请备份密钥以免丢失。", @@ -1053,7 +1053,7 @@ "Language and region": "语言与区域", "Theme": "主题", "Account management": "账号管理", - "Deactivating your account is a permanent action - be careful!": "停用您的账号是一项永久性操作 - 请小心!", + "Deactivating your account is a permanent action - be careful!": "停用你的账号是一项永久性操作 - 请小心!", "General": "通用", "Credits": "感谢", "For help with using %(brand)s, click here.": "对使用 %(brand)s 的说明,请点击 这里。", @@ -1082,7 +1082,7 @@ "Developer options": "开发者选项", "Room Addresses": "聊天室地址", "Roles & Permissions": "角色与权限", - "Changes to who can read history will only apply to future messages in this room. The visibility of existing history will be unchanged.": "历史记录阅读权限的变更只会应用到此聊天室中将来的消息。既有历史记录的可见性将不会变更。", + "Changes to who can read history will only apply to future messages in this room. The visibility of existing history will be unchanged.": "历史记录阅读权限的更改只会应用到此聊天室中将来的消息。既有历史记录的可见性将不会更改。", "Encryption": "加密", "Once enabled, encryption cannot be disabled.": "加密一经启用,便无法禁用。", "Encrypted": "已加密", @@ -1092,32 +1092,32 @@ "Not now": "现在不要", "Don't ask me again": "不再询问", "Add some now": "立即添加", - "Error updating main address": "更新主地址时发生错误", - "There was an error updating the room's main address. It may not be allowed by the server or a temporary failure occurred.": "更新聊天室的主地址时发生错误。可能是该服务器不允许,也可能是出现了一个临时错误。", - "Main address": "主地址", + "Error updating main address": "更新主要地址时发生错误", + "There was an error updating the room's main address. It may not be allowed by the server or a temporary failure occurred.": "更新聊天室的主要地址时发生错误。可能是此服务器不允许,也可能是出现了一个临时错误。", + "Main address": "主要地址", "Error updating flair": "更新个性徽章时发生错误", - "There was an error updating the flair for this room. The server may not allow it or a temporary error occurred.": "更新此聊天室的个性徽章时发生错误。可能时该服务器不允许,也可能是发生了一个临时错误。", + "There was an error updating the flair for this room. The server may not allow it or a temporary error occurred.": "更新此聊天室的个性徽章时发生错误。可能时此服务器不允许,也可能是发生了一个临时错误。", "Room avatar": "聊天室头像", "Room Name": "聊天室名称", "Room Topic": "聊天室话题", "Join": "加入", "That doesn't look like a valid email address": "这看起来不像是有效的电子邮箱地址", "The following users may not exist": "以下用户可能不存在", - "Unable to find profiles for the Matrix IDs listed below - would you like to invite them anyway?": "找不到下列 Matrix ID 的用户资料,您还是要邀请吗?", + "Unable to find profiles for the Matrix IDs listed below - would you like to invite them anyway?": "找不到下列 Matrix ID 的用户资料,你还是要邀请吗?", "Invite anyway and never warn me again": "还是邀请,不用再提醒我", "Invite anyway": "还是邀请", - "Before submitting logs, you must create a GitHub issue to describe your problem.": "在提交日志之前,您必须创建一个GitHub issue 来描述您的问题。", + "Before submitting logs, you must create a GitHub issue to describe your problem.": "在提交日志之前,你必须创建一个GitHub issue 来描述你的问题。", "Unable to load commit detail: %(msg)s": "无法加载提交详情:%(msg)s", - "To avoid losing your chat history, you must export your room keys before logging out. You will need to go back to the newer version of %(brand)s to do this": "为避免丢失聊天记录,您必须在登出前导出房间密钥。 您需要回到较新版本的 %(brand)s 才能执行此操作", - "Verify this user to mark them as trusted. Trusting users gives you extra peace of mind when using end-to-end encrypted messages.": "验证此用户并将其标记为已信任。在收发端到端加密消息时,信任用户可让您更加放心。", + "To avoid losing your chat history, you must export your room keys before logging out. You will need to go back to the newer version of %(brand)s to do this": "为避免丢失聊天记录,你必须在登出前导出房间密钥。 你需要回到较新版本的 %(brand)s 才能执行此操作", + "Verify this user to mark them as trusted. Trusting users gives you extra peace of mind when using end-to-end encrypted messages.": "验证此用户并将其标记为已信任。在收发端到端加密消息时,信任用户可让你更加放心。", "Waiting for partner to confirm...": "等待对方确认中...", "Incoming Verification Request": "收到验证请求", - "You've previously used %(brand)s on %(host)s with lazy loading of members enabled. In this version lazy loading is disabled. As the local cache is not compatible between these two settings, %(brand)s needs to resync your account.": "您之前在 %(host)s 上开启了 %(brand)s 的成员列表延迟加载设置。目前版本中延迟加载功能已被停用。因为本地缓存在这两个设置项上不相容,%(brand)s 需要重新同步您的账号。", + "You've previously used %(brand)s on %(host)s with lazy loading of members enabled. In this version lazy loading is disabled. As the local cache is not compatible between these two settings, %(brand)s needs to resync your account.": "你之前在 %(host)s 上开启了 %(brand)s 的成员列表延迟加载设置。目前版本中延迟加载功能已被停用。因为本地缓存在这两个设置项上不相容,%(brand)s 需要重新同步你的账号。", "%(brand)s now uses 3-5x less memory, by only loading information about other users when needed. Please wait whilst we resynchronise with the server!": "通过仅在需要时加载其他用户的信息,%(brand)s 现在使用的内存减少到了原来的三分之一至五分之一。 请等待与服务器重新同步!", "I don't want my encrypted messages": "我不想要我的加密消息", "Manually export keys": "手动导出密钥", - "You'll lose access to your encrypted messages": "您将失去您的加密消息的访问权", - "Are you sure you want to sign out?": "您确定要登出账号吗?", + "You'll lose access to your encrypted messages": "你将失去你的加密消息的访问权", + "Are you sure you want to sign out?": "你确定要登出账号吗?", "If you run into any bugs or have feedback you'd like to share, please let us know on GitHub.": "如果您碰到任何错误或者希望分享您的反馈,请在 Github 上联系我们。", "To help avoid duplicate issues, please view existing issues first (and add a +1) or create a new issue if you can't find it.": "要帮助避免提交重复的 issue,请您先 查阅是否为已存在的 issue (如有则添加一个 +1 ) ,或者如果找不到则 新建一个 issue 。", "Report bugs & give feedback": "上报错误及提供反馈", @@ -1125,7 +1125,7 @@ "Room Settings - %(roomName)s": "聊天室设置 - %(roomName)s", "A username can only contain lower case letters, numbers and '=_-./'": "用户名只能包含小写字母、数字和 '=_-./'", "Failed to decrypt %(failedCount)s sessions!": "%(failedCount)s 个会话解密失败!", - "Warning: you should only set up key backup from a trusted computer.": "警告:您应该只在受信任的电脑上设置密钥备份。", + "Warning: you should only set up key backup from a trusted computer.": "警告:你应此只在受信任的电脑上设置密钥备份。", "Access your secure message history and set up secure messaging by entering your recovery passphrase.": "通过输入恢复密码来访问您的安全消息历史记录和设置安全通信。", "If you've forgotten your recovery passphrase you can use your recovery key or set up new recovery options": "如果忘记了恢复密码,您可以 使用恢复密钥 或者 设置新的恢复选项", "This looks like a valid recovery key!": "看起来是有效的恢复密钥!", @@ -1137,8 +1137,8 @@ "Set status": "设置状态", "Set a new status...": "设置新状态...", "Hide": "隐藏", - "This homeserver would like to make sure you are not a robot.": "此主服务器想要确认您不是机器人。", - "Please review and accept all of the homeserver's policies": "请阅读并接受该主服务器的所有政策", + "This homeserver would like to make sure you are not a robot.": "此主服务器想要确认你不是机器人。", + "Please review and accept all of the homeserver's policies": "请阅读并接受此主服务器的所有政策", "Please review and accept the policies of this homeserver:": "请阅读并接受此主服务器的政策:", "Your Modular server": "您的模组服务器", "Enter the location of your Modular homeserver. It may use your own domain name or be a subdomain of modular.im.": "输入您的模组主服务器的位置。它可能使用的是您自己的域名或者 modular.im 的子域名。", @@ -1162,14 +1162,14 @@ "Other": "其他", "Find other public servers or use a custom server": "寻找其他公共服务器或使用自定义服务器", "Couldn't load page": "无法加载页面", - "You are an administrator of this community. You will not be able to rejoin without an invite from another administrator.": "您是此社区的管理员。 没有其他管理员的邀请,您将无法重新加入。", - "This homeserver does not support communities": "此主服务器不支持社区功能", + "You are an administrator of this community. You will not be able to rejoin without an invite from another administrator.": "你是此社群的管理员。 没有其他管理员的邀请,你将无法重新加入。", + "This homeserver does not support communities": "此主服务器不支持社群功能", "Guest": "游客", "Could not load user profile": "无法加载用户资料", "Your Matrix account on %(serverName)s": "您在 %(serverName)s 上的 Matrix 账号", - "A verification email will be sent to your inbox to confirm setting your new password.": "一封验证电子邮件将发送到您的邮箱以确认您设置了新密码。", + "A verification email will be sent to your inbox to confirm setting your new password.": "一封验证电子邮件将发送到你的邮箱以确认你设置了新密码。", "Sign in instead": "登入", - "Your password has been reset.": "您的密码已重置。", + "Your password has been reset.": "你的密码已重置。", "Set a new password": "设置新密码", "Invalid homeserver discovery response": "无效的主服务器搜索响应", "Invalid identity server discovery response": "无效的身份服务器搜索响应", @@ -1182,14 +1182,14 @@ "Unable to query for supported registration methods.": "无法查询支持的注册方法。", "Create your account": "创建您的账号", "Keep going...": "请继续...", - "For maximum security, this should be different from your account password.": "为确保最大的安全性,它应该与您的账号密码不同。", + "For maximum security, this should be different from your account password.": "为确保最大的安全性,它应该与你的账号密码不同。", "That matches!": "匹配成功!", "That doesn't match.": "不匹配。", "Go back to set it again.": "返回重新设置。", "Print it and store it somewhere safe": "打印 并存放在安全的地方", "Save it on a USB key or backup drive": "保存 在 U 盘或备份磁盘中", - "Copy it to your personal cloud storage": "复制 到您的个人云端存储", - "Your keys are being backed up (the first backup could take a few minutes).": "正在备份您的密钥(第一次备份可能会花费几分钟时间)。", + "Copy it to your personal cloud storage": "复制 到你的个人云端存储", + "Your keys are being backed up (the first backup could take a few minutes).": "正在备份你的密钥(第一次备份可能会花费几分钟时间)。", "Set up Secure Message Recovery": "设置安全消息恢复", "Starting backup...": "开始备份...", "Success!": "成功!", @@ -1198,18 +1198,18 @@ "If you don't want to set this up now, you can later in Settings.": "如果您现在不想设置,您可以稍后在设置中操作。", "New Recovery Method": "新恢复方式", "A new recovery passphrase and key for Secure Messages have been detected.": "检测到安全消息的一个新恢复密码和密钥。", - "If you didn't set the new recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.": "如果您没有设置新恢复方式,可能有攻击者正试图侵入您的账号。请立即更改您的账号密码并在设置中设定一个新恢复方式。", + "If you didn't set the new recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.": "如果你没有设置新恢复方式,可能有攻击者正试图侵入你的账号。请立即更改你的账号密码并在设置中设定一个新恢复方式。", "Set up Secure Messages": "设置安全消息", "Recovery Method Removed": "恢复方式已移除", - "If you didn't remove the recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.": "如果您没有移除该恢复方式,可能有攻击者正试图侵入您的账号。请立即更改您的账号密码并在设置中设定一个新的恢复方式。", + "If you didn't remove the recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.": "如果你没有移除此恢复方式,可能有攻击者正试图侵入你的账号。请立即更改你的账号密码并在设置中设定一个新的恢复方式。", "Prepends ¯\\_(ツ)_/¯ to a plain-text message": "在纯文本消息开头添加 ¯\\_(ツ)_/¯", "User %(userId)s is already in the room": "用户 %(userId)s 已在聊天室中", "The user must be unbanned before they can be invited.": "用户必须先解封才能被邀请。", - "Upgrade to your own domain": "升级 到您自己的域名", + "Upgrade to your own domain": "升级 到你自己的域名", "Accept all %(invitedRooms)s invites": "接受所有 %(invitedRooms)s 邀请", "Change room avatar": "更改聊天室头像", "Change room name": "更改聊天室名称", - "Change main address for the room": "更改聊天室主地址", + "Change main address for the room": "更改聊天室主要地址", "Change history visibility": "更改历史记录可见性", "Change permissions": "更改权限", "Change topic": "更改话题", @@ -1227,18 +1227,18 @@ "Enable encryption?": "启用加密?", "Once enabled, encryption for a room cannot be disabled. Messages sent in an encrypted room cannot be seen by the server, only by the participants of the room. Enabling encryption may prevent many bots and bridges from working correctly. Learn more about encryption.": "聊天室加密一经启用,便无法禁用。在加密聊天室中,发送的消息无法被服务器看到,只能被聊天室的参与者看到。启用加密可能会使许多机器人和桥接无法正常运作。 详细了解加密。", "Power level": "权限级别", - "Want more than a community? Get your own server": "想要的不只是社区? 架设您自己的服务器", + "Want more than a community? Get your own server": "想要的不只是社群? 架设你自己的服务器", "Please install Chrome, Firefox, or Safari for the best experience.": "请安装 ChromeFirefox,或 Safari 以获得最佳体验。", - "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.": "警告:升级聊天室 不会自动将聊天室成员转移到新版聊天室中。 我们将会在旧版聊天室中发布一个新版聊天室的链接 - 聊天室成员必须点击此链接以加入新聊天室。", "Adds a custom widget by URL to the room": "通过链接为聊天室添加自定义挂件", "Please supply a https:// or http:// widget URL": "请提供一个 https:// 或 http:// 形式的插件", - "You cannot modify widgets in this room.": "您无法修改此聊天室的插件。", + "You cannot modify widgets in this room.": "你无法修改此聊天室的插件。", "%(senderName)s revoked the invitation for %(targetDisplayName)s to join the room.": "%(senderName)s 撤销了对 %(targetDisplayName)s 加入聊天室的邀请。", "Upgrade this room to the recommended room version": "升级此聊天室至推荐版本", - "This room is running room version , which this homeserver has marked as unstable.": "此聊天室运行的聊天室版本是 ,该版本已被主服务器标记为 不稳定 。", + "This room is running room version , which this homeserver has marked as unstable.": "此聊天室运行的聊天室版本是 ,此版本已被主服务器标记为 不稳定 。", "Upgrading this room will shut down the current instance of the room and create an upgraded room with the same name.": "升级此聊天室将会关闭聊天室的当前实例并创建一个具有相同名称的升级版聊天室。", "Failed to revoke invite": "撤销邀请失败", - "Could not revoke the invite. The server may be experiencing a temporary problem or you do not have sufficient permissions to revoke the invite.": "无法撤销邀请。该服务器可能出现了临时错误,或者您没有足够的权限来撤销邀请。", + "Could not revoke the invite. The server may be experiencing a temporary problem or you do not have sufficient permissions to revoke the invite.": "无法撤销邀请。此服务器可能出现了临时错误,或者你没有足够的权限来撤销邀请。", "Revoke invite": "撤销邀请", "Invited by %(sender)s": "被 %(sender)s 邀请", "Maximize apps": "最大化应用程序", @@ -1246,32 +1246,32 @@ "A widget located at %(widgetUrl)s would like to verify your identity. By allowing this, the widget will be able to verify your user ID, but not perform actions as you.": "位于 %(widgetUrl)s 的小部件想要验证您的身份。在您允许后,小部件就可以验证您的用户 ID,但不能代您执行操作。", "Remember my selection for this widget": "记住我对此挂件的选择", "Deny": "拒绝", - "%(brand)s failed to get the protocol list from the homeserver. The homeserver may be too old to support third party networks.": "%(brand)s 无法从主服务器处获取协议列表。该主服务器上的软件可能过旧,不支持第三方网络。", + "%(brand)s failed to get the protocol list from the homeserver. The homeserver may be too old to support third party networks.": "%(brand)s 无法从主服务器处获取协议列表。此主服务器上的软件可能过旧,不支持第三方网络。", "%(brand)s failed to get the public room list.": "%(brand)s 无法获取公开聊天室列表。", "The homeserver may be unavailable or overloaded.": "主服务器似乎不可用或过载。", - "You have %(count)s unread notifications in a prior version of this room.|other": "您在此聊天室的先前版本中有 %(count)s 条未读通知。", - "You have %(count)s unread notifications in a prior version of this room.|one": "您在此聊天室的先前版本中有 %(count)s 条未读通知。", + "You have %(count)s unread notifications in a prior version of this room.|other": "你在此聊天室的先前版本中有 %(count)s 条未读通知。", + "You have %(count)s unread notifications in a prior version of this room.|one": "你在此聊天室的先前版本中有 %(count)s 条未读通知。", "Add Email Address": "添加 Email 地址", "Add Phone Number": "添加电话号码", "Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "是否使用“面包屑”功能(最近访问的房间的图标在房间列表上方显示)", "Call failed due to misconfigured server": "因为服务器配置错误通话失败", - "Please ask the administrator of your homeserver (%(homeserverDomain)s) to configure a TURN server in order for calls to work reliably.": "请联系您主服务器(%(homeserverDomain)s)的管理员设置 TURN 服务器来确保通话运作稳定。", - "Alternatively, you can try to use the public server at turn.matrix.org, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.": "您也可以尝试使用turn.matrix.org公共服务器,但通话质量稍差,并且其将会得知您的 IP。您可以在设置中更改此选项。", + "Please ask the administrator of your homeserver (%(homeserverDomain)s) to configure a TURN server in order for calls to work reliably.": "请联系你主服务器(%(homeserverDomain)s)的管理员设置 TURN 服务器来确保通话运作稳定。", + "Alternatively, you can try to use the public server at turn.matrix.org, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.": "你也可以尝试使用turn.matrix.org公共服务器,但通话质量稍差,并且其将会得知你的 IP。你可以在设置中更改此选项。", "Try using turn.matrix.org": "尝试使用 turn.matrix.org", - "Your %(brand)s is misconfigured": "您的 %(brand)s 配置有错误", + "Your %(brand)s is misconfigured": "你的 %(brand)s 配置有错误", "Use Single Sign On to continue": "使用单点登录继续", - "Confirm adding this email address by using Single Sign On to prove your identity.": "通过使用单点登录来证明您的身份,并确认添加此邮件地址。", + "Confirm adding this email address by using Single Sign On to prove your identity.": "通过使用单点登录来证明你的身份,并确认添加此邮件地址。", "Single Sign On": "单点登录", "Confirm adding email": "确认使用邮件", "Click the button below to confirm adding this email address.": "点击下面的按钮以确认添加此邮箱地址。", - "Confirm adding this phone number by using Single Sign On to prove your identity.": "通过单点登录以证明您的身份,并确认添加此电话号码。", + "Confirm adding this phone number by using Single Sign On to prove your identity.": "通过单点登录以证明你的身份,并确认添加此电话号码。", "Confirm adding phone number": "确认添加电话号码", "Click the button below to confirm adding this phone number.": "点击下面的按钮以确认添加此电话号码。", "Whether you're using %(brand)s on a device where touch is the primary input mechanism": "是否在触屏设备上使用 %(brand)s", - "Whether you're using %(brand)s as an installed Progressive Web App": "您是否已将 %(brand)s 作为渐进式 Web 应用(PWA)安装", - "Your user agent": "您的用户代理(user agent)", + "Whether you're using %(brand)s as an installed Progressive Web App": "你是否已将 %(brand)s 作为渐进式 Web 应用(PWA)安装", + "Your user agent": "你的用户代理(user agent)", "Replying With Files": "回复文件", - "At this time it is not possible to reply with a file. Would you like to upload this file without replying?": "当前无法在回复中附加文件。您想要仅上传此文件而不回复吗?", + "At this time it is not possible to reply with a file. Would you like to upload this file without replying?": "当前无法在回复中附加文件。你想要仅上传此文件而不回复吗?", "The file '%(fileName)s' failed to upload.": "上传文件 ‘%(fileName)s’ 失败。", "The server does not support the room version specified.": "服务器不支持指定的聊天室版本。", "If you cancel now, you won't complete verifying the other user.": "如果现在取消,您将无法完成验证其他用户。", @@ -1283,11 +1283,11 @@ "Encryption upgrade available": "提供加密升级", "Set up encryption": "设置加密", "Review where you’re logged in": "查看您的登录位置", - "New login. Was this you?": "现在登录。请问是您本人吗?", + "New login. Was this you?": "现在登录。请问是你本人吗?", "Name or Matrix ID": "姓名或 Matrix ID", "Identity server has no terms of service": "身份服务器无服务条款", "This action requires accessing the default identity server to validate an email address or phone number, but the server does not have any terms of service.": "此操作需要访问默认的身份服务器 以验证邮箱地址或电话号码,但是此服务器无任何服务条款。", - "Only continue if you trust the owner of the server.": "只有您信任服务器所有者才能继续。", + "Only continue if you trust the owner of the server.": "只有你信任服务器所有者才能继续。", "Trust": "信任", "%(name)s is requesting verification": "%(name)s 正在请求验证", "Sign In or Create Account": "登录或创建账号", @@ -1299,12 +1299,12 @@ "Actions": "动作", "Sends a message as plain text, without interpreting it as markdown": "以纯文本形式发送消息,不将其作为 markdown 处理", "Sends a message as html, without interpreting it as markdown": "以 html 格式发送消息,不将其作为 markdown 处理", - "You do not have the required permissions to use this command.": "您没有权限使用此命令。", + "You do not have the required permissions to use this command.": "你没有权限使用此命令。", "Error upgrading room": "升级聊天室时发生错误", - "Double check that your server supports the room version chosen and try again.": "请再次检查您的服务器是否支持所选聊天室版本,然后再试一次。", + "Double check that your server supports the room version chosen and try again.": "请再次检查你的服务器是否支持所选聊天室版本,然后再试一次。", "Changes the avatar of the current room": "更改当前聊天室头像", - "Changes your avatar in this current room only": "仅改变您在当前聊天室的头像", - "Changes your avatar in all rooms": "改变您在所有聊天室的头像", + "Changes your avatar in this current room only": "仅改变你在当前聊天室的头像", + "Changes your avatar in all rooms": "改变你在所有聊天室的头像", "Failed to set topic": "话题设置失败", "Use an identity server": "使用身份服务器", "Use an identity server to invite by email. Click continue to use the default identity server (%(defaultIdentityServerName)s) or manage in Settings.": "使用身份服务器以通过电子邮件邀请其他用户。单击继续以使用默认身份服务器(%(defaultIdentityServerName)s),或在设置中进行管理。", @@ -1317,23 +1317,23 @@ "Unknown (user, session) pair:": "未知(用户、会话)对:", "Session already verified!": "会话已验证!", "WARNING: Session already verified, but keys do NOT MATCH!": "警告:会话已验证,但密钥不匹配!", - "WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and session %(deviceId)s is \"%(fprint)s\" which does not match the provided key \"%(fingerprint)s\". This could mean your communications are being intercepted!": "警告:密钥验证失败!%(userId)s 的会话 %(deviceId)s 的签名密钥为 %(fprint)s,与提供的密钥 %(fingerprint)s 不符。这可能表示您的通讯已被截获!", - "The signing key you provided matches the signing key you received from %(userId)s's session %(deviceId)s. Session marked as verified.": "您提供的签名密钥与您从 %(userId)s 的会话 %(deviceId)s 获取的一致。该会话被标为已验证。", - "Sends the given message coloured as a rainbow": "以彩虹色发送给定消息", + "WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and session %(deviceId)s is \"%(fprint)s\" which does not match the provided key \"%(fingerprint)s\". This could mean your communications are being intercepted!": "警告:密钥验证失败!%(userId)s 的会话 %(deviceId)s 的签名密钥为 %(fprint)s,与提供的密钥 %(fingerprint)s 不符。这可能表示你的通讯已被截获!", + "The signing key you provided matches the signing key you received from %(userId)s's session %(deviceId)s. Session marked as verified.": "你提供的签名密钥与你从 %(userId)s 的会话 %(deviceId)s 获取的一致。此会话被标为已验证。", + "Sends the given message coloured as a rainbow": "此消息以彩虹色进行渲染", "Sends the given emote coloured as a rainbow": "以彩虹色发送给定表情符号", "Displays list of commands with usages and descriptions": "显示指令清单与其描述和用法", "Displays information about a user": "显示关于用户的信息", "Send a bug report with logs": "发送带日志的错误报告", "Opens chat with the given user": "与指定用户发起聊天", "Sends a message to the given user": "向指定用户发消息", - "%(senderName)s made no change.": "%(senderName)s 未做出变更。", + "%(senderName)s made no change.": "%(senderName)s 未做出更改。", "%(senderDisplayName)s changed the room name from %(oldRoomName)s to %(newRoomName)s.": "%(senderDisplayName)s 将聊天室名称从 %(oldRoomName)s 改为 %(newRoomName)s。", "%(senderName)s added the alternative addresses %(addresses)s for this room.|other": "%(senderName)s 为此聊天室添加备用地址 %(addresses)s。", "%(senderName)s added the alternative addresses %(addresses)s for this room.|one": "%(senderName)s 为此聊天室添加了备用地址 %(addresses)s。", "%(senderName)s removed the alternative addresses %(addresses)s for this room.|other": "%(senderName)s 为此聊天室移除了备用地址 %(addresses)s。", "%(senderName)s removed the alternative addresses %(addresses)s for this room.|one": "%(senderName)s 为此聊天室移除了备用地址 %(addresses)s。", "%(senderName)s changed the alternative addresses for this room.": "%(senderName)s 更改了此聊天室的备用地址。", - "%(senderName)s changed the main and alternative addresses for this room.": "%(senderName)s 更改了此聊天室的主地址与备用地址。", + "%(senderName)s changed the main and alternative addresses for this room.": "%(senderName)s 更改了此聊天室的主要地址与备用地址。", "%(senderName)s changed the addresses for this room.": "%(senderName)s 更改了此聊天室的地址。", "%(senderName)s placed a voice call.": "%(senderName)s 发起了语音通话。", "%(senderName)s placed a voice call. (not supported by this browser)": "%(senderName)s 发起了语音通话。(此浏览器不支持)", @@ -1356,25 +1356,25 @@ "%(senderName)s changed a rule that was banning rooms matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s更改了一个由于%(reason)s而禁止聊天室%(oldGlob)s跟%(newGlob)s匹配的规则", "%(senderName)s changed a rule that was banning servers matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s 更新了一个由于%(reason)s而禁止服务器%(oldGlob)s跟%(newGlob)s匹配的规则", "%(senderName)s updated a ban rule that was matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s 更新了一个由于%(reason)s而禁止%(oldGlob)s跟%(newGlob)s匹配的规则", - "You signed in to a new session without verifying it:": "您登录了未经过验证的新会话:", - "Verify your other session using one of the options below.": "使用以下选项之一验证您的其他会话。", + "You signed in to a new session without verifying it:": "你登录了未经过验证的新会话:", + "Verify your other session using one of the options below.": "使用以下选项之一验证你的其他会话。", "%(name)s (%(userId)s) signed in to a new session without verifying it:": "%(name)s(%(userId)s)登录到未验证的新会话:", - "Ask this user to verify their session, or manually verify it below.": "要求该用户验证其会话,或在下面手动进行验证。", + "Ask this user to verify their session, or manually verify it below.": "要求此用户验证其会话,或在下面手动进行验证。", "Not Trusted": "不可信任", "Manually Verify by Text": "手动验证文字", "Interactively verify by Emoji": "通过表情符号进行交互式验证", "Done": "完成", "Cannot reach homeserver": "不可连接到主服务器", - "Ensure you have a stable internet connection, or get in touch with the server admin": "确保您的网络连接稳定,或与服务器管理员联系", - "Ask your %(brand)s admin to check your config for incorrect or duplicate entries.": "跟您的%(brand)s管理员确认您的配置不正确或重复的条目。", + "Ensure you have a stable internet connection, or get in touch with the server admin": "确保你的网络连接稳定,或与服务器管理员联系", + "Ask your %(brand)s admin to check your config for incorrect or duplicate entries.": "跟你的%(brand)s管理员确认你的配置不正确或重复的条目。", "Cannot reach identity server": "不可连接到身份服务器", "Room name or address": "房间名称或地址", "Joins room with given address": "使用给定地址加入房间", "Verify this login": "验证此登录名", "Confirm your identity by verifying this login from one of your other sessions, granting it access to encrypted messages.": "通过从其他会话之一验证此登录名并授予其访问加密信息的权限来确认您的身份。", - "Which officially provided instance you are using, if any": "如果您在使用官方实例,是哪一个", - "Every page you use in the app": "您在应用中使用的每个页面", - "Are you sure you want to cancel entering passphrase?": "您确定要取消输入密语吗?", + "Which officially provided instance you are using, if any": "如果有的话,你正在使用官方所提供的哪个实例", + "Every page you use in the app": "你在应用中使用的每个页面", + "Are you sure you want to cancel entering passphrase?": "你确定要取消输入密语吗?", "Go Back": "后退", "Use your account to sign in to the latest version": "使用您的帐户登录到最新版本", "We’re excited to announce Riot is now Element": "我们很高兴地宣布Riot现在更名为Element", @@ -1382,9 +1382,9 @@ "Unrecognised room address:": "无法识别的聊天室地址:", "Light": "浅色", "Dark": "深色", - "You can register, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "您可以注册,但部分功能在身份服务器重新上线之前不可用。如果持续看到此警告,请检查配置或联系服务器管理员。", - "You can reset your password, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "您可以重置密码,但部分功能在身份服务器重新上线之前不可用。如果持续看到此警告,请检查配置或联系服务器管理员。", - "You can log in, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "您可以登录,但部分功能在身份服务器重新上线之前不可用。如果持续看到此警告,请检查配置或联系服务器管理员。", + "You can register, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "你可以注册,但部分功能在身份服务器重新上线之前不可用。如果持续看到此警告,请检查配置或联系服务器管理员。", + "You can reset your password, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "你可以重置密码,但部分功能在身份服务器重新上线之前不可用。如果持续看到此警告,请检查配置或联系服务器管理员。", + "You can log in, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "你可以登录,但部分功能在身份服务器重新上线之前不可用。如果持续看到此警告,请检查配置或联系服务器管理员。", "No homeserver URL provided": "未输入主服务器链接", "Unexpected error resolving homeserver configuration": "解析主服务器配置时发生未知错误", "Unexpected error resolving identity server configuration": "解析身份服务器配置时发生未知错误", @@ -1404,17 +1404,17 @@ "about a day from now": "从现在开始约一天", "%(num)s days from now": "从现在开始%(num)s天", "%(name)s (%(userId)s)": "%(name)s%(userId)s", - "Your browser does not support the required cryptography extensions": "您的浏览器不支持所需的密码学扩展", - "The user's homeserver does not support the version of the room.": "用户的主服务器不支持该聊天室版本。", + "Your browser does not support the required cryptography extensions": "你的浏览器不支持所需的密码学扩展", + "The user's homeserver does not support the version of the room.": "用户的主服务器不支持此聊天室版本。", "Help us improve %(brand)s": "请协助我们改进%(brand)s", "Send anonymous usage data which helps us improve %(brand)s. This will use a cookie.": "发送匿名使用情况数据,以协助我们改进%(brand)s。这将使用cookie。", "I want to help": "我乐意协助", "Verify all your sessions to ensure your account & messages are safe": "验证您的所有会话,以确保账号和消息安全", "Review": "开始验证", "Later": "稍后再说", - "Your homeserver has exceeded its user limit.": "您的主服务器已超过用户限制。", - "Your homeserver has exceeded one of its resource limits.": "您的主服务器已超过某项资源限制。", - "Contact your server admin.": "请联系您的服务器管理员。", + "Your homeserver has exceeded its user limit.": "你的主服务器已超过用户限制。", + "Your homeserver has exceeded one of its resource limits.": "你的主服务器已超过某项资源限制。", + "Contact your server admin.": "请联系你的服务器管理员。", "Ok": "确定", "Set password": "设置密码", "To return to your account in future you need to set a password": "要在之后取回您的帐户,您需要设置密码", @@ -1426,13 +1426,13 @@ "Restart": "重启应用", "Upgrade your %(brand)s": "升级您的%(brand)s", "A new version of %(brand)s is available!": "发现%(brand)s的新版本!", - "You joined the call": "您加入通话", + "You joined the call": "你加入通话", "%(senderName)s joined the call": "%(senderName)s加入通话", "Call in progress": "通话中", "You left the call": "您离开了通话", "%(senderName)s left the call": "%(senderName)s离开了通话", "Call ended": "通话结束", - "You started a call": "您开始了通话", + "You started a call": "你开始了通话", "%(senderName)s started a call": "%(senderName)s开始了通话", "Waiting for answer": "等待接听", "%(senderName)s is calling": "%(senderName)s正在通话", @@ -1461,35 +1461,35 @@ "or": "或者", "Start": "开始", "Confirm the emoji below are displayed on both sessions, in the same order:": "确认两个会话上都以同样顺序显示了下面的emoji:", - "The person who invited you already left the room.": "邀请您的人已经离开了聊天室。", - "The person who invited you already left the room, or their server is offline.": "邀请您的人已经离开了聊天室,或者其服务器为离线状态。", + "The person who invited you already left the room.": "邀请你的人已经离开了聊天室。", + "The person who invited you already left the room, or their server is offline.": "邀请你的人已经离开了聊天室,或者其服务器为离线状态。", "Change notification settings": "修改通知设置", "Manually verify all remote sessions": "手动验证所有远程会话", "My Ban List": "我的封禁列表", - "This is your list of users/servers you have blocked - don't leave the room!": "这是您屏蔽的用户和服务器的列表——请不要离开此聊天室!", + "This is your list of users/servers you have blocked - don't leave the room!": "这是你屏蔽的用户和服务器的列表——请不要离开此聊天室!", "Unknown caller": "未知来电人", "Incoming voice call": "语音来电", "Incoming video call": "视频来电", "Incoming call": "来电", - "Waiting for your other session, %(deviceName)s (%(deviceId)s), to verify…": "等待您的另一个会话 %(deviceName)s (%(deviceId)s) 进行验证…", - "Waiting for your other session to verify…": "等待您的另一个会话进行验证…", + "Waiting for your other session, %(deviceName)s (%(deviceId)s), to verify…": "等待你的另一个会话 %(deviceName)s (%(deviceId)s) 进行验证…", + "Waiting for your other session to verify…": "等待你的另一个会话进行验证…", "Waiting for %(displayName)s to verify…": "等待 %(displayName)s 进行验证…", "Cancelling…": "正在取消…", "They match": "它们匹配", "They don't match": "它们不匹配", "To be secure, do this in person or use a trusted way to communicate.": "为了安全,请当面完成或使用信任的方法交流。", "Lock": "锁", - "Your server isn't responding to some requests.": "您的服务器没有响应一些请求。", + "Your server isn't responding to some requests.": "你的服务器没有响应一些请求。", "From %(deviceName)s (%(deviceId)s)": "来自 %(deviceName)s (%(deviceId)s)", "Decline (%(counter)s)": "拒绝 (%(counter)s)", "Accept to continue:": "接受 以继续:", "Upload": "上传", "Show less": "显示更少", "Show more": "显示更多", - "Changing password will currently reset any end-to-end encryption keys on all sessions, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.": "修改密码会重置所有会话上的端对端加密的密钥,使加密聊天记录不可读,除非您先导出您的聊天室密钥,之后再重新导入。在未来会有所改进。", - "Your homeserver does not support cross-signing.": "您的主服务器不支持交叉签名。", + "Changing password will currently reset any end-to-end encryption keys on all sessions, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.": "修改密码会重置所有会话上的端对端加密的密钥,使加密聊天记录不可读,除非你先导出你的聊天室密钥,之后再重新导入。在未来会有所改进。", + "Your homeserver does not support cross-signing.": "你的主服务器不支持交叉签名。", "Cross-signing and secret storage are enabled.": "交叉签名和秘密存储已启用。", - "Your account has a cross-signing identity in secret storage, but it is not yet trusted by this session.": "您的账号在秘密存储中有交叉签名身份,但并没有被此会话信任。", + "Your account has a cross-signing identity in secret storage, but it is not yet trusted by this session.": "你的账号在秘密存储中有交叉签名身份,但并没有被此会话信任。", "Cross-signing and secret storage are not yet set up.": "交叉签名和秘密存储尚未设置。", "Reset cross-signing and secret storage": "重置交叉签名和秘密存储", "Bootstrap cross-signing and secret storage": "自举交叉签名和秘密存储", @@ -1505,7 +1505,7 @@ "Secret storage public key:": "秘密存储公钥:", "in account data": "在账号数据中", "exists": "存在", - "Your homeserver does not support session management.": "您的主服务器不支持会话管理。", + "Your homeserver does not support session management.": "你的主服务器不支持会话管理。", "Unable to load session list": "无法加载会话列表", "Confirm deleting these sessions": "确认删除这些会话", "Click the button below to confirm deleting these sessions.|other": "点击下方按钮以确认删除这些会话。", @@ -1518,9 +1518,9 @@ "Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.": "逐一验证用户的每一个会话以将其标记为已信任,而不信任交叉签名的设备。", "Manage": "管理", "Enable": "启用", - "%(brand)s is missing some components required for securely caching encrypted messages locally. If you'd like to experiment with this feature, build a custom %(brand)s Desktop with search components added.": "%(brand)s 缺少安全地在本地缓存加密信息所必须的部件。如果您想实验此功能,请构建一个自定义的带有搜索部件的 %(brand)s 桌面版。", + "%(brand)s is missing some components required for securely caching encrypted messages locally. If you'd like to experiment with this feature, build a custom %(brand)s Desktop with search components added.": "%(brand)s 缺少安全地在本地缓存加密信息所必须的部件。如果你想实验此功能,请构建一个自定义的带有搜索部件的 %(brand)s 桌面版。", "%(brand)s can't securely cache encrypted messages locally while running in a web browser. Use %(brand)s Desktop for encrypted messages to appear in search results.": "%(brand)s 在浏览器中运行时不能安全地在本地缓存加密信息。请使用%(brand)s 桌面版以使加密信息出现在搜索结果中。", - "This session is backing up your keys. ": "此会话正在备份您的密钥。 ", + "This session is backing up your keys. ": "此会话正在备份你的密钥。 ", "Connect this session to key backup before signing out to avoid losing any keys that may only be on this session.": "在登出前连接此会话到密钥备份以避免丢失可能仅在此会话上的密钥。", "Connect this session to Key Backup": "将此会话连接到密钥备份", "Backup has a valid signature from this user": "备份有来自此用户的有效签名", @@ -1533,13 +1533,13 @@ "Backup has a valid signature from unverified session ": "备份有一个有效的签名,它来自未验证的会话", "Backup has an invalid signature from verified session ": "备份有一个无效的签名,它来自已验证的会话", "Backup has an invalid signature from unverified session ": "备份有一个无效的签名,它来自未验证的会话", - "Backup is not signed by any of your sessions": "备份没有被您的任何一个会话签名", + "Backup is not signed by any of your sessions": "备份没有被你的任何一个会话签名", "This backup is trusted because it has been restored on this session": "此备份是受信任的因为它被恢复到了此会话上", "Backup key stored: ": "存储的备份密钥: ", - "Your keys are not being backed up from this session.": "您的密钥没有被此会话备份。", + "Your keys are not being backed up from this session.": "你的密钥没有被此会话备份。", "Clear notifications": "清除通知", "There are advanced notifications which are not shown here.": "有高级通知没有显示在此处。", - "You might have configured them in a client other than %(brand)s. You cannot tune them in %(brand)s but they still apply.": "您可能在非 %(brand)s 的客户端里配置了它们。您在 %(brand)s 里无法修改它们,但它们仍然适用。", + "You might have configured them in a client other than %(brand)s. You cannot tune them in %(brand)s but they still apply.": "你可能在非 %(brand)s 的客户端里配置了它们。你在 %(brand)s 里无法修改它们,但它们仍然适用。", "Enable desktop notifications for this session": "为此会话启用桌面通知", "Enable audible notifications for this session": "为此会话启用声音通知", "Identity Server URL must be HTTPS": "身份服务器连接必须是 HTTPS", @@ -1549,29 +1549,29 @@ "Change identity server": "更改身份服务器", "Disconnect from the identity server and connect to instead?": "从 身份服务器断开连接并连接到 吗?", "Terms of service not accepted or the identity server is invalid.": "服务协议未同意或身份服务器无效。", - "The identity server you have chosen does not have any terms of service.": "您选择的身份服务器没有服务协议。", + "The identity server you have chosen does not have any terms of service.": "你选择的身份服务器没有服务协议。", "Disconnect identity server": "断开身份服务器连接", "Disconnect from the identity server ?": "从身份服务器 断开连接吗?", "Disconnect": "断开连接", "You should remove your personal data from identity server before disconnecting. Unfortunately, identity server is currently offline or cannot be reached.": "断开连接前,你应当删除你的个人信息从身份服务器。不幸的是,身份服务器当前处于离线状态或无法访问。", - "You should:": "您应该:", + "You should:": "你应该:", "contact the administrators of identity server ": "联系身份服务器 的管理员", "wait and try again later": "等待并稍后重试", "Disconnect anyway": "仍然断开连接", - "You are still sharing your personal data on the identity server .": "您仍然在分享您的个人信息在身份服务器上。", - "We recommend that you remove your email addresses and phone numbers from the identity server before disconnecting.": "我们推荐您在断开连接前从身份服务器上删除您的邮箱地址和电话号码。", + "You are still sharing your personal data on the identity server .": "你仍然在分享你的个人信息在身份服务器上。", + "We recommend that you remove your email addresses and phone numbers from the identity server before disconnecting.": "我们推荐你在断开连接前从身份服务器上删除你的邮箱地址和电话号码。", "Identity Server (%(server)s)": "身份服务器(%(server)s)", "not stored": "未存储", - "You are currently using to discover and be discoverable by existing contacts you know. You can change your identity server below.": "您正在使用 以发现您认识的现存联系人并被其发现。您可以在下方更改您的身份服务器。", - "If you don't want to use to discover and be discoverable by existing contacts you know, enter another identity server below.": "如果您不想使用 以发现您认识的现存联系人并被其发现,请在下方输入另一个身份服务器。", + "You are currently using to discover and be discoverable by existing contacts you know. You can change your identity server below.": "你正在使用 以发现你认识的现存联系人并被其发现。你可以在下方更改你的身份服务器。", + "If you don't want to use to discover and be discoverable by existing contacts you know, enter another identity server below.": "如果你不想使用 以发现你认识的现存联系人并被其发现,请在下方输入另一个身份服务器。", "Identity Server": "身份服务器", - "You are not currently using an identity server. To discover and be discoverable by existing contacts you know, add one below.": "您现在没有使用身份服务器。若想发现您认识的现存联系人并被其发现,请在下方添加一个身份服务器。", - "Disconnecting from your identity server will mean you won't be discoverable by other users and you won't be able to invite others by email or phone.": "从您的身份服务器断开连接意味着您将不可被别的用户发现,同时您也将不能用邮箱或电话邀请别人。", - "Using an identity server is optional. If you choose not to use an identity server, you won't be discoverable by other users and you won't be able to invite others by email or phone.": "使用身份服务器是可选的。如果您选择不使用身份服务器,您将不能被别的用户发现,也不能用邮箱或电话邀请别人。", + "You are not currently using an identity server. To discover and be discoverable by existing contacts you know, add one below.": "你现在没有使用身份服务器。若想发现你认识的现存联系人并被其发现,请在下方添加一个身份服务器。", + "Disconnecting from your identity server will mean you won't be discoverable by other users and you won't be able to invite others by email or phone.": "从你的身份服务器断开连接意味着你将不可被别的用户发现,同时你也将不能用邮箱或电话邀请别人。", + "Using an identity server is optional. If you choose not to use an identity server, you won't be discoverable by other users and you won't be able to invite others by email or phone.": "使用身份服务器是可选的。如果你选择不使用身份服务器,你将不能被别的用户发现,也不能用邮箱或电话邀请别人。", "Do not use an identity server": "不使用身份服务器", "Enter a new identity server": "输入一个新的身份服务器", "New version available. Update now.": "新版本可用。现在更新。", - "Hey you. You're the best!": "嘿呀。您就是最棒的!", + "Hey you. You're the best!": "嘿呀。你就是最棒的!", "Size must be a number": "大小必须是数字", "Custom font size can only be between %(min)s pt and %(max)s pt": "自定义字体大小只能介于 %(min)s pt 和 %(max)s pt 之间", "Error downloading theme information.": "下载主题信息时发生错误。", @@ -1581,10 +1581,10 @@ "Message layout": "信息布局", "Compact": "紧凑", "Modern": "现代", - "Set the name of a font installed on your system & %(brand)s will attempt to use it.": "设置一个安装在您的系统上的字体名称,%(brand)s 会尝试使用它。", - "Customise your appearance": "自定义您的外观", + "Set the name of a font installed on your system & %(brand)s will attempt to use it.": "设置一个安装在你的系统上的字体名称,%(brand)s 会尝试使用它。", + "Customise your appearance": "自定义你的外观", "Appearance Settings only affect this %(brand)s session.": "外观设置仅会影响此 %(brand)s 会话。", - "Your password was successfully changed. You will not receive push notifications on other sessions until you log back in to them": "您的密码已成功更改。在您重新登录别的会话之前,您将不会在那里收到推送通知", + "Your password was successfully changed. You will not receive push notifications on other sessions until you log back in to them": "你的密码已成功更改。在你重新登录别的会话之前,你将不会在那里收到推送通知", "Agree to the identity server (%(serverName)s) Terms of Service to allow yourself to be discoverable by email address or phone number.": "同意身份服务器(%(serverName)s)的服务协议以允许自己被通过邮件地址或电话号码发现。", "Discovery": "发现", "Clear cache and reload": "清除缓存重新加载", @@ -1600,22 +1600,22 @@ "Ban list rules - %(roomName)s": "封禁列表规则 - %(roomName)s", "Server rules": "服务器规则", "User rules": "用户规则", - "You have not ignored anyone.": "您没有忽略任何人。", - "You are currently ignoring:": "您正在忽略:", - "You are not subscribed to any lists": "您没有订阅任何列表", + "You have not ignored anyone.": "你没有忽略任何人。", + "You are currently ignoring:": "你正在忽略:", + "You are not subscribed to any lists": "你没有订阅任何列表", "Unsubscribe": "取消订阅", "View rules": "查看规则", - "You are currently subscribed to:": "您正在订阅:", + "You are currently subscribed to:": "你正在订阅:", "⚠ These settings are meant for advanced users.": "⚠ 这些设置是为高级用户准备的。", - "Add users and servers you want to ignore here. Use asterisks to have %(brand)s match any characters. For example, @bot:* would ignore all users that have the name 'bot' on any server.": "在此处添加您想忽略的用户和服务器。使用星号以使 %(brand)s 匹配任何字符。例如,@bot:* 会忽略在任何服务器上以「bot」为名的用户。", - "Ignoring people is done through ban lists which contain rules for who to ban. Subscribing to a ban list means the users/servers blocked by that list will be hidden from you.": "忽略人是通过含有封禁规则的封禁列表来完成的。订阅一个封禁列表意味着被该列表阻止的用户/服务器将会对您隐藏。", + "Add users and servers you want to ignore here. Use asterisks to have %(brand)s match any characters. For example, @bot:* would ignore all users that have the name 'bot' on any server.": "在此处添加你想忽略的用户和服务器。使用星号以使 %(brand)s 匹配任何字符。例如,@bot:* 会忽略在任何服务器上以「bot」为名的用户。", + "Ignoring people is done through ban lists which contain rules for who to ban. Subscribing to a ban list means the users/servers blocked by that list will be hidden from you.": "忽略人是通过含有封禁规则的封禁列表来完成的。订阅一个封禁列表意味着被此列表阻止的用户/服务器将会对你隐藏。", "Personal ban list": "个人封禁列表", - "Your personal ban list holds all the users/servers you personally don't want to see messages from. After ignoring your first user/server, a new room will show up in your room list named 'My Ban List' - stay in this room to keep the ban list in effect.": "您的个人封禁列表包含所有您不想看见信息的用户/服务器。您第一次忽略用户/服务器。后,一个名叫「我的封禁列表」的新聊天室将会显示在您的聊天室列表中——留在该聊天室以保持该封禁列表生效。", + "Your personal ban list holds all the users/servers you personally don't want to see messages from. After ignoring your first user/server, a new room will show up in your room list named 'My Ban List' - stay in this room to keep the ban list in effect.": "你的个人封禁列表包含所有你不想看见信息的用户/服务器。你第一次忽略用户/服务器。后,一个名叫「我的封禁列表」的新聊天室将会显示在你的聊天室列表中——留在此聊天室以保持此封禁列表生效。", "Server or user ID to ignore": "要忽略的服务器或用户 ID", "eg: @bot:* or example.org": "例如: @bot:* 或 example.org", "Subscribed lists": "订阅的列表", - "Subscribing to a ban list will cause you to join it!": "订阅一个封禁列表会使您加入它!", - "If this isn't what you want, please use a different tool to ignore users.": "如果这不是您想要的,请使用别的的工具来忽略用户。", + "Subscribing to a ban list will cause you to join it!": "订阅一个封禁列表会使你加入它!", + "If this isn't what you want, please use a different tool to ignore users.": "如果这不是你想要的,请使用别的的工具来忽略用户。", "Room ID or address of ban list": "封禁列表的聊天室 ID 或地址", "Subscribe": "订阅", "Always show the window menu bar": "总是显示窗口菜单栏", @@ -1624,8 +1624,8 @@ "Session key:": "会话密钥:", "Message search": "信息搜索", "Cross-signing": "交叉签名", - "Where you’re logged in": "您在何处登录", - "A session's public name is visible to people you communicate with": "会话的公共名称会对和您交流的人显示", + "Where you’re logged in": "你在何处登录", + "A session's public name is visible to people you communicate with": "会话的公共名称会对和你交流的人显示", "this room": "此聊天室", "View older messages in %(roomName)s.": "查看 %(roomName)s 里更旧的信息。", "Uploaded sound": "已上传的声音", @@ -1637,26 +1637,26 @@ "Upgrade the room": "更新聊天室", "Enable room encryption": "启用聊天室加密", "Error changing power level requirement": "更改权限级别需求时出错", - "An error occurred changing the room's power level requirements. Ensure you have sufficient permissions and try again.": "更改此聊天室的权限级别需求时出错。请确保您有足够的权限后重试。", + "An error occurred changing the room's power level requirements. Ensure you have sufficient permissions and try again.": "更改此聊天室的权限级别需求时出错。请确保你有足够的权限后重试。", "Error changing power level": "更改权限级别时出错", - "An error occurred changing the user's power level. Ensure you have sufficient permissions and try again.": "更改此用户的权限级别时出错。请确保您有足够权限后重试。", + "An error occurred changing the user's power level. Ensure you have sufficient permissions and try again.": "更改此用户的权限级别时出错。请确保你有足够权限后重试。", "To link to this room, please add an address.": "要链接至此聊天室,请添加一个地址。", "Unable to share email address": "无法共享邮件地址", - "Your email address hasn't been verified yet": "您的邮件地址尚未被验证", - "Click the link in the email you received to verify and then click continue again.": "请点击您收到的邮件中的链接后再点击继续。", - "Verify the link in your inbox": "验证您的收件箱中的链接", + "Your email address hasn't been verified yet": "你的邮件地址尚未被验证", + "Click the link in the email you received to verify and then click continue again.": "请点击你收到的邮件中的链接后再点击继续。", + "Verify the link in your inbox": "验证你的收件箱中的链接", "Complete": "完成", "Share": "共享", - "Discovery options will appear once you have added an email above.": "您在上方添加邮箱后发现选项将会出现。", + "Discovery options will appear once you have added an email above.": "你在上方添加邮箱后发现选项将会出现。", "Unable to share phone number": "无法共享电话号码", "Please enter verification code sent via text.": "请输入短信中发送的验证码。", - "Discovery options will appear once you have added a phone number above.": "您添加电话号码后发现选项将会出现。", + "Discovery options will appear once you have added a phone number above.": "你添加电话号码后发现选项将会出现。", "Remove %(email)s?": "删除 %(email)s 吗?", "Remove %(phone)s?": "删除 %(phone)s 吗?", "A text message has been sent to +%(msisdn)s. Please enter the verification code it contains.": "一封短信已发送至 +%(msisdn)s。请输入其中包含的验证码。", "This user has not verified all of their sessions.": "此用户没有验证其全部会话。", - "You have not verified this user.": "您没有验证此用户。", - "You have verified this user. This user has verified all of their sessions.": "您验证了此用户。此用户已验证了其全部会话。", + "You have not verified this user.": "你没有验证此用户。", + "You have verified this user. This user has verified all of their sessions.": "你验证了此用户。此用户已验证了其全部会话。", "* %(senderName)s %(emote)s": "* %(senderName)s %(emote)s", "%(senderName)s: %(message)s": "%(senderName)s: %(message)s", "%(senderName)s: %(reaction)s": "%(senderName)s: %(reaction)s", @@ -1670,33 +1670,33 @@ "Show shortcuts to recently viewed rooms above the room list": "在聊天室列表上方显示最近浏览过的聊天室的快捷方式", "Show hidden events in timeline": "显示时间线中的隐藏事件", "Low bandwidth mode": "低带宽模式", - "Allow fallback call assist server turn.matrix.org when your homeserver does not offer one (your IP address would be shared during a call)": "当您的主服务器没有提供通话辅助服务器时使用备用的 turn.matrix.org 服务器(您的IP地址会在通话期间被共享)", + "Allow fallback call assist server turn.matrix.org when your homeserver does not offer one (your IP address would be shared during a call)": "当你的主服务器没有提供通话辅助服务器时使用备用的 turn.matrix.org 服务器(你的IP地址会在通话期间被共享)", "Send read receipts for messages (requires compatible homeserver to disable)": "为消息发送已读回执(需要兼容的主服务器方可禁用)", "Scan this unique code": "扫描此唯一代码", "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": "若你在两个设备上都没有相机,比较唯一一组表情符号", "Verify this session by confirming the following number appears on its screen.": "确认下方数字显示在屏幕上以验证此会话。", "This bridge is managed by .": "此桥接由 管理。", "Homeserver feature support:": "主服务器功能支持:", - "Confirm deleting these sessions by using Single Sign On to prove your identity.|other": "通过单点登录证明您的身份并确认删除这些会话。", - "Confirm deleting these sessions by using Single Sign On to prove your identity.|one": "通过单点登录证明您的身份并确认删除此会话。", + "Confirm deleting these sessions by using Single Sign On to prove your identity.|other": "通过单点登录证明你的身份并确认删除这些会话。", + "Confirm deleting these sessions by using Single Sign On to prove your identity.|one": "通过单点登录证明你的身份并确认删除此会话。", "Public Name": "公开名称", "Securely cache encrypted messages locally for them to appear in search results.": "在本地安全地缓存加密消息以使其出现在搜索结果中。", "Connecting to integration manager...": "正在连接至集成管理器...", "Cannot connect to integration manager": "不能连接到集成管理器", - "The integration manager is offline or it cannot reach your homeserver.": "此集成管理器为离线状态或者其不能访问您的主服务器。", - "check your browser plugins for anything that might block the identity server (such as Privacy Badger)": "检查您的浏览器是否安装有可能屏蔽身份服务器的插件(例如 Privacy Badger)", + "The integration manager is offline or it cannot reach your homeserver.": "此集成管理器为离线状态或者其不能访问你的主服务器。", + "check your browser plugins for anything that might block the identity server (such as Privacy Badger)": "检查你的浏览器是否安装有可能屏蔽身份服务器的插件(例如 Privacy Badger)", "Use an Integration Manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "使用集成管理器 (%(serverName)s) 以管理机器人、挂件和贴图集。", "Use an Integration Manager to manage bots, widgets, and sticker packs.": "使用集成管理器以管理机器人、挂件和贴图集。", "Manage integrations": "管理集成", - "Integration Managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "集成管理器接收配置数据,并可以以您的名义修改挂件、发送聊天室邀请及设置权限级别。", + "Integration Managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "集成管理器接收配置数据,并可以以你的名义修改挂件、发送聊天室邀请及设置权限级别。", "Use between %(min)s pt and %(max)s pt": "请使用介于 %(min)s pt 和 %(max)s pt 之间的大小", "Deactivate account": "停用账号", "To report a Matrix-related security issue, please read the Matrix.org Security Disclosure Policy.": "要报告 Matrix 相关的安全问题,请阅读 Matrix.org 的安全公开策略。", - "Something went wrong. Please try again or view your console for hints.": "出现问题。请重试或查看您的终端以获得提示。", - "Please try again or view your console for hints.": "请重试或查看您的终端以获得提示。", - "Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.": "您的服务器管理员未在私人聊天室和私聊中默认启用端对端加密。", - "Manage the names of and sign out of your sessions below or verify them in your User Profile.": "在下方管理会话名称,登出您的会话或在您的用户资料中验证它们。", + "Something went wrong. Please try again or view your console for hints.": "出现问题。请重试或查看你的终端以获得提示。", + "Please try again or view your console for hints.": "请重试或查看你的终端以获得提示。", + "Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.": "你的服务器管理员未在私人聊天室和私聊中默认启用端对端加密。", + "Manage the names of and sign out of your sessions below or verify them in your User Profile.": "在下方管理会话名称,登出你的会话或在你的用户资料中验证它们。", "This room is bridging messages to the following platforms. Learn more.": "此聊天室正将消息桥接到以下平台。了解更多。", "This room isn’t bridging messages to any platforms. Learn more.": "此聊天室未将消息桥接到任何平台。了解更多。", "Bridges": "桥接", @@ -1704,10 +1704,10 @@ "This room is end-to-end encrypted": "此聊天室是端对端加密的", "Everyone in this room is verified": "聊天室中所有人都已被验证", "Edit message": "编辑消息", - "Your key share request has been sent - please check your other sessions for key share requests.": "您的密钥共享请求已发送——请检查您别的会话。", - "Key share requests are sent to your other sessions automatically. If you rejected or dismissed the key share request on your other sessions, click here to request the keys for this session again.": "密钥共享请求会自动发送至您别的会话。如果您拒绝或忽略了您别的会话上的密钥共享请求,请点击此处重新为此会话请求密钥。", - "If your other sessions do not have the key for this message you will not be able to decrypt them.": "如果您别的会话没有此消息的密钥您将不能解密它们。", - "Re-request encryption keys from your other sessions.": "从您别的会话重新请求加密密钥。", + "Your key share request has been sent - please check your other sessions for key share requests.": "你的密钥共享请求已发送——请检查你别的会话。", + "Key share requests are sent to your other sessions automatically. If you rejected or dismissed the key share request on your other sessions, click here to request the keys for this session again.": "密钥共享请求会自动发送至你别的会话。如果你拒绝或忽略了你别的会话上的密钥共享请求,请点击此处重新为此会话请求密钥。", + "If your other sessions do not have the key for this message you will not be able to decrypt them.": "如果你别的会话没有此消息的密钥你将不能解密它们。", + "Re-request encryption keys from your other sessions.": "从你别的会话重新请求加密密钥。", "This message cannot be decrypted": "此消息无法被解密", "Encrypted by an unverified session": "由未验证的会话加密", "Unencrypted": "未加密", @@ -1715,7 +1715,7 @@ "The authenticity of this encrypted message can't be guaranteed on this device.": "此加密消息的权威性不能在此设备上被保证。", "Scroll to most recent messages": "滚动到最近的消息", "Close preview": "关闭预览", - "Emoji picker": "表情符号选择器", + "Emoji picker": "Emoji 选择器", "Send a reply…": "发送回复…", "Send a message…": "发送消息…", "Bold": "粗体", @@ -1733,32 +1733,32 @@ "Join the conversation with an account": "使用一个账号加入对话", "Sign Up": "注册", "Loading room preview": "正在加载聊天室预览", - "You were kicked from %(roomName)s by %(memberName)s": "您被 %(memberName)s 踢出了 %(roomName)s", + "You were kicked from %(roomName)s by %(memberName)s": "你被 %(memberName)s 踢出了 %(roomName)s", "Reason: %(reason)s": "原因:%(reason)s", "Forget this room": "忘记此聊天室", "Re-join": "重新加入", - "You were banned from %(roomName)s by %(memberName)s": "您被 %(memberName)s 从 %(roomName)s 封禁了", - "Something went wrong with your invite to %(roomName)s": "您到 %(roomName)s 的邀请出错", - "An error (%(errcode)s) was returned while trying to validate your invite. You could try to pass this information on to a room admin.": "尝试验证您的邀请时返回错误(%(errcode)s)。您可以将此信息转告给聊天室管理员。", + "You were banned from %(roomName)s by %(memberName)s": "你被 %(memberName)s 从 %(roomName)s 封禁了", + "Something went wrong with your invite to %(roomName)s": "你到 %(roomName)s 的邀请出错", + "An error (%(errcode)s) was returned while trying to validate your invite. You could try to pass this information on to a room admin.": "尝试验证你的邀请时返回错误(%(errcode)s)。你可以将此信息转告给聊天室管理员。", "Try to join anyway": "仍然尝试加入", - "You can still join it because this is a public room.": "您仍然能加入,因为这是一个公共聊天室。", + "You can still join it because this is a public room.": "你仍然能加入,因为这是一个公共聊天室。", "Join the discussion": "加入讨论", - "This invite to %(roomName)s was sent to %(email)s which is not associated with your account": "这个到 %(roomName)s 的邀请是发送给 %(email)s 的,而此邮箱没有关联您的账号", - "Link this email with your account in Settings to receive invites directly in %(brand)s.": "要在 %(brand)s 中直接接收邀请,请在设置中将您的账号连接到此邮箱。", + "This invite to %(roomName)s was sent to %(email)s which is not associated with your account": "这个到 %(roomName)s 的邀请是发送给 %(email)s 的,而此邮箱没有关联你的账号", + "Link this email with your account in Settings to receive invites directly in %(brand)s.": "要在 %(brand)s 中直接接收邀请,请在设置中将你的账号连接到此邮箱。", "This invite to %(roomName)s was sent to %(email)s": "这个到 %(roomName)s 的邀请是发送给 %(email)s 的", "Use an identity server in Settings to receive invites directly in %(brand)s.": "要直接在 %(brand)s 中接收邀请,请在设置中使用一个身份服务器。", "Share this email in Settings to receive invites directly in %(brand)s.": "要在 %(brand)s 中直接接收邀请,请在设置中共享此邮箱。", - "Do you want to chat with %(user)s?": "您想和 %(user)s 聊天吗?", + "Do you want to chat with %(user)s?": "你想和 %(user)s 聊天吗?", " wants to chat": " 想聊天", "Start chatting": "开始聊天", - "Do you want to join %(roomName)s?": "您想加入 %(roomName)s 吗?", - " invited you": " 邀请了您", + "Do you want to join %(roomName)s?": "你想加入 %(roomName)s 吗?", + " invited you": " 邀请了你", "Reject & Ignore user": "拒绝并忽略用户", - "You're previewing %(roomName)s. Want to join it?": "您正在预览 %(roomName)s。想加入吗?", - "%(roomName)s can't be previewed. Do you want to join it?": "%(roomName)s 不能被预览。您想加入吗?", - "This room doesn't exist. Are you sure you're at the right place?": "此聊天室不存在。您确定您在正确的地方吗?", - "Try again later, or ask a room admin to check if you have access.": "请稍后重试,或询问聊天室管理员以检查您是否有权限。", - "%(errcode)s was returned while trying to access the room. If you think you're seeing this message in error, please submit a bug report.": "尝试访问该房间是返回了 %(errcode)s。如果您认为您看到此消息是个错误,请提交一个错误报告。", + "You're previewing %(roomName)s. Want to join it?": "你正在预览 %(roomName)s。想加入吗?", + "%(roomName)s can't be previewed. Do you want to join it?": "%(roomName)s 不能被预览。你想加入吗?", + "This room doesn't exist. Are you sure you're at the right place?": "此聊天室不存在。你确定你在正确的地方吗?", + "Try again later, or ask a room admin to check if you have access.": "请稍后重试,或询问聊天室管理员以检查你是否有权限。", + "%(errcode)s was returned while trying to access the room. If you think you're seeing this message in error, please submit a bug report.": "尝试访问该房间是返回了 %(errcode)s。如果你认为你看到此消息是个错误,请提交一个错误报告。", "Appearance": "外观", "Show rooms with unread messages first": "优先显示有未读消息的聊天室", "Show previews of messages": "显示消息预览", @@ -1788,41 +1788,41 @@ "This room has already been upgraded.": "此聊天室已经被升级。", "Unknown Command": "未知命令", "Unrecognised command: %(commandText)s": "未识别的命令:%(commandText)s", - "You can use /help to list available commands. Did you mean to send this as a message?": "您可以使用 /help 列出可用命令。您是否要将其作为消息发送?", - "Hint: Begin your message with // to start it with a slash.": "提示:以 // 开始您的消息来使其以一个斜杠开始。", + "You can use /help to list available commands. Did you mean to send this as a message?": "你可以使用 /help 列出可用命令。你是否要将其作为消息发送?", + "Hint: Begin your message with // to start it with a slash.": "提示:以 // 开始你的消息来使其以一个斜杠开始。", "Send as message": "作为消息发送", "Failed to connect to integration manager": "连接至集成管理器失败", "Mark all as read": "标记所有为已读", "There was an error updating the room's alternative addresses. It may not be allowed by the server or a temporary failure occurred.": "更新此聊天室的备用地址时出现错误。可能是服务器不允许,也可能是出现了一个暂时的错误。", "Error creating address": "创建地址时出现错误", "There was an error creating that address. It may not be allowed by the server or a temporary failure occurred.": "创建地址时出现错误。可能是服务器不允许,也可能是出现了一个暂时的错误。", - "You don't have permission to delete the address.": "您没有权限删除此地址。", + "You don't have permission to delete the address.": "你没有权限删除此地址。", "There was an error removing that address. It may no longer exist or a temporary error occurred.": "删除那个地址时出现错误。可能它已不存在,也可能出现了一个暂时的错误。", "Error removing address": "删除地址时出现错误", "Local address": "本地地址", "Published Addresses": "发布的地址", - "Published addresses can be used by anyone on any server to join your room. To publish an address, it needs to be set as a local address first.": "发布的地址可以被任何服务器上的任何人用来加入您的聊天室。要发布一个地址,它必须先被设为一个本地地址。", + "Published addresses can be used by anyone on any server to join your room. To publish an address, it needs to be set as a local address first.": "发布的地址可以被任何服务器上的任何人用来加入你的聊天室。要发布一个地址,它必须先被设为一个本地地址。", "Other published addresses:": "其它发布的地址:", "No other published addresses yet, add one below": "还没有别的发布的地址,可在下方添加", "New published address (e.g. #alias:server)": "新的发布的地址(例如 #alias:server)", "Local Addresses": "本地地址", - "Set addresses for this room so users can find this room through your homeserver (%(localDomain)s)": "为此聊天室设置地址以便用户通过您的主服务器(%(localDomain)s)找到此聊天室", + "Set addresses for this room so users can find this room through your homeserver (%(localDomain)s)": "为此聊天室设置地址以便用户通过你的主服务器(%(localDomain)s)找到此聊天室", "Waiting for you to accept on your other session…": "等待您在您别的会话上接受…", "Waiting for %(displayName)s to accept…": "等待 %(displayName)s 接受…", "Accepting…": "正在接受…", "Start Verification": "开始验证", "Messages in this room are end-to-end encrypted.": "此聊天室内的消息是端对端加密的。", - "Your messages are secured and only you and the recipient have the unique keys to unlock them.": "您的消息是安全的,只有您和收件人有解开它们的唯一密钥。", + "Your messages are secured and only you and the recipient have the unique keys to unlock them.": "你的消息是安全的,只有你和收件人有解开它们的唯一密钥。", "Messages in this room are not end-to-end encrypted.": "此聊天室内的消息未端对端加密。", - "In encrypted rooms, your messages are secured and only you and the recipient have the unique keys to unlock them.": "在加密聊天室中,您的消息是安全的,只有您和收件人有解开它们的唯一密钥。", + "In encrypted rooms, your messages are secured and only you and the recipient have the unique keys to unlock them.": "在加密聊天室中,你的消息是安全的,只有你和收件人有解开它们的唯一密钥。", "Verify User": "验证用户", - "For extra security, verify this user by checking a one-time code on both of your devices.": "为了更加安全,通过在您二者的设备上检查一次性代码来验证此用户。", - "Your messages are not secure": "您的消息不安全", + "For extra security, verify this user by checking a one-time code on both of your devices.": "为了更加安全,通过在你二者的设备上检查一次性代码来验证此用户。", + "Your messages are not secure": "你的消息不安全", "One of the following may be compromised:": "以下之一可能被损害:", - "Your homeserver": "您的主服务器", - "The homeserver the user you’re verifying is connected to": "您在验证的用户连接到的主服务器", - "Yours, or the other users’ internet connection": "您的或另一位用户的网络连接", - "Yours, or the other users’ session": "您的或另一位用户的会话", + "Your homeserver": "你的主服务器", + "The homeserver the user you’re verifying is connected to": "你在验证的用户连接到的主服务器", + "Yours, or the other users’ internet connection": "你的或另一位用户的网络连接", + "Yours, or the other users’ session": "你的或另一位用户的会话", "Trusted": "受信任的", "Not trusted": "不受信任的", "%(count)s verified sessions|other": "%(count)s 个已验证的会话", @@ -1835,39 +1835,39 @@ "No recent messages by %(user)s found": "没有找到 %(user)s 最近发送的消息", "Try scrolling up in the timeline to see if there are any earlier ones.": "请尝试在时间线中向上滚动以查看是否有更早的。", "Remove recent messages by %(user)s": "删除 %(user)s 最近发送的消息", - "You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?|other": "您将删除 %(user)s 发送的 %(count)s 条消息。此操作不能撤销。您想继续吗?", - "You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?|one": "您将删除 %(user)s 发送的 1 条消息。此操作不能撤销。您想继续吗?", - "For a large amount of messages, this might take some time. Please don't refresh your client in the meantime.": "对于大量消息,可能会消耗一段时间。在此期间请不要刷新您的客户端。", + "You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?|other": "你将删除 %(user)s 发送的 %(count)s 条消息。此操作不能撤销。你想继续吗?", + "You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?|one": "你将删除 %(user)s 发送的 1 条消息。此操作不能撤销。你想继续吗?", + "For a large amount of messages, this might take some time. Please don't refresh your client in the meantime.": "对于大量消息,可能会消耗一段时间。在此期间请不要刷新你的客户端。", "Remove %(count)s messages|other": "删除 %(count)s 条消息", "Remove %(count)s messages|one": "删除 1 条消息", "Remove recent messages": "删除最近消息", "%(role)s in %(roomName)s": "%(roomName)s 中的 %(role)s", "Deactivate user?": "停用用户吗?", - "Deactivating this user will log them out and prevent them from logging back in. Additionally, they will leave all the rooms they are in. This action cannot be reversed. Are you sure you want to deactivate this user?": "停用此用户将会使其登出并阻止其再次登入。而且此用户也会离开其所在的所有聊天室。此操作不可逆。您确定要停用此用户吗?", + "Deactivating this user will log them out and prevent them from logging back in. Additionally, they will leave all the rooms they are in. This action cannot be reversed. Are you sure you want to deactivate this user?": "停用此用户将会使其登出并阻止其再次登入。而且此用户也会离开其所在的所有聊天室。此操作不可逆。你确定要停用此用户吗?", "Deactivate user": "停用用户", "Failed to deactivate user": "停用用户失败", "This client does not support end-to-end encryption.": "此客户端不支持端对端加密。", "Security": "安全", "Verify by scanning": "扫码验证", - "Ask %(displayName)s to scan your code:": "请 %(displayName)s 扫描您的代码:", - "If you can't scan the code above, verify by comparing unique emoji.": "如果您不能扫描以上代码,请通过比较唯一的表情符号来验证。", + "Ask %(displayName)s to scan your code:": "请 %(displayName)s 扫描你的代码:", + "If you can't scan the code above, verify by comparing unique emoji.": "如果你不能扫描以上代码,请通过比较唯一的表情符号来验证。", "Verify by comparing unique emoji.": "通过比较唯一的表情符号来验证。", "Verify by emoji": "通过表情符号验证", - "Almost there! Is your other session showing the same shield?": "快完成了!您的另一个会话显示了同样的盾牌吗?", + "Almost there! Is your other session showing the same shield?": "快完成了!你的另一个会话显示了同样的盾牌吗?", "Almost there! Is %(displayName)s showing the same shield?": "快完成了!%(displayName)s 显示了同样的盾牌吗?", "Verify all users in a room to ensure it's secure.": "验证聊天室中所有用户以确保其安全。", "In encrypted rooms, verify all users to ensure it’s secure.": "在加密聊天室中,验证所有用户以确保其安全。", - "You've successfully verified your device!": "您成功验证了您的设备!", - "You've successfully verified %(deviceName)s (%(deviceId)s)!": "您成功验证了 %(deviceName)s (%(deviceId)s)!", - "You've successfully verified %(displayName)s!": "您成功验证了 %(displayName)s!", + "You've successfully verified your device!": "你成功验证了你的设备!", + "You've successfully verified %(deviceName)s (%(deviceId)s)!": "你成功验证了 %(deviceName)s (%(deviceId)s)!", + "You've successfully verified %(displayName)s!": "你成功验证了 %(displayName)s!", "Verified": "已验证", "Got it": "知道了", "Start verification again from the notification.": "请从提示重新开始验证。", "Start verification again from their profile.": "请从对方资料重新开始验证。", "Verification timed out.": "雅正超时。", - "You cancelled verification on your other session.": "您在您别的会话上取消了验证。", + "You cancelled verification on your other session.": "你在你别的会话上取消了验证。", "%(displayName)s cancelled verification.": "%(displayName)s 取消了验证。", - "You cancelled verification.": "您取消了验证。", + "You cancelled verification.": "你取消了验证。", "Verification cancelled": "验证已取消", "Compare emoji": "比较表情符号", "Encryption enabled": "已启用加密", @@ -1877,20 +1877,20 @@ "React": "回应", "Message Actions": "消息操作", "Show image": "显示图像", - "You have ignored this user, so their message is hidden. Show anyways.": "您已忽略该用户,所以其消息已被隐藏。仍然显示。", - "You verified %(name)s": "您验证了 %(name)s", - "You cancelled verifying %(name)s": "您取消了 %(name)s 的验证", + "You have ignored this user, so their message is hidden. Show anyways.": "你已忽略此用户,所以其消息已被隐藏。仍然显示。", + "You verified %(name)s": "你验证了 %(name)s", + "You cancelled verifying %(name)s": "你取消了 %(name)s 的验证", "%(name)s cancelled verifying": "%(name)s 取消了验证", - "You accepted": "您接受了", + "You accepted": "你接受了", "%(name)s accepted": "%(name)s 接受了", - "You declined": "您拒绝了", - "You cancelled": "您取消了", + "You declined": "你拒绝了", + "You cancelled": "你取消了", "%(name)s declined": "%(name)s 拒绝了", "%(name)s cancelled": "%(name)s 取消了", "Accepting …": "正在接受…", "Declining …": "正在拒绝…", "%(name)s wants to verify": "%(name)s 想要验证", - "You sent a verification request": "您发送了一个验证请求", + "You sent a verification request": "你发送了一个验证请求", "Show all": "显示全部", "Reactions": "回应", " reacted with %(content)s": " 回应了 %(content)s", @@ -1917,14 +1917,14 @@ "Quick Reactions": "快速回应", "Cancel search": "取消搜索", "Any of the following data may be shared:": "以下数据之一可能被分享:", - "Your display name": "您的显示名称", - "Your avatar URL": "您的头像链接", - "Your user ID": "您的用户 ID", - "Your theme": "您的主题", + "Your display name": "你的显示名称", + "Your avatar URL": "你的头像链接", + "Your user ID": "你的用户 ID", + "Your theme": "你的主题", "%(brand)s URL": "%(brand)s 的链接", "Room ID": "聊天室 ID", "Widget ID": "挂件 ID", - "Using this widget may share data with %(widgetDomain)s & your Integration Manager.": "使用此挂件可能会和 %(widgetDomain)s 及您的集成管理器共享数据 。", + "Using this widget may share data with %(widgetDomain)s & your Integration Manager.": "使用此挂件可能会和 %(widgetDomain)s 及你的集成管理器共享数据 。", "Using this widget may share data with %(widgetDomain)s.": "使用此挂件可能会和 %(widgetDomain)s 共享数据 。", "Widgets do not use message encryption.": "挂件不适用消息加密。", "This widget may use cookies.": "此挂件可能使用 cookie。", @@ -1945,12 +1945,12 @@ "Looks good": "看着不错", "Can't find this server or its room list": "找不到此服务器或其聊天室列表", "All rooms": "所有聊天室", - "Your server": "您的服务器", - "Are you sure you want to remove %(serverName)s": "您确定要移除 %(serverName)s 吗", + "Your server": "你的服务器", + "Are you sure you want to remove %(serverName)s": "你确定要移除 %(serverName)s 吗", "Remove server": "移除服务器", "Matrix": "Matrix", "Add a new server": "添加新服务器", - "Enter the name of a new server you want to explore.": "输入您想探索的新服务器的服务器名。", + "Enter the name of a new server you want to explore.": "输入你想探索的新服务器的服务器名。", "Server name": "服务器名", "Add a new server...": "添加新服务器…", "%(networkName)s rooms": "%(networkName)s 的聊天室", @@ -1958,14 +1958,14 @@ "Use an identity server to invite by email. Use the default (%(defaultIdentityServerName)s) or manage in Settings.": "使用一个身份服务器以通过邮箱邀请。使用默认(%(defaultIdentityServerName)s)或在设置中管理。", "Use an identity server to invite by email. Manage in Settings.": "使用一个身份服务器以通过邮箱邀请。在设置中管理。", "Close dialog": "关闭对话框", - "Please tell us what went wrong or, better, create a GitHub issue that describes the problem.": "请告诉我们哪里出错了,或最好创建一个 GitHub issue 来描述该问题。", - "Reminder: Your browser is unsupported, so your experience may be unpredictable.": "提醒:您的浏览器不被支持,所以您的体验可能不可预料。", + "Please tell us what went wrong or, better, create a GitHub issue that describes the problem.": "请告诉我们哪里出错了,或最好创建一个 GitHub issue 来描述此问题。", + "Reminder: Your browser is unsupported, so your experience may be unpredictable.": "提醒:你的浏览器不被支持,所以你的体验可能不可预料。", "GitHub issue": "GitHub 上的 issue", "Notes": "提示", - "If there is additional context that would help in analysing the issue, such as what you were doing at the time, room IDs, user IDs, etc., please include those things here.": "如果有额外的上下文可以帮助我们分析问题,比如您当时在做什么、房间 ID、用户 ID 等等,请将其列于此处。", + "If there is additional context that would help in analysing the issue, such as what you were doing at the time, room IDs, user IDs, etc., please include those things here.": "如果有额外的上下文可以帮助我们分析问题,比如你当时在做什么、房间 ID、用户 ID 等等,请将其列于此处。", "Removing…": "正在移除…", "Destroy cross-signing keys?": "销毁交叉签名密钥?", - "Deleting cross-signing keys is permanent. Anyone you have verified with will see security alerts. You almost certainly don't want to do this, unless you've lost every device you can cross-sign from.": "删除交叉签名密钥是永久的。所有您验证过的人都会看到安全警报。除非您丢失了所有可以交叉签名的设备,否则几乎可以确定您不想这么做。", + "Deleting cross-signing keys is permanent. Anyone you have verified with will see security alerts. You almost certainly don't want to do this, unless you've lost every device you can cross-sign from.": "删除交叉签名密钥是永久的。所有你验证过的人都会看到安全警报。除非你丢失了所有可以交叉签名的设备,否则几乎可以确定你不想这么做。", "Clear cross-signing keys": "清楚交叉签名密钥", "Clear all data in this session?": "是否清除此会话中的所有数据?", "Clearing all data from this session is permanent. Encrypted messages will be lost unless their keys have been backed up.": "清除此会话中的所有数据是永久的。加密消息会丢失,除非其密钥已被备份。", @@ -1973,7 +1973,7 @@ "Please enter a name for the room": "请输入聊天室名称", "Set a room address to easily share your room with other people.": "设置一个聊天室地址以轻松地和别人共享您的聊天室。", "This room is private, and can only be joined by invitation.": "此聊天室是私人的,只能通过邀请加入。", - "You can’t disable this later. Bridges & most bots won’t work yet.": "您之后不能禁用此项。桥接和大部分机器人还不能正常工作。", + "You can’t disable this later. Bridges & most bots won’t work yet.": "你之后不能禁用此项。桥接和大部分机器人还不能正常工作。", "Enable end-to-end encryption": "启用端对端加密", "Create a public room": "创建一个公共聊天室", "Create a private room": "创建一个私人聊天室", @@ -1982,28 +1982,28 @@ "Hide advanced": "隐藏高级", "Show advanced": "显示高级", "Block users on other matrix homeservers from joining this room (This setting cannot be changed later!)": "阻止别的 matrix 主服务器上的用户加入此聊天室(此设置之后不能更改!)", - "You've previously used a newer version of %(brand)s with this session. To use this version again with end to end encryption, you will need to sign out and back in again.": "您曾在此会话中使用了一个更新版本的 %(brand)s。要再使用此版本并使用端对端加密,您需要登出再重新登录。", - "Confirm your account deactivation by using Single Sign On to prove your identity.": "通过单点登录证明您的身份并确认停用您的账号。", - "Are you sure you want to deactivate your account? This is irreversible.": "您确定要停用您的账号吗?此操作不可逆。", + "You've previously used a newer version of %(brand)s with this session. To use this version again with end to end encryption, you will need to sign out and back in again.": "你曾在此会话中使用了一个更新版本的 %(brand)s。要再使用此版本并使用端对端加密,你需要登出再重新登录。", + "Confirm your account deactivation by using Single Sign On to prove your identity.": "通过单点登录证明你的身份并确认停用你的账号。", + "Are you sure you want to deactivate your account? This is irreversible.": "你确定要停用你的账号吗?此操作不可逆。", "Confirm account deactivation": "确认账号停用", "There was a problem communicating with the server. Please try again.": "联系服务器时出现问题。请重试。", "Server did not require any authentication": "服务器不要求任何认证", "Server did not return valid authentication information.": "服务器未返回有效认证信息。", "View Servers in Room": "查看聊天室中的服务器", "Verification Requests": "验证请求", - "Verifying this user will mark their session as trusted, and also mark your session as trusted to them.": "验证此用户会将其会话标记为已信任,与此同时,您的会话也会被此用户标记为已信任。", - "Verify this device to mark it as trusted. Trusting this device gives you and other users extra peace of mind when using end-to-end encrypted messages.": "验证此设备以将其标记为已信任。在收发端对端加密消息时,信任设备可让您与其他用户更加放心。", - "Verifying this device will mark it as trusted, and users who have verified with you will trust this device.": "验证此设备会将其标记为已信任,与此同时,其他验证了您的用户也会信任此设备。", + "Verifying this user will mark their session as trusted, and also mark your session as trusted to them.": "验证此用户会将其会话标记为已信任,与此同时,你的会话也会被此用户标记为已信任。", + "Verify this device to mark it as trusted. Trusting this device gives you and other users extra peace of mind when using end-to-end encrypted messages.": "验证此设备以将其标记为已信任。在收发端对端加密消息时,信任设备可让你与其他用户更加放心。", + "Verifying this device will mark it as trusted, and users who have verified with you will trust this device.": "验证此设备会将其标记为已信任,与此同时,其他验证了你的用户也会信任此设备。", "Integrations are disabled": "集成已禁用", "Enable 'Manage Integrations' in Settings to do this.": "在设置中启用「管理集成」以执行此操作。", "Integrations not allowed": "集成未被允许", - "Your %(brand)s doesn't allow you to use an Integration Manager to do this. Please contact an admin.": "您的 %(brand)s 不允许您使用集成管理器来完成此操作。请联系管理员。", - "To continue, use Single Sign On to prove your identity.": "要继续,请使用单点登录证明您的身份。", + "Your %(brand)s doesn't allow you to use an Integration Manager to do this. Please contact an admin.": "你的 %(brand)s 不允许你使用集成管理器来完成此操作。请联系管理员。", + "To continue, use Single Sign On to prove your identity.": "要继续,请使用单点登录证明你的身份。", "Confirm to continue": "确认以继续", - "Click the button below to confirm your identity.": "点击下方按钮确认您的身份。", + "Click the button below to confirm your identity.": "点击下方按钮确认你的身份。", "Failed to invite the following users to chat: %(csvUsers)s": "邀请以下用户加入聊天失败:%(csvUsers)s", "Something went wrong trying to invite the users.": "尝试邀请用户时出错。", - "We couldn't invite those users. Please check the users you want to invite and try again.": "我们不能邀请这些用户。请检查您想邀请的用户并重试。", + "We couldn't invite those users. Please check the users you want to invite and try again.": "我们不能邀请这些用户。请检查你想邀请的用户并重试。", "Failed to find the following users": "寻找以下用户失败", "The following users might not exist or are invalid, and cannot be invited: %(csvNames)s": "下列用户可能不存在或无效,因此不能被邀请:%(csvNames)s", "Recent Conversations": "最近对话", @@ -2023,48 +2023,48 @@ "Signature upload success": "签名上传成功", "Signature upload failed": "签名上传失败", "If the other version of %(brand)s is still open in another tab, please close it as using %(brand)s on the same host with both lazy loading enabled and disabled simultaneously will cause issues.": "如果别的 %(brand)s 版本在别的标签页中仍然开启,请关闭它,因为在同一宿主上同时使用开启了延迟加载和关闭了延迟加载的 %(brand)s 会导致问题。", - "Confirm by comparing the following with the User Settings in your other session:": "通过比较下方内容和您别的会话中的用户设置来确认:", + "Confirm by comparing the following with the User Settings in your other session:": "通过比较下方内容和你别的会话中的用户设置来确认:", "Confirm this user's session by comparing the following with their User Settings:": "通过比较下方内容和对方用户设置来确认此用户会话:", "Session name": "会话名称", "Session key": "会话密钥", - "If they don't match, the security of your communication may be compromised.": "如果它们不匹配,您通讯的安全性可能已受损。", + "If they don't match, the security of your communication may be compromised.": "如果它们不匹配,你通讯的安全性可能已受损。", "Verify session": "验证会话", - "Your homeserver doesn't seem to support this feature.": "您的主服务器似乎不支持此功能。", + "Your homeserver doesn't seem to support this feature.": "你的主服务器似乎不支持此功能。", "Message edits": "消息编辑历史", - "Your account is not secure": "您的账号不安全", - "Your password": "您的密码", + "Your account is not secure": "你的账号不安全", + "Your password": "你的密码", "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 recovery key in Settings immediately": "我们推荐您立刻在设置中更改您的密码和恢复密钥", "New session": "新会话", - "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.": "如果您没有登录进此会话,您的账号可能已受损。", + "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.": "如果你没有登录进此会话,你的账号可能已受损。", "This wasn't me": "这不是我", "Use your account to sign in to the latest version of the app at ": "使用您的账户在 登录此应用的最新版", "You’re already signed in and good to go here, but you can also grab the latest versions of the app on all platforms at element.io/get-started.": "您已经登录且一切已就绪,但您也可以在 element.io/get-started 获取此应用在全平台上的最新版。", "Go to Element": "前往 Element", "We’re excited to announce Riot is now Element!": "我们很兴奋地宣布 Riot 现在是 Element 了!", "Learn more at element.io/previously-riot": "访问 element.io/previously-riot 了解更多", - "Please fill why you're reporting.": "请填写您为何做此报告。", - "Report Content to Your Homeserver Administrator": "向您的主服务器管理员举报内容", - "Reporting this message will send its unique 'event ID' to the administrator of your homeserver. If messages in this room are encrypted, your homeserver administrator will not be able to read the message text or view any files or images.": "举报此消息会将其唯一的「事件 ID」发送给您的主服务器的管理员。如果此聊天室中的消息被加密,您的主服务器管理员将不能阅读消息文本,也不能查看任何文件或图片。", + "Please fill why you're reporting.": "请填写你为何做此报告。", + "Report Content to Your Homeserver Administrator": "向你的主服务器管理员举报内容", + "Reporting this message will send its unique 'event ID' to the administrator of your homeserver. If messages in this room are encrypted, your homeserver administrator will not be able to read the message text or view any files or images.": "举报此消息会将其唯一的「事件 ID」发送给你的主服务器的管理员。如果此聊天室中的消息被加密,你的主服务器管理员将不能阅读消息文本,也不能查看任何文件或图片。", "Send report": "发送报告", "Upgrading this room requires closing down the current instance of the room and creating a new room in its place. To give room members the best possible experience, we will:": "更新此聊天室需要关闭此聊天室的当前实力并创建一个新的聊天室代替它。为了给聊天室成员最好的体验,我们会:", "Automatically invite users": "自动邀请用户", "Upgrade private room": "更新私人聊天室", "Upgrade public room": "更新公共聊天室", "Upgrading a room is an advanced action and is usually recommended when a room is unstable due to bugs, missing features or security vulnerabilities.": "更新聊天室是高级操作,通常建议在聊天室由于错误、缺失功能或安全漏洞而不稳定时使用。", - "This usually only affects how the room is processed on the server. If you're having problems with your %(brand)s, please report a bug.": "通常这只影响聊天室在服务器上的处理方式。如果您对您的 %(brand)s 有问题,请报告一个错误。", - "You'll upgrade this room from to .": "您将把此聊天室从 升级至 。", + "This usually only affects how the room is processed on the server. If you're having problems with your %(brand)s, please report a bug.": "通常这只影响聊天室在服务器上的处理方式。如果你对你的 %(brand)s 有问题,请报告一个错误。", + "You'll upgrade this room from to .": "你将把此聊天室从 升级至 。", "You're all caught up.": "全数阅毕。", "Server isn't responding": "服务器未响应", - "Your server isn't responding to some of your requests. Below are some of the most likely reasons.": "您的服务器未响应您的一些请求。下方是一些最可能的原因。", + "Your server isn't responding to some of your requests. Below are some of the most likely reasons.": "你的服务器未响应你的一些请求。下方是一些最可能的原因。", "The server (%(serverName)s) took too long to respond.": "服务器(%(serverName)s)花了太长时间响应。", - "Your firewall or anti-virus is blocking the request.": "您的防火墙或防病毒软件阻止了该请求。", - "A browser extension is preventing the request.": "一个浏览器扩展阻止了该请求。", - "The server is offline.": "该服务器为离线状态。", - "The server has denied your request.": "该服务器拒绝了您的请求。", - "Your area is experiencing difficulties connecting to the internet.": "您的区域难以连接上互联网。", + "Your firewall or anti-virus is blocking the request.": "你的防火墙或防病毒软件阻止了此请求。", + "A browser extension is preventing the request.": "一个浏览器扩展阻止了此请求。", + "The server is offline.": "此服务器为离线状态。", + "The server has denied your request.": "此服务器拒绝了你的请求。", + "Your area is experiencing difficulties connecting to the internet.": "你的区域难以连接上互联网。", "A connection error occurred while trying to contact the server.": "尝试联系服务器时出现连接错误。", "Recent changes that have not yet been received": "尚未被接受的最近更改", "Sign out and remove encryption keys?": "登出并删除加密密钥?", @@ -2073,13 +2073,13 @@ "To help us prevent this in future, please send us logs.": "要帮助我们防止其以后发生,请给我们发送日志。", "Missing session data": "缺失会话数据", "Some session data, including encrypted message keys, is missing. Sign out and sign in to fix this, restoring keys from backup.": "一些会话数据,包括加密消息密钥,已缺失。要修复此问题,登出并重新登录,然后从备份恢复密钥。", - "Your browser likely removed this data when running low on disk space.": "您的浏览器可能在磁盘空间不足时删除了此数据。", + "Your browser likely removed this data when running low on disk space.": "你的浏览器可能在磁盘空间不足时删除了此数据。", "Integration Manager": "集成管理器", "Find others by phone or email": "通过电话或邮箱寻找别人", "Be found by phone or email": "通过电话或邮箱被寻找", "Use bots, bridges, widgets and sticker packs": "使用机器人、桥接、挂件和贴图集", "Terms of Service": "服务协议", - "To continue you need to accept the terms of this service.": "要继续,您需要接受此服务协议。", + "To continue you need to accept the terms of this service.": "要继续,你需要接受此服务协议。", "Service": "服务", "Summary": "总结", "Document": "文档", @@ -2101,9 +2101,9 @@ "Invalid Recovery Key": "无效的恢复密钥", "Security Phrase": "安全密码", "Unable to access secret storage. Please verify that you entered the correct recovery passphrase.": "无法访问秘密存储。请确认您输入了正确的恢复密码。", - "Enter your Security Phrase or to continue.": "输入您的安全密码或以继续。", + "Enter your Security Phrase or to continue.": "输入你的安全密码或以继续。", "Security Key": "安全密钥", - "Use your Security Key to continue.": "使用您的安全密钥以继续。", + "Use your Security Key to continue.": "使用你的安全密钥以继续。", "Restoring keys from backup": "从备份恢复密钥", "Fetching keys from server...": "正在从服务器获取密钥...", "%(completed)s of %(total)s keys restored": "%(total)s 个密钥中之 %(completed)s 个已恢复", @@ -2115,7 +2115,7 @@ "Successfully restored %(sessionCount)s keys": "成功恢复了 %(sessionCount)s 个密钥", "Enter recovery passphrase": "输入恢复密码", "Enter recovery key": "输入恢复密钥", - "Warning: You should only set up key backup from a trusted computer.": "警告:您应该只从信任的计算机设置密钥备份。", + "Warning: You should only set up key backup from a trusted computer.": "警告:你应此只从信任的计算机设置密钥备份。", "If you've forgotten your recovery key you can ": "如果您忘记了恢复密钥,您可以", "Address (optional)": "地址(可选)", "Resend edit": "重新发送编辑", @@ -2130,19 +2130,19 @@ "Remove for me": "为我删除", "User Status": "用户状态", "You can use the custom server options to sign into other Matrix servers by specifying a different homeserver URL. This allows you to use %(brand)s with an existing Matrix account on a different homeserver.": "您可以使用自定义服务器选项以使用不同的主服务器链接登录至别的 Matrix 服务器。这允许您通过不同的主服务器上的现存 Matrix 账户使用 %(brand)s。", - "Confirm your identity by entering your account password below.": "在下方输入账号密码以确认您的身份。", - "Missing captcha public key in homeserver configuration. Please report this to your homeserver administrator.": "在主服务器配置中缺少验证码公钥。请将此报告给您的主服务器管理员。", + "Confirm your identity by entering your account password below.": "在下方输入账号密码以确认你的身份。", + "Missing captcha public key in homeserver configuration. Please report this to your homeserver administrator.": "在主服务器配置中缺少验证码公钥。请将此报告给你的主服务器管理员。", "Unable to validate homeserver/identity server": "无法验证主服务器/身份服务器", "Enter the location of your Element Matrix Services homeserver. It may use your own domain name or be a subdomain of element.io.": "输入您的 Element Matrix Services 主服务器的地址。它可能使用您自己的域名,也可能是 element.io 的子域名。", "Enter password": "输入密码", "Nice, strong password!": "不错,是个强密码!", "Password is allowed, but unsafe": "密码允许但不安全", "No identity server is configured so you cannot add an email address in order to reset your password in the future.": "没有配置身份服务器因此您不能添加邮件地址以在将来重置您的密码。", - "Use an email address to recover your account": "使用邮件地址恢复您的账号", + "Use an email address to recover your account": "使用邮件地址恢复你的账号", "Enter email address (required on this homeserver)": "输入邮件地址(此主服务器上必须)", "Doesn't look like a valid email address": "看起来不像有效的邮件地址", "Passwords don't match": "密码不匹配", - "Other users can invite you to rooms using your contact details": "别的用户可以使用您的联系人信息邀请您加入聊天室", + "Other users can invite you to rooms using your contact details": "别的用户可以使用你的联系人信息邀请你加入聊天室", "Enter phone number (required on this homeserver)": "输入电话号码(此主服务器上必须)", "Doesn't look like a valid phone number": "看着不像一个有效的电话号码", "Use lowercase letters, numbers, dashes and underscores only": "仅使用小写字母,数字,横杠和下划线", @@ -2156,7 +2156,7 @@ "Sign in with SSO": "使用单点登录", "No files visible in this room": "此聊天室中没有可见文件", "Welcome to %(appName)s": "欢迎来到 %(appName)s", - "Liberate your communication": "解放您的交流", + "Liberate your communication": "解放你的交流", "Send a Direct Message": "发送私聊", "Explore Public Rooms": "探索公共聊天室", "Create a Group Chat": "创建一个群聊", @@ -2171,7 +2171,7 @@ "View": "查看", "Find a room…": "寻找聊天室…", "Find a room… (e.g. %(exampleRoom)s)": "寻找聊天室... (例如 %(exampleRoom)s)", - "If you can't find the room you're looking for, ask for an invite or Create a new room.": "如果您不能找到您所寻找的聊天室,请索要一个邀请或创建新聊天室。", + "If you can't find the room you're looking for, ask for an invite or Create a new room.": "如果你不能找到你所寻找的聊天室,请索要一个邀请或创建新聊天室。", "Search rooms": "搜索聊天室", "Switch to light mode": "切换到浅色模式", "Switch to dark mode": "切换到深色模式", @@ -2183,7 +2183,7 @@ "Session verified": "会话已验证", "Your Matrix account on ": "您在 上的 Matrix 账户", "No identity server is configured: add one in server settings to reset your password.": "没有配置身份服务器:在服务器设置中添加一个以重设您的密码。", - "You have been logged out of all sessions and will no longer receive push notifications. To re-enable notifications, sign in again on each device.": "您已经登出了所有会话,并将不会收到推送通知。要重新启用通知,请在每个设备上重新登录。", + "You have been logged out of all sessions and will no longer receive push notifications. To re-enable notifications, sign in again on each device.": "你已经登出了所有会话,并将不会收到推送通知。要重新启用通知,请在每个设备上重新登录。", "Failed to get autodiscovery configuration from server": "从服务器获取自动发现配置时失败", "Invalid base_url for m.homeserver": "m.homeserver 的 base_url 无效", "Homeserver URL does not appear to be a valid Matrix homeserver": "主服务器链接不像是有效的 Matrix 主服务器", @@ -2192,11 +2192,11 @@ "This account has been deactivated.": "此账号已被停用。", "Syncing...": "正在同步...", "Signing In...": "正在登录...", - "If you've joined lots of rooms, this might take a while": "如果您加入了很多聊天室,可能会消耗一些时间", - "Your new account (%(newAccountId)s) is registered, but you're already logged into a different account (%(loggedInUserId)s).": "您的新账号(%(newAccountId)s)已注册,但您已经登录了一个不同的账号(%(loggedInUserId)s)。", + "If you've joined lots of rooms, this might take a while": "如果你加入了很多聊天室,可能会消耗一些时间", + "Your new account (%(newAccountId)s) is registered, but you're already logged into a different account (%(loggedInUserId)s).": "你的新账号(%(newAccountId)s)已注册,但你已经登录了一个不同的账号(%(loggedInUserId)s)。", "Continue with previous account": "用之前的账号继续", - "Log in to your new account.": "登录到您的新账号。", - "You can now close this window or log in to your new account.": "您现在可以关闭此窗口或登录到您的新账号。", + "Log in to your new account.": "登录到你的新账号。", + "You can now close this window or log in to your new account.": "你现在可以关闭此窗口或登录到你的新账号。", "Registration Successful": "注册成功", "Use Recovery Key or Passphrase": "使用恢复密钥或密码", "Use Recovery Key": "使用恢复密钥", @@ -2206,21 +2206,21 @@ "%(brand)s iOS": "%(brand)s iOS", "%(brand)s X for Android": "%(brand)s X for Android", "or another cross-signing capable Matrix client": "或者别的可以交叉签名的 Matrix 客户端", - "Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.": "您的新会话现已被验证。它可以访问您的加密消息,别的用户也会视其为受信任的。", - "Your new session is now verified. Other users will see it as trusted.": "您的新会话现已被验证。别的用户会视其为受信任的。", + "Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.": "你的新会话现已被验证。它可以访问你的加密消息,别的用户也会视其为受信任的。", + "Your new session is now verified. Other users will see it as trusted.": "你的新会话现已被验证。别的用户会视其为受信任的。", "Without completing security on this session, it won’t have access to encrypted messages.": "若不在此会话中完成安全验证,它便不能访问加密消息。", "Failed to re-authenticate due to a homeserver problem": "由于主服务器的问题,重新认证失败", "Failed to re-authenticate": "重新认证失败", - "Regain access to your account and recover encryption keys stored in this session. Without them, you won’t be able to read all of your secure messages in any session.": "重新获得访问您账号的权限,并恢复存储在此会话中的加密密钥。没有这些密钥,您将不能在任何会话中阅读您的所有安全消息。", - "Enter your password to sign in and regain access to your account.": "输入您的密码以登录并重新获取访问您账号的权限。", - "Forgotten your password?": "忘记您的密码了吗?", - "Sign in and regain access to your account.": "请登录以重新获取访问您账号的权限。", - "You cannot sign in to your account. Please contact your homeserver admin for more information.": "您不能登录进您的账号。请联系您的主服务器管理员以获取更多信息。", - "You're signed out": "您已登出", + "Regain access to your account and recover encryption keys stored in this session. Without them, you won’t be able to read all of your secure messages in any session.": "重新获得访问你账号的权限,并恢复存储在此会话中的加密密钥。没有这些密钥,你将不能在任何会话中阅读你的所有安全消息。", + "Enter your password to sign in and regain access to your account.": "输入你的密码以登录并重新获取访问你账号的权限。", + "Forgotten your password?": "忘记你的密码了吗?", + "Sign in and regain access to your account.": "请登录以重新获取访问你账号的权限。", + "You cannot sign in to your account. Please contact your homeserver admin for more information.": "你不能登录进你的账号。请联系你的主服务器管理员以获取更多信息。", + "You're signed out": "你已登出", "Clear personal data": "清除个人信息", - "Warning: Your personal data (including encryption keys) is still stored in this session. Clear it if you're finished using this session, or want to sign in to another account.": "警告:您的个人信息(包括加密密钥)仍存储于此会话中。如果您不用再使用此会话或想登录进另一个账号,请清除它。", + "Warning: Your personal data (including encryption keys) is still stored in this session. Clear it if you're finished using this session, or want to sign in to another account.": "警告:你的个人信息(包括加密密钥)仍存储于此会话中。如果你不用再使用此会话或想登录进另一个账号,请清除它。", "Command Autocomplete": "命令自动补全", - "Community Autocomplete": "社区自动补全", + "Community Autocomplete": "社群自动补全", "DuckDuckGo Results": "DuckDuckGo 结果", "Emoji Autocomplete": "表情符号自动补全", "Notification Autocomplete": "通知自动补全", @@ -2228,32 +2228,32 @@ "User Autocomplete": "用户自动补全", "Confirm encryption setup": "确认加密设置", "Click the button below to confirm setting up encryption.": "点击下方按钮以确认设置加密。", - "Safeguard against losing access to encrypted messages & data by backing up encryption keys on your server.": "通过在您的服务器上备份加密密钥来防止丢失您对加密消息和数据的访问权。", + "Safeguard against losing access to encrypted messages & data by backing up encryption keys on your server.": "通过在你的服务器上备份加密密钥来防止丢失你对加密消息和数据的访问权。", "Generate a Security Key": "生成一个安全密钥", - "We’ll generate a Security Key for you to store somewhere safe, like a password manager or a safe.": "我们会生成一个安全密钥以便让您存储在安全的地方,比如密码管理器或保险箱里。", + "We’ll generate a Security Key for you to store somewhere safe, like a password manager or a safe.": "我们会生成一个安全密钥以便让你存储在安全的地方,比如密码管理器或保险箱里。", "Enter a Security Phrase": "输入一个安全密码", - "Use a secret phrase only you know, and optionally save a Security Key to use for backup.": "使用一个只有您知道的密码,您也可以保存安全密钥以供备份使用。", - "Enter your account password to confirm the upgrade:": "输入您的账号密码以确认更新:", - "Restore your key backup to upgrade your encryption": "恢复您的密钥备份以更新您的加密方式", + "Use a secret phrase only you know, and optionally save a Security Key to use for backup.": "使用一个只有你知道的密码,你也可以保存安全密钥以供备份使用。", + "Enter your account password to confirm the upgrade:": "输入你的账号密码以确认更新:", + "Restore your key backup to upgrade your encryption": "恢复你的密钥备份以更新你的加密方式", "Restore": "恢复", - "You'll need to authenticate with the server to confirm the upgrade.": "您需要和服务器进行认证以确认更新。", + "You'll need to authenticate with the server to confirm the upgrade.": "你需要和服务器进行认证以确认更新。", "Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.": "更新此会话以允许其验证其他会话、允许其他会话访问加密消息,并将它们对别的用户标记为已信任。", - "Enter a security phrase only you know, as it’s used to safeguard your data. To be secure, you shouldn’t re-use your account password.": "输入一个只有您知道的安全密码,它将被用来保护您的数据。为了安全,您不应该复用您的账号密码。", + "Enter a security phrase only you know, as it’s used to safeguard your data. To be secure, you shouldn’t re-use your account password.": "输入一个只有你知道的安全密码,它将被用来保护你的数据。为了安全,你不应该复用你的账号密码。", "Enter a recovery passphrase": "输入一个恢复密码", "Great! This recovery passphrase looks strong enough.": "棒!这个恢复密码看着够强。", "Use a different passphrase?": "使用不同的密语?", "Enter your recovery passphrase a second time to confirm it.": "再次输入您的恢复密语以确认。", "Confirm your recovery passphrase": "确认您的恢复密语", - "Store your Security Key somewhere safe, like a password manager or a safe, as it’s used to safeguard your encrypted data.": "将您的安全密钥存储在安全的地方,像是密码管理器或保险箱里,它将被用来保护您的加密数据。", + "Store your Security Key somewhere safe, like a password manager or a safe, as it’s used to safeguard your encrypted data.": "将你的安全密钥存储在安全的地方,像是密码管理器或保险箱里,它将被用来保护你的加密数据。", "Copy": "复制", "Unable to query secret storage status": "无法查询秘密存储状态", - "If you cancel now, you may lose encrypted messages & data if you lose access to your logins.": "如果您现在取消,您可能会丢失加密的消息和数据,如果您丢失了登录信息的话。", - "You can also set up Secure Backup & manage your keys in Settings.": "您也可以在设置中设置安全备份并管理您的密钥。", + "If you cancel now, you may lose encrypted messages & data if you lose access to your logins.": "如果你现在取消,你可能会丢失加密的消息和数据,如果你丢失了登录信息的话。", + "You can also set up Secure Backup & manage your keys in Settings.": "你也可以在设置中设置安全备份并管理你的密钥。", "Set up Secure backup": "设置安全备份", - "Upgrade your encryption": "更新您的加密方法", + "Upgrade your encryption": "更新你的加密方法", "Set a Security Phrase": "设置一个安全密码", "Confirm Security Phrase": "确认安全密码", - "Save your Security Key": "保存您的安全密钥", + "Save your Security Key": "保存你的安全密钥", "Unable to set up secret storage": "无法设置秘密存储", "We'll store an encrypted copy of your keys on our server. Secure your backup with a recovery passphrase.": "我们会在服务器上存储一份您的密钥的加密副本。用恢复密码来保护您的备份。", "Set up with a recovery key": "用恢复密钥设置", @@ -2264,13 +2264,13 @@ "Your recovery key": "您的恢复密钥", "Your recovery key has been copied to your clipboard, paste it to:": "您的恢复密钥已被复制到您的剪贴板,将其粘贴至:", "Your recovery key is in your Downloads folder.": "您的恢复密钥在您的下载文件夹里。", - "Without setting up Secure Message Recovery, you won't be able to restore your encrypted message history if you log out or use another session.": "若不设置安全消息恢复,您如果登出或使用另一个会话,则将不能恢复您的加密消息历史。", + "Without setting up Secure Message Recovery, you won't be able to restore your encrypted message history if you log out or use another session.": "若不设置安全消息恢复,你如果登出或使用另一个会话,则将不能恢复你的加密消息历史。", "Secure your backup with a recovery passphrase": "用恢复密码保护您的备份", "Make a copy of your recovery key": "制作一份您的恢复密钥的副本", "Create key backup": "创建密钥备份", "This session is encrypting history using the new recovery method.": "此会话正在使用新的恢复方法加密历史。", "This session has detected that your recovery passphrase and key for Secure Messages have been removed.": "此会话检测到您的恢复密码和安全消息的密钥已被移除。", - "If you did this accidentally, you can setup Secure Messages on this session which will re-encrypt this session's message history with a new recovery method.": "如果您出于意外这样做了,您可以在此会话上设置安全消息,以使用新的加密方式重新加密此会话的消息历史。", + "If you did this accidentally, you can setup Secure Messages on this session which will re-encrypt this session's message history with a new recovery method.": "如果你出于意外这样做了,你可以在此会话上设置安全消息,以使用新的加密方式重新加密此会话的消息历史。", "If disabled, messages from encrypted rooms won't appear in search results.": "如果被禁用,加密聊天室内的消息不会显示在搜索结果中。", "Disable": "禁用", "Not currently indexing messages for any room.": "现在没有为任何聊天室索引消息。", @@ -2329,7 +2329,7 @@ "Securely cache encrypted messages locally for them to appear in search results, using ": "在本地安全缓存已加密消息以使其出现在搜索结果中,使用 ", " to store messages from ": " 存储来自 ", "rooms.": "聊天室的消息。", - "This session is not backing up your keys, but you do have an existing backup you can restore from and add to going forward.": "此会话未备份您的密钥,但您已有备份,您可以继续并从中恢复和向其添加。", + "This session is not backing up your keys, but you do have an existing backup you can restore from and add to going forward.": "此会话未备份你的密钥,但如果你已有现存备份,你可以继续并从中恢复和向其添加。", "Invalid theme schema.": "无效主题方案。", "Read Marker lifetime (ms)": "已读标记生存期 (ms)", "Read Marker off-screen lifetime (ms)": "已读标记屏幕外生存期 (ms)", @@ -2338,11 +2338,11 @@ "Unable to revoke sharing for phone number": "无法撤销电话号码共享", "Mod": "管理员", "Explore public rooms": "探索公共聊天室", - "Can't see what you’re looking for?": "看不到您要找的吗?", + "Can't see what you’re looking for?": "看不到你要找的吗?", "Explore all public rooms": "探索所有公共聊天室", "%(count)s results|other": "%(count)s 个结果", - "You can only join it with a working invite.": "您只能通过有效邀请加入。", - "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.": "您尝试验证的会话不支持 %(brand)s 支持的扫描二维码或表情符号验证。尝试使用其他客户端。", + "You can only join it with a working invite.": "你只能通过有效邀请加入。", + "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.": "你尝试验证的会话不支持 %(brand)s 支持的扫描二维码或表情符号验证。尝试使用其他客户端。", "Language Dropdown": "语言下拉菜单", "%(severalUsers)smade no changes %(count)s times|other": "%(severalUsers)s 未做更改 %(count)s 次", "%(severalUsers)smade no changes %(count)s times|one": "%(severalUsers)s 未做更改", @@ -2380,7 +2380,7 @@ "Group call started by %(senderName)s": "%(senderName)s 发起的群通话", "Group call ended by %(senderName)s": "%(senderName)s 结束了群通话", "Unknown App": "未知应用", - "Communities v2 prototypes. Requires compatible homeserver. Highly experimental - use with caution.": "社区 v2 原型。需要兼容的主服务器。高度实验性 - 谨慎使用。", + "Communities v2 prototypes. Requires compatible homeserver. Highly experimental - use with caution.": "社群 v2 原型。需要兼容的主服务器。高度实验性 - 谨慎使用。", "Cross-signing is ready for use.": "交叉签名已可用。", "Cross-signing is not set up.": "未设置交叉签名。", "Backup version:": "备份版本:", @@ -2396,7 +2396,7 @@ "not ready": "尚未就绪", "Secure Backup": "安全备份", "Privacy": "隐私", - "Explore community rooms": "探索社区聊天室", + "Explore community rooms": "探索社群聊天室", "%(count)s results|one": "%(count)s 个结果", "Room Info": "聊天室信息", "No other application is using the webcam": "没有其他应用程序正在使用摄像头", @@ -2406,7 +2406,7 @@ "Unable to access webcam / microphone": "无法访问摄像头/麦克风", "Call failed because no microphone could not be accessed. Check that a microphone is plugged in and set up correctly.": "呼叫失败,因为无法访问任何麦克风。 检查是否已插入麦克风并正确设置。", "Unable to access microphone": "无法使用麦克风", - "The call was answered on another device.": "在另一台设备上应答了该通话。", + "The call was answered on another device.": "在另一台设备上应答了此通话。", "The call could not be established": "无法建立通话", "The other party declined the call.": "对方拒绝了通话。", "Call Declined": "通话被拒绝", @@ -2471,18 +2471,18 @@ "Afghanistan": "阿富汗", "United States": "美国", "United Kingdom": "英国", - "Your homeserver rejected your log in attempt. This could be due to things just taking too long. Please try again. If this continues, please contact your homeserver administrator.": "您的主服务器已拒绝您的登入尝试。请重试。如果此情况持续发生,请联系您的主服务器管理员。", - "Your homeserver was unreachable and was not able to log you in. Please try again. If this continues, please contact your homeserver administrator.": "无法访问您的主服务器,因而无法登入。请重试。如果此情况持续发生,请联系您的主服务器管理员。", + "Your homeserver rejected your log in attempt. This could be due to things just taking too long. Please try again. If this continues, please contact your homeserver administrator.": "你的主服务器已拒绝你的登入尝试。请重试。如果此情况持续发生,请联系你的主服务器管理员。", + "Your homeserver was unreachable and was not able to log you in. Please try again. If this continues, please contact your homeserver administrator.": "无法访问你的主服务器,因而无法登入。请重试。如果此情况持续发生,请联系你的主服务器管理员。", "Try again": "重试", - "We asked the browser to remember which homeserver you use to let you sign in, but unfortunately your browser has forgotten it. Go to the sign in page and try again.": "我们已要求浏览器记住您使用的主服务器,但不幸的是您的浏览器已忘记。请前往登录页面重试。", - "We couldn't log you in": "我们无法使您登入", + "We asked the browser to remember which homeserver you use to let you sign in, but unfortunately your browser has forgotten it. Go to the sign in page and try again.": "我们已要求浏览器记住你使用的主服务器,但不幸的是你的浏览器已忘记。请前往登录页面重试。", + "We couldn't log you in": "我们无法使你登入", "This will end the conference for everyone. Continue?": "这将结束所有人的会议。是否继续?", "End conference": "结束会议", - "You've reached the maximum number of simultaneous calls.": "您已达到并行呼叫最大数量。", + "You've reached the maximum number of simultaneous calls.": "你已达到并行呼叫最大数量。", "Too Many Calls": "太多呼叫", "Call failed because webcam or microphone could not be accessed. Check that:": "通话失败,因为无法访问网络摄像头或麦克风。请检查:", "Call failed because microphone could not be accessed. Check that a microphone is plugged in and set up correctly.": "呼叫失败,因为无法访问任何麦克风。 检查是否已插入麦克风并正确设置。", - "Answered Elsewhere": "在其他地方已回答", + "Answered Elsewhere": "已在其他地方回答", "Use the + to make a new room or explore existing ones below": "使用 + 创建新的聊天室或通过下面列出的方式探索已有聊天室", "Start a new chat": "开始新会话", "Room settings": "聊天室设置", @@ -2497,10 +2497,10 @@ "Creating...": "创建中……", "You can change these at any point.": "您可随时更改这些。", "Give it a photo, name and description to help you identify it.": "为它添加一张照片、姓名与描述来帮助您辨认它。", - "Your private space": "您的私有空间", - "Your public space": "您的公共空间", - "You can change this later": "您可稍后更改此项", - "Invite only, best for yourself or teams": "仅邀请,适合您自己或团队", + "Your private space": "你的私有空间", + "Your public space": "你的公共空间", + "You can change this later": "你可稍后更改此项", + "Invite only, best for yourself or teams": "仅邀请,适合你自己或团队", "Private": "私有", "Public": "公共", "Delete": "删除", @@ -2511,8 +2511,8 @@ "Voice Call": "语音通话", "Video Call": "视频通话", "%(peerName)s held the call": "%(peerName)s 挂起了通话", - "You held the call Resume": "您挂起了通话 恢复", - "You held the call Switch": "您挂起了通话 切换", + "You held the call Resume": "你挂起了通话 恢复", + "You held the call Switch": "你挂起了通话 切换", "Takes the call in the current room off hold": "解除挂起当前聊天室的通话", "Places the call in the current room on hold": "挂起当前聊天室的通话", "Show chat effects (animations when receiving e.g. confetti)": "显示聊天特效(如收到五彩纸屑时的动画效果)", @@ -2526,7 +2526,7 @@ "Show stickers button": "显示贴纸按钮", "Render LaTeX maths in messages": "在信息中渲染 LaTeX 数学", "%(senderName)s ended the call": "%(senderName)s 结束了通话", - "You ended the call": "您结束了通话", + "You ended the call": "你结束了通话", "This homeserver has been blocked by it's administrator.": "此 homeserver 已被其管理员屏蔽。", "Use app": "使用 app", "Element Web is experimental on mobile. For a better experience and the latest features, use our free native app.": "Element 网页版在移动设备上仍处于试验阶段。使用免费的原生 app 以获得更好的体验与最新的功能。", @@ -2534,7 +2534,7 @@ "Enable desktop notifications": "开启桌面通知", "Don't miss a reply": "不要错过任何回复", "This homeserver has been blocked by its administrator.": "此 homeserver 已被其管理员屏蔽。", - "Send stickers to this room as you": "以您的身份发送贴纸到此聊天室", + "Send stickers to this room as you": "以你的身份发送贴纸到此聊天室", "Change the avatar of your active room": "更改活跃聊天室的头像", "Change the avatar of this room": "更改当前聊天室的头像", "Change the name of your active room": "更改活跃聊天室的名称", @@ -2543,10 +2543,10 @@ "Change the topic of this room": "更改当前聊天室的话题", "Change which room, message, or user you're viewing": "更改当前正在查看哪个聊天室、消息或用户", "Change which room you're viewing": "更改当前正在查看哪个聊天室", - "Send stickers into your active room": "发送贴纸到您的活跃聊天室", + "Send stickers into your active room": "发送贴纸到你的活跃聊天室", "Send stickers into this room": "发送贴纸到此聊天室", - "Remain on your screen while running": "运行时始终保留在您的屏幕上", - "Remain on your screen when viewing another room, when running": "运行时始终保留在您的屏幕上,即使您在浏览其它聊天室", + "Remain on your screen while running": "运行时始终保留在你的屏幕上", + "Remain on your screen when viewing another room, when running": "运行时始终保留在你的屏幕上,即使你在浏览其它聊天室", "%(senderName)s declined the call.": "%(senderName)s 拒绝了通话。", "(an error occurred)": "(发生了一个错误)", "(their device couldn't start the camera / microphone)": "(对方的设备无法开启摄像头/麦克风)", @@ -2554,12 +2554,12 @@ "Converts the DM to a room": "将此私聊会话转化为聊天室会话", "Prepends ┬──┬ ノ( ゜-゜ノ) to a plain-text message": "在纯文本消息开头添加 ┬──┬ ノ( ゜-゜ノ)", "Prepends (╯°□°)╯︵ ┻━┻ to a plain-text message": "在纯文本消息开头添加 (╯°□°)╯︵ ┻━┻", - "You're already in a call with this person.": "您与此人已处在通话中。", + "You're already in a call with this person.": "你正在与其通话中。", "Already in call": "已在通话中", "Navigate composer history": "浏览编辑区历史", "Go to Home View": "转到主视图", "Search (must be enabled)": "搜索(必须启用)", - "Your Security Key": "您的安全密钥", + "Your Security Key": "你的安全密钥", "Use Security Key": "使用安全密钥", "%(ssoButtons)s Or %(usernamePassword)s": "%(ssoButtons)s 或 %(usernamePassword)s", "User settings": "用户设置", @@ -2575,7 +2575,7 @@ "Remove from Space": "从空间中移除", "Undo": "撤销", "Welcome %(name)s": "欢迎 %(name)s", - "Create community": "创建社区", + "Create community": "创建社群", "Forgot password?": "忘记密码?", "Enter Security Key": "输入安全密钥", "Invalid Security Key": "安全密钥无效", @@ -2603,7 +2603,7 @@ "Value": "值", "Setting ID": "设置 ID", "Enter name": "输入名称", - "Community ID: +:%(domain)s": "社区 ID:+:%(domain)s", + "Community ID: +:%(domain)s": "社群 ID:+:%(domain)s", "Reason (optional)": "理由(可选)", "Show": "显示", "Apply": "应用", @@ -2634,7 +2634,7 @@ "Send message": "发送消息", "Invite to this space": "邀请至此空间", "Your message was sent": "消息已发送", - "Back up your encryption keys with your account data in case you lose access to your sessions. Your keys will be secured with a unique Security Key.": "请使用您的账号数据备份加密密钥,以免您无法访问您的会话。密钥将通过一个唯一的安全密钥进行保护。", + "Back up your encryption keys with your account data in case you lose access to your sessions. Your keys will be secured with a unique Security Key.": "请使用你的账号数据备份加密密钥,以免你无法访问你的会话。密钥将通过一个唯一的安全密钥进行保护。", "Spell check dictionaries": "拼写检查字典", "Failed to save your profile": "个人资料保存失败", "The operation could not be completed": "操作无法完成", @@ -2654,11 +2654,11 @@ "Sends the given message with fireworks": "附加烟火发送", "sends fireworks": "发送烟火", "Offline encrypted messaging using dehydrated devices": "需要离线设备(dehydrated devices)的加密消息离线传递", - "Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags. Requires compatible homeserver for some features.": "正在开发的空间功能的原型。与社区、社区 V2 和自定义标签功能不兼容。需要主服务器兼容才能使用某些功能。", + "Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags. Requires compatible homeserver for some features.": "正在开发的空间功能的原型。与社群、社群 V2 和自定义标签功能不兼容。需要主服务器兼容才能使用某些功能。", "The %(capability)s capability": "%(capability)s 容量", "%(senderName)s has updated the widget layout": "%(senderName)s 已更新挂件布局", "Support": "支持", - "Your server does not support showing space hierarchies.": "您的服务器不支持显示空间层次结构。", + "Your server does not support showing space hierarchies.": "你的服务器不支持显示空间层次结构。", "This version of %(brand)s does not support searching encrypted messages": "当前版本的 %(brand)s 不支持搜索加密消息", "This version of %(brand)s does not support viewing some encrypted files": "当前版本的 %(brand)s 不支持查看某些加密文件", "Effects": "效果", @@ -2760,7 +2760,7 @@ "Explore rooms in %(communityName)s": "在 %(communityName)s 中探索聊天室", "%(count)s messages deleted.|one": "已删除 %(count)s 条消息。", "%(count)s messages deleted.|other": "已删除 %(count)s 条消息。", - "Cannot create rooms in this community": "无法在此社区中创建聊天室", + "Cannot create rooms in this community": "无法在此社群中创建聊天室", "Upgrade to %(hostSignupBrand)s": "升级至 %(hostSignupBrand)s", "Enter phone number": "输入电话号码", "Enter email address": "输入邮箱地址", @@ -2769,13 +2769,13 @@ "Revoke permissions": "撤销权限", "Take a picture": "拍照", "Enter Security Phrase": "输入安全密语", - "Allow this widget to verify your identity": "允许此挂件验证您的身份", + "Allow this widget to verify your identity": "允许此挂件验证你的身份", "Decline All": "全部拒绝", "Approve": "批准", "This widget would like to:": "此挂件想要:", "Approve widget permissions": "批准挂件权限", "Failed to save space settings.": "空间设置保存失败。", - "Sign into your homeserver": "登录您的主服务器", + "Sign into your homeserver": "登录你的主服务器", "Unable to validate homeserver": "无法验证主服务器", "Invalid URL": "URL 无效", "Modal Widget": "模态框挂件(Modal Widget)", @@ -2793,24 +2793,24 @@ "Widgets": "挂件", "This is the start of .": "这里是 的开始。", "Add a photo, so people can easily spot your room.": "添加图片,让人们一眼就能看到你的聊天室。", - "You can change these anytime.": "您随时可以更改它们。", - "Add some details to help people recognise it.": "添加一些细节,以便人们辨识你的社区。", - "Open space for anyone, best for communities": "适合每一个人的开放空间,社区的理想选择", + "You can change these anytime.": "你随时可以更改它们。", + "Add some details to help people recognise it.": "添加一些细节,以便人们辨识你的社群。", + "Open space for anyone, best for communities": "适合每一个人的开放空间,社群的理想选择", "Spaces are new ways to group rooms and people. To join an existing space you'll need an invite.": "空间是为房间和人员分组的新方法。要加入现有的空间,您需要被邀请。", "From %(deviceName)s (%(deviceId)s) at %(ip)s": "来自 %(deviceName)s(%(deviceId)s)于 %(ip)s", "New version of %(brand)s is available": "%(brand)s 有新版本可用", - "You have unverified logins": "您有未验证的登录", + "You have unverified logins": "你有未验证的登录", "You should know": "你应当知道", "Learn more in our , and .": "请通过我们的了解更多信息。", - "Failed to connect to your homeserver. Please close this dialog and try again.": "无法连接至您的主服务器。请关闭此对话框并再试一次。", - "Are you sure you wish to abort creation of the host? The process cannot be continued.": "您确定要放弃创建主机吗?被放弃的创建流程将无法再继续。", + "Failed to connect to your homeserver. Please close this dialog and try again.": "无法连接至你的主服务器。请关闭此对话框并再试一次。", + "Are you sure you wish to abort creation of the host? The process cannot be continued.": "你确定要放弃创建主机吗?被放弃的创建流程将无法再继续。", "Confirm abort of host creation": "确定放弃创建主机", "Please view existing bugs on Github first. No match? Start a new one.": "请先查找一下 Github 上已有的问题,以免重复。找不到重复问题?发起一个吧。", - "PRO TIP: If you start a bug, please submit debug logs to help us track down the problem.": "专业建议:如果您要发起新问题,请一并提交调试日志,以便我们找出问题根源。", + "PRO TIP: If you start a bug, please submit debug logs to help us track down the problem.": "专业建议:如果你要发起新问题,请一并提交调试日志,以便我们找出问题根源。", "There are two ways you can provide feedback and help us improve %(brand)s.": "有两种方式可以提供反馈,并帮助我们改进 %(brand)s。", "Please go into as much detail as you like, so we can track down the problem.": "请按照你的意愿,尽可能详细地描述问题,以便我们找出问题根源。", - "Tell us below how you feel about %(brand)s so far.": "请在下面告诉我们直到目前为止您使用 %(brand)s 的感受。", - "There was an error updating your community. The server is unable to process your request.": "更新你的社区时出现错误。服务器无法处理你的请求。", + "Tell us below how you feel about %(brand)s so far.": "请在下面告诉我们直到目前为止你使用 %(brand)s 的感受。", + "There was an error updating your community. The server is unable to process your request.": "更新你的社群时出现错误。服务器无法处理你的请求。", "Values at explicit levels": "各层级的值", "Values at explicit levels:": "各层级的值:", "Values at explicit levels in this room": "此聊天室中各层级的值", @@ -2824,12 +2824,12 @@ "Value in this room": "此聊天室中的值", "Settings Explorer": "设置浏览器", "with state key %(stateKey)s": "附带有状态键(state key)%(stateKey)s", - "Your server requires encryption to be enabled in private rooms.": "您的服务器要求私人房间启用加密。", - "An image will help people identify your community.": "图片可以让人们辨识您的社区。", - "What's the name of your community or team?": "你的社区或者团队的名称是什么?", - "You can change this later if needed.": "如果需要,您可以稍后更改。", - "Use this when referencing your community to others. The community ID cannot be changed.": "在将您的社区推荐给其他人时使用此 ID,社区 ID 不能更改。", - "There was an error creating your community. The name may be taken or the server is unable to process your request.": "创建社区时发生错误。名称可能已被使用,或者服务器无法处理您的请求。", + "Your server requires encryption to be enabled in private rooms.": "你的服务器要求私人房间启用加密。", + "An image will help people identify your community.": "图片可以让人们辨识你的社群。", + "What's the name of your community or team?": "你的社群或者团队的名称是什么?", + "You can change this later if needed.": "如果需要,你可以稍后更改。", + "Use this when referencing your community to others. The community ID cannot be changed.": "在将你的社群推荐给其他人时使用此 ID,社群 ID 不能更改。", + "There was an error creating your community. The name may be taken or the server is unable to process your request.": "创建社群时发生错误。名称可能已被使用,或者服务器无法处理你的请求。", "Invite people to join %(communityName)s": "邀请人们加入 %(communityName)s", "Send %(count)s invites|one": "发送 %(count)s 个邀请", "Send %(count)s invites|other": "发送 %(count)s 个邀请", @@ -2864,7 +2864,7 @@ "Add comment": "添加备注", "Rate %(brand)s": "评价 %(brand)s", "Feedback sent": "反馈已发送", - "Update community": "更新社区", + "Update community": "更新社群", "Failed to save settings": "设置保存失败", "Create a room in %(communityName)s": "在 %(communityName)s 中创建聊天室", "Add image (optional)": "添加图片(可选)", @@ -2879,7 +2879,7 @@ "Invite with email or username": "使用邮箱或者用户名邀请", "Invite people": "邀请人们", "Update %(brand)s": "更新 %(brand)s", - "Check your devices": "检查您的设备", + "Check your devices": "检查你的设备", "Zimbabwe": "津巴布韦", "Zambia": "赞比亚", "Western Sahara": "西撒哈拉", @@ -2960,16 +2960,16 @@ "Ignored attempt to disable encryption": "已忽略禁用加密的尝试", "Messages in this room are end-to-end encrypted. When people join, you can verify them in their profile, just tap on their avatar.": "此聊天室中的消息已被端对端加密。当人们加入,你可以点击他们的头像,在他们的资料中验证他们。", "Messages here are end-to-end encrypted. Verify %(displayName)s in their profile - tap on their avatar.": "此处的消息已被端对端加密。请点击对方头像,在其资料中验证 %(displayName)s。", - "Secure your backup with a Security Phrase": "使用安全密语保护您的备份", - "Confirm your Security Phrase": "确认您的安全密语", + "Secure your backup with a Security Phrase": "使用安全密语保护你的备份", + "Confirm your Security Phrase": "确认你的安全密语", "Verify with another session": "使用另一个会话验证", "Use Security Key or Phrase": "使用安全密钥或密语", "Continue with %(ssoButtons)s": "使用 %(ssoButtons)s 继续", "There was a problem communicating with the homeserver, please try again later.": "与主服务器通讯时出现问题,请稍后再试。", "Decrypted event source": "解密事件源码", "Original event source": "原始事件源码", - "Community and user menu": "社区与用户菜单", - "Community settings": "社区设置", + "Community and user menu": "社群与用户菜单", + "Community settings": "社群设置", "Invite by username": "按照用户名邀请", "Inviting...": "正在邀请…", "Welcome to ": "欢迎来到 ", @@ -3013,5 +3013,266 @@ "Guyana": "圭亚那", "Guinea-Bissau": "几内亚比绍", "Guinea": "几内亚", - "Guernsey": "根西岛" + "Guernsey": "根西岛", + "You might enable this if the room will only be used for collaborating with internal teams on your homeserver. This cannot be changed later.": "你可以启用此选项如果此聊天室将仅用于你的主服务器上的内部团队协作。此选项之后无法更改。", + "Unable to access secret storage. Please verify that you entered the correct Security Phrase.": "无法访问秘密存储。请确认你输入了正确的恢复密码。", + "Backup could not be decrypted with this Security Key: please verify that you entered the correct Security Key.": "无法使用此安全密钥解密备份:请检查你输入的安全密钥是否正确。", + "sends space invaders": "发送空间入侵者", + "This session has detected that your Security Phrase and key for Secure Messages have been removed.": "此会话已检测到你的安全短语和安全消息密钥被移除。", + "A new Security Phrase and key for Secure Messages have been detected.": "检测到新的安全短语和安全信息密钥。", + "Make a copy of your Security Key": "复制你的安全密钥", + "Your Security Key is in your Downloads folder.": "你的安全密钥在你的下载文件夹中。", + "Your Security Key has been copied to your clipboard, paste it to:": "你的安全密钥已 复制到你的粘贴板,将其粘贴至:", + "Your Security Key is a safety net - you can use it to restore access to your encrypted messages if you forget your Security Phrase.": "你的安全密钥是一张安全网——如果你忘记了你的安全短语的话,你可以用它来恢复你对已加密消息的访问权。", + "Repeat your Security Phrase...": "重复你的安全短语……", + "Enter your Security Phrase a second time to confirm it.": "再次输入你的安全短语进行确认。", + "Set up with a Security Key": "设置安全密钥", + "Great! This Security Phrase looks strong enough.": "棒!这个安全短语看着够强。", + "We'll store an encrypted copy of your keys on our server. Secure your backup with a Security Phrase.": "我们会将你的密钥的加密副本保存在我们的服务器上。使用安全短语来保证你的备份。", + "Space Autocomplete": "空间自动完成", + "Without verifying, you won’t have access to all your messages and may appear as untrusted to others.": "未经验证,你将无法获取你的所有信息,且可能不被他人所信任。", + "Verify your identity to access encrypted messages and prove your identity to others.": "验证你的身份来获取已加密的消息并向其他人证明你的身份。", + "Use another login": "使用其他账号登录", + "Decide where your account is hosted": "决定账号托管位置", + "Host account on": "账号托管于", + "Already have an account? Sign in here": "已有账号?在此登录", + "That username already exists, please try another.": "用户名已存在,请试试别的。", + "New? Create account": "新来的?创建账号", + "Please choose a strong password": "请选择强密码", + "New here? Create an account": "新来的?创建账号", + "Got an account? Sign in": "有账号了?登录", + "Failed to find the general chat for this community": "找不到此社群的一般性聊天记录", + "We'll create rooms for each of them. You can add more later too, including already existing ones.": "我们将会为每个主题创建一个聊天室。你也可以稍后再进行添加,包括现有聊天室。", + "What projects are you working on?": "你正在从事哪些项目?", + "You can add more later too, including already existing ones.": "稍后你可以添加更多聊天室,包括现有的。", + "Let's create a room for each of them.": "让我们未每个主题都创建一个聊天室吧。", + "What are some things you want to discuss in %(spaceName)s?": "你想在 %(spaceName)s 中讨论什么?", + "This is an experimental feature. For now, new users receiving an invite will have to open the invite on to actually join.": "这是实验性功能。当前收到邀请的新用户必须在上开启邀请才能真正加入。", + "Make sure the right people have access. You can invite more later.": "确保对的人可以访问。稍后你可以邀请更多人。", + "Invite your teammates": "邀请你的伙伴", + "Failed to invite the following users to your space: %(csvUsers)s": "邀请以下用户加入你的空间失败:%(csvUsers)s", + "A private space for you and your teammates": "供你和你的伙伴使用的私有空间", + "Me and my teammates": "我和我的伙伴", + "A private space to organise your rooms": "用于整理你聊天室的私有空间", + "Just me": "仅有我", + "Make sure the right people have access to %(name)s": "确保对的人有权访问 %(name)s", + "Who are you working with?": "你与谁一同工作?", + "Go to my space": "前往我的空间", + "Go to my first room": "前往我的第一个聊天室", + "It's just you at the moment, it will be even better with others.": "当前仅有你一人,与人同道而行会更好。", + "Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.": "选择要添加的聊天室或对话。这是专属于你的空间,不会有人被通知。你稍后可以再增加更多。", + "Select a room below first": "首先选择一个聊天室", + "%(count)s rooms and %(numSpaces)s spaces|other": "%(count)s 个聊天室和 %(numSpaces)s 个空间", + "This room is suggested as a good one to join": "此聊天室很适合加入", + "You can select all or individual messages to retry or delete": "你可以选择全部或单独的消息来重试或删除", + "Sending": "正在发送", + "Delete all": "删除全部", + "Some of your messages have not been sent": "你的部分消息未被发送", + "Your message wasn't sent because this homeserver has been blocked by it's administrator. Please contact your service administrator to continue using the service.": "你的消息未被发送,因为此主服务器已被其管理员封禁。请联络你的服务管理员已继续使用服务。", + "Filter all spaces": "过滤所有空间", + "You have no visible notifications.": "你没有可见的通知。", + "Communities are changing to Spaces": "社群正在转变为空间", + "%(creator)s created this DM.": "%(creator)s 创建了此私聊。", + "Verification requested": "已请求验证", + "Security Key mismatch": "安全密钥不符", + "Unable to set up keys": "无法设置密钥", + "If you reset everything, you will restart with no trusted sessions, no trusted users, and might not be able to see past messages.": "如果你全部重置,你将会在没有受信任的会话重新开始、没有受信任的用户,且可能会看不到过去的消息。", + "Only do this if you have no other device to complete verification with.": "当你没有其他设备可以用于完成验证时,方可执行此操作。", + "Reset everything": "全部重置", + "Forgotten or lost all recovery methods? Reset all": "忘记或丢失了所有恢复方式?全部重置", + "Remember this": "记住", + "The widget will verify your user ID, but won't be able to perform actions for you:": "挂件将会验证你的用户 ID,但将无法为你执行动作:", + "Verify other login": "验证其他登录", + "Make this space private": "将此空间设为私有", + "Edit settings relating to your space.": "编辑关于你的空间的设置。", + "Reset event store": "重置活动存储", + "If you do, please note that none of your messages will be deleted, but the search experience might be degraded for a few moments whilst the index is recreated": "如果这样做,请注意你的信息并不会被删除,但在重新建立索引是,搜索体验可能会退步一些", + "You most likely do not want to reset your event index store": "你大概率不想重置你的活动缩影存储", + "Reset event store?": "重置活动存储?", + "Use your preferred Matrix homeserver if you have one, or host your own.": "如果你可以使用自己所偏好的 Matrix 主服务器,或是自己搭建一个。", + "We call the places where you can host your account ‘homeservers’.": "我们将你可以托管账号的地方称为「主服务器」。", + "Matrix.org is the biggest public homeserver in the world, so it’s a good place for many.": "Matrix.org 是世界上最大的公开主服务器,因此对于许多人而言是个好地方。", + "This usually only affects how the room is processed on the server. If you're having problems with your %(brand)s, please report a bug.": "这通常仅影响服务器如何处理聊天室。如果你的 %(brand)s 遇到问题,请回报错误。", + "Just a heads up, if you don't add an email and forget your password, you could permanently lose access to your account.": "请注意,如果你不添加电子邮箱并且忘记密码,你将永远失去对你账号的访问权。", + "Continuing without email": "不使用电子邮箱并继续", + "We recommend you change your password and Security Key in Settings immediately": "我们建议你立即在设置中更改你的密码和安全密钥", + "Data on this screen is shared with %(widgetDomain)s": "在此画面上的资料会与 %(widgetDomain)s 分享", + "Consult first": "先询问", + "Invited people will be able to read old messages.": "被邀请的人将能够阅读过去的消息。", + "Invite someone using their name, username (like ) or share this room.": "使用某人的名字、用户名(如 )或分享此聊天室来邀请他们。", + "Invite someone using their name, email address, username (like ) or share this room.": "使用某人的名字、电子邮箱地址或用户名来与他们开始对话(如 )或分享此聊天室。", + "Invite someone using their name, username (like ) or share this space.": "使用某人的名字、用户名(如 )邀请他们,或分享此空间。", + "Invite someone using their name, email address, username (like ) or share this space.": "使用某人的名字、电子邮箱地址或用户名(如 )邀请他们,或分享此空间。", + "This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click here": "这不会邀请他们加入 %(communityName)s。要邀请某人加入 %(communityName)s,请点击这里", + "Start a conversation with someone using their name or username (like ).": "使用某人的名字或用户名开始与其进行对话(如 )。", + "Start a conversation with someone using their name, email address or username (like ).": "使用某人的名称、电子邮箱地址或用户名来与其开始对话(如 )。", + "May include members not in %(communityName)s": "可能不包括 %(communityName)s 中的成员", + "A call can only be transferred to a single user.": "通话只能转移到单个用户。", + "We couldn't create your DM.": "我们无法创建你的私聊。", + "Continuing temporarily allows the %(hostSignupBrand)s setup process to access your account to fetch verified email addresses. This data is not stored.": "继续暂时允许让 %(hostSignupBrand)s 安装过程可以访问你的账号以获取已验证电子邮箱地址。此数据不保存。", + "Block anyone not part of %(serverName)s from ever joining this room.": "阻住任何不属于 %(serverName)s 的人加入此聊天室。", + "You might disable this if the room will be used for collaborating with external teams who have their own homeserver. This cannot be changed later.": "如果聊天室用于与自己的主服务器外的团队进行协助的话,可以停用此功能。这将无法在稍后进行更改。", + "What do you want to organise?": "你想要组织什么?", + "Skip for now": "暂时跳过", + "Failed to create initial space rooms": "创建初始空间聊天室失败", + "To join %(spaceName)s, turn on the Spaces beta": "加入 %(spaceName)s 前,请开启空间测试版", + "To view %(spaceName)s, turn on the Spaces beta": "查看 %(spaceName)s 前,请开启空间测试版", + "Private space": "私有空间", + "Public space": "公开空间", + "Spaces are a beta feature.": "空间为测试版功能。", + "If you can't find the room you're looking for, ask for an invite or create a new room.": "如果你找不到正在寻找的聊天室,请请求邀请或创建一个新的聊天室。", + "Private rooms can be found and joined by invitation only. Public rooms can be found and joined by anyone.": "私人聊天室仅能通过邀请找到与加入。公开聊天室则能够被所有人找到并加入。", + "Private rooms can be found and joined by invitation only. Public rooms can be found and joined by anyone in this community.": "私人聊天室仅能通过邀请找到与加入。公开聊天室则能够被所有在此社群的人找到并加入。", + "Search names and descriptions": "搜索名称和描述", + "You may want to try a different search or check for typos.": "你可能要尝试其他搜索或检查是否有错别字。", + "You may contact me if you have any follow up questions": "如果你有任何后续问题,可以联系我", + "To leave the beta, visit your settings.": "要退出测试版,请访问你的设置。", + "Your platform and username will be noted to help us use your feedback as much as we can.": "我们将会记录你的平台及用户名,以帮助我们尽我们所能地使用你的反馈。", + "%(featureName)s beta feedback": "%(featureName)s 测试版反馈", + "Thank you for your feedback, we really appreciate it.": "感谢你的反馈,我们衷心地感谢。", + "Beta feedback": "测试版反馈", + "Want to add a new room instead?": "想要添加一个新的聊天室吗?", + "Add existing rooms": "添加现有聊天室", + "You can add existing spaces to a space.": "你可以添加现有空间到另一空间中。", + "Feeling experimental?": "想要来点实验吗?", + "Adding rooms... (%(progress)s out of %(count)s)|one": "正在新增聊天室……", + "Adding rooms... (%(progress)s out of %(count)s)|other": "正在新增聊天室……(%(count)s 中的第 %(progress)s 个)", + "Not all selected were added": "并非所有选中的都被添加", + "You are not allowed to view this server's rooms list": "你不被允许查看此服务器的聊天室列表", + "Sends the given message with a space themed effect": "此消息带有空间主题化效果", + "Kick, ban, or invite people to your active room, and make you leave": "移除、封禁或邀请人们到你所活跃的聊天室,并让你离开", + "Sends the given message as a spoiler": "此消息包含剧透", + "Retry all": "全部重试", + "View message": "查看消息", + "Zoom in": "放大", + "Zoom out": "缩小", + "%(count)s people you know have already joined|one": "已有你所认识的 %(count)s 个人加入", + "%(count)s people you know have already joined|other": "已有你所认识的 %(count)s 个人加入", + "%(count)s members including %(commaSeparatedMembers)s|one": "%(commaSeparatedMembers)s", + "%(count)s members including %(commaSeparatedMembers)s|other": "%(count)s 位成员中包括 %(commaSeparatedMembers)s", + "Including %(commaSeparatedMembers)s": "包括 %(commaSeparatedMembers)s", + "View all %(count)s members|one": "查看 1 位成员", + "View all %(count)s members|other": "查看全部 %(count)s 位成员", + "Use the Desktop app to search encrypted messages": "使用桌面端英语来搜索加密消息", + "Use the Desktop app to see all encrypted files": "使用桌面端应用来查看所有加密文件", + "Add reaction": "添加回应", + "Error processing voice message": "处理语音消息时发生错误", + "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the space it will be impossible to regain privileges.": "当你将自己降级后,你将无法撤销此更改。如果你是此空间的最后一名拥有权限的用户,则无法重新获得权限。", + "Unpin a widget to view it in this panel": "取消固定挂件以在此面板中查看", + "You can only pin up to %(count)s widgets|other": "你仅能固定 %(count)s 个挂件", + "Accept on your other login…": "接受你的其他登录……", + "Delete recording": "删除录制", + "Stop the recording": "停止录制", + "Record a voice message": "录制语音消息", + "We didn't find a microphone on your device. Please check your settings and try again.": "我们没能在你的设备上找到麦克风。请检查设置并重试。", + "No microphone found": "无法发现麦克风", + "We were unable to access your microphone. Please check your browser settings and try again.": "我们无法访问你的麦克风。 请检查浏览器设置并重试。", + "Unable to access your microphone": "无法访问你的麦克风", + "%(count)s results in all spaces|one": "在所有空间中有 %(count)s 个结果", + "%(count)s results in all spaces|other": "在所有空间中有 %(count)s 个结果", + "Quick actions": "快捷操作", + "You do not have permissions to add rooms to this space": "你没有权限添加聊天室至此空间", + "You do not have permissions to create new rooms in this space": "你没有权限在此空间内创建新的聊天室", + "Invite to just this room": "仅邀请至此聊天室", + "This is the beginning of your direct message history with .": "这是你与 间进行私聊的历史记录的开始。", + "Only the two of you are in this conversation, unless either of you invites anyone to join.": "除非你们其中一个邀请了别人加入,否则将仅有你们两个人在此对话中。", + "%(seconds)ss left": "剩余 %(seconds)s 秒", + "Failed to send": "发送失败", + "Change server ACLs": "更改服务器访问控制列表", + "You have no ignored users.": "你没有设置忽略用户。", + "Warn before quitting": "退出前警告", + "Feeling experimental? Labs are the best way to get things early, test out new features and help shape them before they actually launch. Learn more.": "想要来点实验?实验室是提前体验、测试新功能并在它们正式发布前帮助它们定型的最佳方式。了解更多。", + "Your access token gives full access to your account. Do not share it with anyone.": "你的访问令牌可以完全访问你的帐户。不要将其与任何人分享。", + "Access Token": "访问令牌", + "Message search initialisation failed": "消息搜索初始化失败", + "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|one": "使用 %(size)s 来存储来自 %(rooms)s 聊天室的消息。在本地安全地缓存已加密的消息以使其出现在搜索结果中。", + "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|other": "使用 %(size)s 来存储来自 %(rooms)s 聊天室的消息。在本地安全地缓存已加密的消息以使其出现在搜索结果中。", + "Manage & explore rooms": "管理并探索聊天室", + "Spaces are a new way to group rooms and people. To join an existing space you'll need an invite.": "空间是一种将聊天室和人们进行分组的新方法。你需要得到邀请方可加入现有空间。", + "Please enter a name for the space": "请输入空间名称", + "Play": "播放", + "Pause": "暂停", + "%(name)s on hold": "保留 %(name)s", + "Connecting": "连接中", + "Consulting with %(transferTarget)s. Transfer to %(transferee)s": "与 %(transferTarget)s 进行协商。转让至 %(transferee)s", + "unknown person": "陌生人", + "Allow Peer-to-Peer for 1:1 calls (if you enable this, the other party might be able to see your IP address)": "允许在一对一通话中使用点对点通讯(如果你启用此功能,对方可能会看到你的 IP 地址)", + "Send and receive voice messages": "发送并接收语音消息", + "Show options to enable 'Do not disturb' mode": "显示启用「请勿打扰」模式的选项", + "Your feedback will help make spaces better. The more detail you can go into, the better.": "你的反馈将帮助空间变得更好。你能讲得越仔细越好。", + "Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.": "测试版适用于网页端、桌面端以及安卓端。但在你的主服务器上有些特性可能不可用。", + "You can leave the beta any time from settings or tapping on a beta badge, like the one above.": "你随时可以在设置中退出测试版,或轻点如上所示的测试版徽章。", + "%(brand)s will reload with Spaces enabled. Communities and custom tags will be hidden.": "%(brand)s 将在空间启用时重载。社群和自定义标签将被隐藏。", + "Beta available for web, desktop and Android. Thank you for trying the beta.": "测试版适用于网页端、桌面端以及安卓端。感谢你试用测试版。", + "If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "如果你离开,%(brand)s 将会在空间禁用时重载。社群和自定义标记将再次可见。", + "Spaces are a new way to group rooms and people.": "空间是一种将聊天室与人们进行分组的新方式。", + "%(deviceId)s from %(ip)s": "来自 %(ip)s 的 %(deviceId)s", + "Review to ensure your account is safe": "检查以确保你的账号是安全的", + "See %(msgtype)s messages posted to your active room": "查看发布到你所活跃的聊天室的 %(msgtype)s 消息", + "See %(msgtype)s messages posted to this room": "查看发布到此聊天室的 %(msgtype)s 消息", + "Send %(msgtype)s messages as you in your active room": "在你所活跃的聊天室以你的身份发送 %(msgtype)s 消息", + "Send %(msgtype)s messages as you in this room": "在此聊天室以你的身份发送 %(msgtype)s 消息", + "See general files posted to your active room": "查看发布到你所活跃的聊天室的一般性文件", + "See general files posted to this room": "查看发布到此聊天室的一般性文件", + "Send general files as you in your active room": "在你所活跃的聊天室以你的身份发送一般性文件", + "Are you sure you want to leave the space '%(spaceName)s'?": "你确定要离开空间「%(spaceName)s」吗?", + "This space is not public. You will not be able to rejoin without an invite.": "此空间并不公开。在没有得到邀请的情况下,你将无法重新加入。", + "You are the only person here. If you leave, no one will be able to join in the future, including you.": "你是这里唯一的人。如果你离开了,以后包括你以在内任何人都将无法加入。", + "You do not have permission to create rooms in this community.": "你没有在此社群中建立聊天室的权限。", + "Now, let's help you get started": "现在,让我们协助你开始", + "Add a photo so people know it's you.": "添加照片,让人们知道这是你。", + "Great, that'll help people know it's you": "很好,这样大家就知道是你了", + "

      HTML for your community's page

      \n

      \n Use the long description to introduce new members to the community, or distribute\n some important links\n

      \n

      \n You can even add images with Matrix URLs \n

      \n": "

      社群页面的 HTML

      \n

      \n 使用详细秒速向社群介绍新成员,或分发\n 一些重要链接\n

      \n

      \n 你甚至可以使用 Matrix 链接 新增图片\n

      \n", + "Use email to optionally be discoverable by existing contacts.": "使用电子邮箱以选择性地被现有联系人搜索。", + "Use email or phone to optionally be discoverable by existing contacts.": "使用电子邮箱或电话以选择性地被现有联系人搜索。", + "Add an email to be able to reset your password.": "添加电子邮箱以重置你的密码。", + "That phone number doesn't look quite right, please check and try again": "电话号码看起来不太对,请检查并重试", + "Something went wrong in confirming your identity. Cancel and try again.": "确认你的身份时出了一点问题。取消并重试。", + "Open the link in the email to continue registration.": "打开电子邮件中的链接以继续注册。", + "A confirmation email has been sent to %(emailAddress)s": "确认电子邮件以发送至 %(emailAddress)s", + "Avatar": "头像", + "Join the beta": "加入测试版", + "Leave the beta": "退出测试版", + "Beta": "测试版", + "Tap for more info": "点击以获取更多信息", + "Spaces is a beta feature": "空间为测试功能", + "Start audio stream": "开始音频流", + "Failed to start livestream": "开始流直播失败", + "Unable to start audio streaming.": "无法开始音频流媒体。", + "Hold": "挂起", + "Resume": "恢复", + "If you've forgotten your Security Key you can ": "如果你忘记了你的安全密钥,你可以", + "Access your secure message history and set up secure messaging by entering your Security Key.": "通过输入你的安全密钥来访问你的安全消息历史记录并设置安全通信。", + "Not a valid Security Key": "安全密钥无效", + "This looks like a valid Security Key!": "看起来是有效的安全密钥!", + "If you've forgotten your Security Phrase you can use your Security Key or set up new recovery options": "如果你忘记了你的安全短语,你可以使用你的安全密钥设置新的恢复选项", + "Access your secure message history and set up secure messaging by entering your Security Phrase.": "无法通过你的安全短语访问你的安全消息历史记录并设置安全通信。", + "Backup could not be decrypted with this Security Phrase: please verify that you entered the correct Security Phrase.": "无法使用此安全短语解密备份:请确认你是否输入了正确的安全短语。", + "Incorrect Security Phrase": "安全短语错误", + "Send general files as you in this room": "查看发布到此聊天室的一般性文件", + "See videos posted to your active room": "查看发布到你所活跃的聊天室的视频", + "See videos posted to this room": "查看发布到此聊天室的视频", + "Send videos as you in your active room": "查看发布到你所活跃的聊天室的视频", + "Send videos as you in this room": "查看发布到此聊天室的视频", + "See images posted to your active room": "查看发布到你所活跃的聊天室的图片", + "See images posted to this room": "查看发布到此聊天室的图片", + "Send images as you in your active room": "在你所活跃的聊天室以你的身份发送图片", + "Send images as you in this room": "在此聊天室以你的身份发送图片", + "See emotes posted to your active room": "查看发布到你所活跃的聊天室的表情", + "See emotes posted to this room": "查看发布到此聊天室的表情", + "Send emotes as you in your active room": "在你所活跃的聊天室以你的身份发送表情", + "Send emotes as you in this room": "在此聊天室以你的身份发送表情", + "See text messages posted to your active room": "查看发布到你所活跃的聊天室的文本消息", + "See text messages posted to this room": "查看发布到此聊天室的文本消息", + "Send text messages as you in your active room": "在你所活跃的聊天室以你的身份发送文本消息", + "Send text messages as you in this room": "在此聊天室以你的身份发送文本消息", + "See messages posted to your active room": "查看发布到你所活跃的聊天室的消息", + "See messages posted to this room": "查看发布到此聊天室的消息", + "Send messages as you in your active room": "在你所活跃的聊天室以你的身份发送消息", + "Send messages as you in this room": "在此聊天室以你的身份发送消息", + "See when anyone posts a sticker to your active room": "查看何时有人发送贴纸到你所活跃的聊天室", + "Send stickers to your active room as you": "发送贴纸到你所活跃的聊天室", + "See when people join, leave, or are invited to your active room": "查看人们何时加入、离开或被邀请到你所活跃的聊天室", + "See when people join, leave, or are invited to this room": "查看人们何时加入、离开或被邀请到这个房间", + "Kick, ban, or invite people to this room, and make you leave": "移除、封禁或邀请用户到此聊天室,并让你离开" } From dfa8d1a8ac0aa7256e3993ee7faede5861b6ac7c Mon Sep 17 00:00:00 2001 From: c-cal Date: Tue, 1 Jun 2021 14:51:53 +0000 Subject: [PATCH 317/999] Translated using Weblate (French) Currently translated at 99.7% (2973 of 2979 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/fr/ --- src/i18n/strings/fr.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index 6f7221dbe7..5a8208f50b 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -3175,8 +3175,8 @@ "Delete": "Supprimer", "Jump to the bottom of the timeline when you send a message": "Sauter en bas du fil de discussion lorsque vous envoyez un message", "Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags. Requires compatible homeserver for some features.": "Prototype d’espaces. Incompatible avec les communautés, les communautés v2 et les étiquettes personnalisées. Nécessite un serveur d’accueil compatible pour certaines fonctionnalités.", - "This homeserver has been blocked by it's administrator.": "Ce serveur d’accueil a été banni par ses administrateurs.", - "This homeserver has been blocked by its administrator.": "Ce serveur d’accueil a été banni par ses administrateurs.", + "This homeserver has been blocked by it's administrator.": "Ce serveur d’accueil a été bloqué par son administrateur.", + "This homeserver has been blocked by its administrator.": "Ce serveur d’accueil a été bloqué par son administrateur.", "You're already in a call with this person.": "Vous êtes déjà en cours d’appel avec cette personne.", "Already in call": "Déjà en cours d’appel", "Space selection": "Sélection d’un espace", From 0c5e8ef381815725656c6619364fb54089a53137 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 1 Jun 2021 16:13:01 +0100 Subject: [PATCH 318/999] Upgrade matrix-js-sdk to 11.2.0-rc.1 --- package.json | 2 +- yarn.lock | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 13047b69cf..108a61cdaf 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,7 @@ "katex": "^0.12.0", "linkifyjs": "^2.1.9", "lodash": "^4.17.20", - "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", + "matrix-js-sdk": "11.2.0-rc.1", "matrix-widget-api": "^0.1.0-beta.14", "minimist": "^1.2.5", "opus-recorder": "^8.0.3", diff --git a/yarn.lock b/yarn.lock index 0ff235a660..1b07d4502b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5673,9 +5673,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@github:matrix-org/matrix-js-sdk#develop": - version "11.1.0" - resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/acb9bc8cc5234326a7583514a8e120a4ac42eedc" +matrix-js-sdk@11.2.0-rc.1: + version "11.2.0-rc.1" + resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-11.2.0-rc.1.tgz#339259d892ce08195864c1130780dd0b3d996af3" + integrity sha512-ACFONqdRqiPIj7aWf7G4f/2d0S/hfouIH6tdkFGDbizbWJCmqXRJ8EN2gmu0F3A6GGDo0+k9cqaeMf3Y1KIM8w== dependencies: "@babel/runtime" "^7.12.5" another-json "^0.2.0" From d02f462df9a3cd0a336aea733c2992abcbc07b2d Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 1 Jun 2021 16:18:25 +0100 Subject: [PATCH 319/999] Prepare changelog for v3.23.0-rc.1 --- CHANGELOG.md | 98 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f3d9afd51d..a3abd1f32b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,101 @@ +Changes in [3.23.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.23.0-rc.1) (2021-06-01) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.22.0...v3.23.0-rc.1) + + * Upgrade to JS SDK 11.2.0-rc.1 + * Translations update from Weblate + [\#6128](https://github.com/matrix-org/matrix-react-sdk/pull/6128) + * Fix all DMs wrongly appearing in room list when `m.direct` is changed + [\#6122](https://github.com/matrix-org/matrix-react-sdk/pull/6122) + * Update way of checking for registration disabled + [\#6123](https://github.com/matrix-org/matrix-react-sdk/pull/6123) + * Fix the ability to remove avatar from a space via settings + [\#6126](https://github.com/matrix-org/matrix-react-sdk/pull/6126) + * Switch to stable endpoint/fields for MSC2858 + [\#6125](https://github.com/matrix-org/matrix-react-sdk/pull/6125) + * Clear stored editor state when canceling editing using a shortcut + [\#6117](https://github.com/matrix-org/matrix-react-sdk/pull/6117) + * Respect newlines in space topics + [\#6124](https://github.com/matrix-org/matrix-react-sdk/pull/6124) + * Add url param `defaultUsername` to prefill the login username field + [\#5674](https://github.com/matrix-org/matrix-react-sdk/pull/5674) + * Bump ws from 7.4.2 to 7.4.6 + [\#6115](https://github.com/matrix-org/matrix-react-sdk/pull/6115) + * Sticky headers repositioning without layout trashing + [\#6110](https://github.com/matrix-org/matrix-react-sdk/pull/6110) + * Handle user_busy in voip calls + [\#6112](https://github.com/matrix-org/matrix-react-sdk/pull/6112) + * Avoid showing warning modals from the invite dialog after it unmounts + [\#6105](https://github.com/matrix-org/matrix-react-sdk/pull/6105) + * Fix misleading child counts in spaces + [\#6109](https://github.com/matrix-org/matrix-react-sdk/pull/6109) + * Close creation menu when expanding space panel via expand hierarchy + [\#6090](https://github.com/matrix-org/matrix-react-sdk/pull/6090) + * Prevent having duplicates in pending room state + [\#6108](https://github.com/matrix-org/matrix-react-sdk/pull/6108) + * Update reactions row on event decryption + [\#6106](https://github.com/matrix-org/matrix-react-sdk/pull/6106) + * Destroy playback instance on voice message unmount + [\#6101](https://github.com/matrix-org/matrix-react-sdk/pull/6101) + * Fix message preview not up to date + [\#6102](https://github.com/matrix-org/matrix-react-sdk/pull/6102) + * Convert some Flow typed files to TS (round 2) + [\#6076](https://github.com/matrix-org/matrix-react-sdk/pull/6076) + * Remove unused middlePanelResized event listener + [\#6086](https://github.com/matrix-org/matrix-react-sdk/pull/6086) + * Fix accessing currentState on an invalid joinedRoom + [\#6100](https://github.com/matrix-org/matrix-react-sdk/pull/6100) + * Remove Promise allSettled polyfill as js-sdk uses it directly + [\#6097](https://github.com/matrix-org/matrix-react-sdk/pull/6097) + * Prevent DecoratedRoomAvatar to update its state for the same value + [\#6099](https://github.com/matrix-org/matrix-react-sdk/pull/6099) + * Skip generatePreview if event is not part of the live timeline + [\#6098](https://github.com/matrix-org/matrix-react-sdk/pull/6098) + * fix sticky headers when results num get displayed + [\#6095](https://github.com/matrix-org/matrix-react-sdk/pull/6095) + * Improve addEventsToTimeline performance scoping WhoIsTypingTile::setState + [\#6094](https://github.com/matrix-org/matrix-react-sdk/pull/6094) + * Safeguards to prevent layout trashing for window dimensions + [\#6092](https://github.com/matrix-org/matrix-react-sdk/pull/6092) + * Use local room state to render space hierarchy if the room is known + [\#6089](https://github.com/matrix-org/matrix-react-sdk/pull/6089) + * Add spinner in UserMenu to list pending long running actions + [\#6085](https://github.com/matrix-org/matrix-react-sdk/pull/6085) + * Stop overscroll in Firefox Nightly for macOS + [\#6093](https://github.com/matrix-org/matrix-react-sdk/pull/6093) + * Move SettingsStore watchers/monitors over to ES6 maps for performance + [\#6063](https://github.com/matrix-org/matrix-react-sdk/pull/6063) + * Bump libolm version. + [\#6080](https://github.com/matrix-org/matrix-react-sdk/pull/6080) + * Improve styling of the message action bar + [\#6066](https://github.com/matrix-org/matrix-react-sdk/pull/6066) + * Improve explore rooms when no results are found + [\#6070](https://github.com/matrix-org/matrix-react-sdk/pull/6070) + * Remove logo spinner + [\#6078](https://github.com/matrix-org/matrix-react-sdk/pull/6078) + * Fix add reaction prompt showing even when user is not joined to room + [\#6073](https://github.com/matrix-org/matrix-react-sdk/pull/6073) + * Vectorize spinners + [\#5680](https://github.com/matrix-org/matrix-react-sdk/pull/5680) + * Fix handling of via servers for suggested rooms + [\#6077](https://github.com/matrix-org/matrix-react-sdk/pull/6077) + * Upgrade showChatEffects to room-level setting exposure + [\#6075](https://github.com/matrix-org/matrix-react-sdk/pull/6075) + * Delete RoomView dead code + [\#6071](https://github.com/matrix-org/matrix-react-sdk/pull/6071) + * Reduce noise in tests + [\#6074](https://github.com/matrix-org/matrix-react-sdk/pull/6074) + * Fix room name issues in right panel summary card + [\#6069](https://github.com/matrix-org/matrix-react-sdk/pull/6069) + * Cache normalized room name + [\#6072](https://github.com/matrix-org/matrix-react-sdk/pull/6072) + * Update MemberList to reflect changes for invite permission change + [\#6061](https://github.com/matrix-org/matrix-react-sdk/pull/6061) + * Delete RoomView dead code + [\#6065](https://github.com/matrix-org/matrix-react-sdk/pull/6065) + * Show subspace rooms count even if it is 0 for consistency + [\#6067](https://github.com/matrix-org/matrix-react-sdk/pull/6067) + Changes in [3.22.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.22.0) (2021-05-24) ===================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.22.0-rc.1...v3.22.0) From 300b0016d60fd3f3b0527e68f987df3731f2feb0 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 1 Jun 2021 16:18:26 +0100 Subject: [PATCH 320/999] v3.23.0-rc.1 --- package.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 108a61cdaf..4f6a023383 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "3.22.0", + "version": "3.23.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", @@ -200,5 +200,6 @@ "coverageReporters": [ "text" ] - } + }, + "typings": "./lib/index.d.ts" } From 0b7d3f007aece9a15d53e53f0f953fa203da8457 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 1 Jun 2021 17:30:57 +0100 Subject: [PATCH 321/999] Remove react-beautiful-dnd --- package.json | 1 - src/components/structures/GroupFilterPanel.js | 32 ++--- src/components/structures/LoggedInView.tsx | 65 ++------- src/components/views/elements/DNDTagTile.js | 32 ++--- .../views/groups/GroupPublicityToggle.js | 4 +- src/components/views/groups/GroupTile.js | 44 +----- test/components/views/rooms/RoomList-test.js | 5 +- yarn.lock | 130 +----------------- 8 files changed, 36 insertions(+), 277 deletions(-) diff --git a/package.json b/package.json index 13047b69cf..270c86ddba 100644 --- a/package.json +++ b/package.json @@ -91,7 +91,6 @@ "qs": "^6.9.6", "re-resizable": "^6.9.0", "react": "^16.14.0", - "react-beautiful-dnd": "^4.0.1", "react-dom": "^16.14.0", "react-focus-lock": "^2.5.0", "react-transition-group": "^4.4.1", diff --git a/src/components/structures/GroupFilterPanel.js b/src/components/structures/GroupFilterPanel.js index 2ff91e4976..f1c28d588a 100644 --- a/src/components/structures/GroupFilterPanel.js +++ b/src/components/structures/GroupFilterPanel.js @@ -24,7 +24,6 @@ import * as sdk from '../../index'; import dis from '../../dispatcher/dispatcher'; import { _t } from '../../languageHandler'; -import { Droppable } from 'react-beautiful-dnd'; import classNames from 'classnames'; import MatrixClientContext from "../../contexts/MatrixClientContext"; import AutoHideScrollbar from "./AutoHideScrollbar"; @@ -83,7 +82,7 @@ class GroupFilterPanel extends React.Component { } }; - onMouseDown = e => { + onClick = e => { // only dispatch if its not a no-op if (this.state.selectedTags.length > 0) { dis.dispatch({action: 'deselect_tags'}); @@ -151,28 +150,15 @@ class GroupFilterPanel extends React.Component { return
      - - { (provided, snapshot) => ( -
      - { this.renderGlobalIcon() } - { tags } -
      - {createButton} -
      - { provided.placeholder } -
      - ) } -
      +
      + { this.renderGlobalIcon() } + { tags } +
      + { createButton } +
      +
      ; } diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx index ad5c759f0d..f5df99d8c9 100644 --- a/src/components/structures/LoggedInView.tsx +++ b/src/components/structures/LoggedInView.tsx @@ -19,7 +19,6 @@ limitations under the License. import * as React from 'react'; import * as PropTypes from 'prop-types'; import { MatrixClient } from 'matrix-js-sdk/src/client'; -import { DragDropContext } from 'react-beautiful-dnd'; import {Key} from '../../Keyboard'; import PageTypes from '../../PageTypes'; @@ -569,50 +568,6 @@ class LoggedInView extends React.Component { } }; - _onDragEnd = (result) => { - // Dragged to an invalid destination, not onto a droppable - if (!result.destination) { - return; - } - - const dest = result.destination.droppableId; - - if (dest === 'tag-panel-droppable') { - // Could be "GroupTile +groupId:domain" - const draggableId = result.draggableId.split(' ').pop(); - - // Dispatch synchronously so that the GroupFilterPanel receives an - // optimistic update from GroupFilterOrderStore before the previous - // state is shown. - dis.dispatch(TagOrderActions.moveTag( - this._matrixClient, - draggableId, - result.destination.index, - ), true); - } else if (dest.startsWith('room-sub-list-droppable_')) { - this._onRoomTileEndDrag(result); - } - }; - - _onRoomTileEndDrag = (result) => { - let newTag = result.destination.droppableId.split('_')[1]; - let prevTag = result.source.droppableId.split('_')[1]; - if (newTag === 'undefined') newTag = undefined; - if (prevTag === 'undefined') prevTag = undefined; - - const roomId = result.draggableId.split('_')[1]; - - const oldIndex = result.source.index; - const newIndex = result.destination.index; - - dis.dispatch(RoomListActions.tagRoom( - this._matrixClient, - this._matrixClient.getRoom(roomId), - prevTag, newTag, - oldIndex, newIndex, - ), true); - }; - render() { const RoomView = sdk.getComponent('structures.RoomView'); const UserView = sdk.getComponent('structures.UserView'); @@ -679,17 +634,15 @@ class LoggedInView extends React.Component { aria-hidden={this.props.hideToSRUsers} > - -
      - { SettingsStore.getValue("feature_spaces") ? : null } - - - { pageElement } -
      -
      +
      + { SettingsStore.getValue("feature_spaces") ? : null } + + + { pageElement } +
      diff --git a/src/components/views/elements/DNDTagTile.js b/src/components/views/elements/DNDTagTile.js index 67572d4508..eaaa0f183b 100644 --- a/src/components/views/elements/DNDTagTile.js +++ b/src/components/views/elements/DNDTagTile.js @@ -18,7 +18,6 @@ limitations under the License. import TagTile from './TagTile'; import React from 'react'; -import { Draggable } from 'react-beautiful-dnd'; import { ContextMenu, toRightOf, useContextMenu } from "../../structures/ContextMenu"; import * as sdk from '../../../index'; @@ -35,28 +34,13 @@ export default function DNDTagTile(props) { ); } - return
      - - {(provided, snapshot) => ( -
      - -
      - )} -
      + return <> + {contextMenu} -
      ; + ; } diff --git a/src/components/views/groups/GroupPublicityToggle.js b/src/components/views/groups/GroupPublicityToggle.js index c06d827550..6bef141cb8 100644 --- a/src/components/views/groups/GroupPublicityToggle.js +++ b/src/components/views/groups/GroupPublicityToggle.js @@ -66,9 +66,7 @@ export default class GroupPublicityToggle extends React.Component { render() { const GroupTile = sdk.getComponent('groups.GroupTile'); return
      - + diff --git a/src/components/views/groups/GroupTile.js b/src/components/views/groups/GroupTile.js index 42a977fb79..ce42662462 100644 --- a/src/components/views/groups/GroupTile.js +++ b/src/components/views/groups/GroupTile.js @@ -16,7 +16,6 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; -import { Draggable, Droppable } from 'react-beautiful-dnd'; import * as sdk from '../../../index'; import dis from '../../../dispatcher/dispatcher'; import FlairStore from '../../../stores/FlairStore'; @@ -24,8 +23,6 @@ import MatrixClientContext from "../../../contexts/MatrixClientContext"; import {replaceableComponent} from "../../../utils/replaceableComponent"; import {mediaFromMxc} from "../../../customisations/Media"; -function nop() {} - @replaceableComponent("views.groups.GroupTile") class GroupTile extends React.Component { static propTypes = { @@ -34,7 +31,6 @@ class GroupTile extends React.Component { showDescription: PropTypes.bool, // Height of the group avatar in pixels avatarHeight: PropTypes.number, - draggable: PropTypes.bool, }; static contextType = MatrixClientContext; @@ -42,7 +38,6 @@ class GroupTile extends React.Component { static defaultProps = { showDescription: true, avatarHeight: 50, - draggable: true, }; state = { @@ -57,7 +52,7 @@ class GroupTile extends React.Component { }); } - onMouseDown = e => { + onClick = e => { e.preventDefault(); dis.dispatch({ action: 'view_group', @@ -78,7 +73,7 @@ class GroupTile extends React.Component { ? mediaFromMxc(profile.avatarUrl).getSquareThumbnailHttp(avatarHeight) : null; - let avatarElement = ( + const avatarElement = (
      ); - if (this.props.draggable) { - const avatarClone = avatarElement; - avatarElement = ( - - { (droppableProvided, droppableSnapshot) => ( -
      - - { (provided, snapshot) => ( -
      -
      - {avatarClone} -
      - { /* Instead of a blank placeholder, use a copy of the avatar itself. */ } - { provided.placeholder ? avatarClone :
      } -
      - ) } - -
      - ) } - - ); - } - // XXX: Use onMouseDown as a workaround for https://github.com/atlassian/react-beautiful-dnd/issues/273 - // instead of onClick. Otherwise we experience https://github.com/vector-im/element-web/issues/6156 - return + return { avatarElement }
      { name }
      diff --git a/test/components/views/rooms/RoomList-test.js b/test/components/views/rooms/RoomList-test.js index bfb8e1afd4..6aad6a90fd 100644 --- a/test/components/views/rooms/RoomList-test.js +++ b/test/components/views/rooms/RoomList-test.js @@ -6,7 +6,6 @@ import * as TestUtils from '../../../test-utils'; import {MatrixClientPeg} from '../../../../src/MatrixClientPeg'; import sdk from '../../../skinned-sdk'; -import { DragDropContext } from 'react-beautiful-dnd'; import dis from '../../../../src/dispatcher/dispatcher'; import DMRoomMap from '../../../../src/utils/DMRoomMap'; @@ -68,9 +67,7 @@ describe('RoomList', () => { const RoomList = sdk.getComponent('views.rooms.RoomList'); const WrappedRoomList = TestUtils.wrapInMatrixClientContext(RoomList); root = ReactDOM.render( - - {}} /> - , + {}} />, parentDiv, ); ReactTestUtils.findRenderedComponentWithType(root, RoomList); diff --git a/yarn.lock b/yarn.lock index 0ff235a660..2c84237730 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1017,7 +1017,7 @@ pirates "^4.0.0" source-map-support "^0.5.16" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.12.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7": version "7.12.5" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.5.tgz#410e7e487441e1b360c29be715d870d9b985882e" integrity sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg== @@ -2114,14 +2114,6 @@ babel-preset-jest@^26.6.2: babel-plugin-jest-hoist "^26.6.2" babel-preset-current-node-syntax "^1.0.0" -babel-runtime@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe" - integrity sha1-llxwWGaOgrVde/4E/yM3vItWR/4= - dependencies: - core-js "^2.4.0" - regenerator-runtime "^0.11.0" - bail@^1.0.0: version "1.0.5" resolved "https://registry.yarnpkg.com/bail/-/bail-1.0.5.tgz#b6fa133404a392cbc1f8c4bf63f5953351e7a776" @@ -2645,11 +2637,6 @@ core-js@^1.0.0: resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636" integrity sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY= -core-js@^2.4.0: - version "2.6.12" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec" - integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ== - core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" @@ -4215,13 +4202,6 @@ highlight.js@^10.5.0: resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.5.0.tgz#3f09fede6a865757378f2d9ebdcbc15ba268f98f" integrity sha512-xTmvd9HiIHR6L53TMC7TKolEj65zG1XU+Onr8oi86mYa+nLcIbxTTWkpW7CsEwv/vK7u1zb8alZIMLDqqN6KTw== -hoist-non-react-statics@^3.3.0: - version "3.3.2" - resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" - integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== - dependencies: - react-is "^16.7.0" - hosted-git-info@^2.1.4: version "2.8.9" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9" @@ -4430,13 +4410,6 @@ internal-slot@^1.0.2: has "^1.0.3" side-channel "^1.0.2" -invariant@^2.2.2, invariant@^2.2.4: - version "2.2.4" - resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" - integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== - dependencies: - loose-envify "^1.0.0" - ip-regex@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9" @@ -5556,11 +5529,6 @@ locate-path@^5.0.0: dependencies: p-locate "^4.1.0" -lodash-es@^4.2.1: - version "4.17.20" - resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.20.tgz#29f6332eefc60e849f869c264bc71126ad61e8f7" - integrity sha512-JD1COMZsq8maT6mnuz1UMV0jvYD0E0aUsSOdrr1/nAG3dhqQXwRRgeW0cSqH1U43INKcqxaiVIQNOUDld7gRDA== - lodash.escape@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/lodash.escape/-/lodash.escape-4.0.1.tgz#c9044690c21e04294beaa517712fded1fa88de98" @@ -5581,7 +5549,7 @@ lodash.sortby@^4.7.0: resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg= -lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.2.1: +lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -5749,11 +5717,6 @@ mdurl@~1.0.1: resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" integrity sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4= -memoize-one@^3.0.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-3.1.1.tgz#ef609811e3bc28970eac2884eece64d167830d17" - integrity sha512-YqVh744GsMlZu6xkhGslPSqSurOv6P+kLN2J3ysBZfagLcL5FdRK/0UpgLoL8hwjjEvvAVkjJZyFP+1T6p1vgA== - meow@^9.0.0: version "9.0.0" resolved "https://registry.yarnpkg.com/meow/-/meow-9.0.0.tgz#cd9510bc5cac9dee7d03c73ee1f9ad959f4ea364" @@ -6374,11 +6337,6 @@ path-type@^4.0.0: resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== -performance-now@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5" - integrity sha1-M+8wxcd9TqIcWlOGnZG1bY8lVeU= - performance-now@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" @@ -6597,7 +6555,7 @@ prop-types-exact@^1.2.0: object.assign "^4.1.0" reflect.ownkeys "^0.2.0" -prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2: +prop-types@^15.6.2, prop-types@^15.7.2: version "15.7.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== @@ -6674,12 +6632,7 @@ quick-lru@^4.0.1: resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f" integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g== -raf-schd@^2.1.0: - version "2.1.2" - resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-2.1.2.tgz#ec622b5167f2912089f054dc03ebd5bcf33c8f62" - integrity sha512-Orl0IEvMtUCgPddgSxtxreK77UiQz4nPYJy9RggVzu4mKsZkQWiAaG1y9HlYWdvm9xtN348xRaT37qkvL/+A+g== - -raf@^3.1.0, raf@^3.4.1: +raf@^3.4.1: version "3.4.1" resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39" integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA== @@ -6706,22 +6659,6 @@ re-resizable@^6.9.0: dependencies: fast-memoize "^2.5.1" -react-beautiful-dnd@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-4.0.1.tgz#3b0a49bf6be75af351176c904f012611dd292b81" - integrity sha512-d73RMu4QOFCyjUELLWFyY/EuclnfqulI9pECx+2gIuJvV0ycf1uR88o+1x0RSB9ILD70inHMzCBKNkWVbbt+vA== - dependencies: - babel-runtime "^6.26.0" - invariant "^2.2.2" - memoize-one "^3.0.1" - prop-types "^15.6.0" - raf-schd "^2.1.0" - react-motion "^0.5.2" - react-redux "^5.0.6" - redux "^3.7.2" - redux-thunk "^2.2.0" - reselect "^3.0.1" - 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" @@ -6751,7 +6688,7 @@ react-focus-lock@^2.5.0: use-callback-ref "^1.2.1" use-sidecar "^1.0.1" -react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.6: +react-is@^16.13.1, react-is@^16.8.1, react-is@^16.8.6: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== @@ -6761,33 +6698,6 @@ react-is@^17.0.1: resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.1.tgz#5b3531bd76a645a4c9fb6e693ed36419e3301339" integrity sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA== -react-lifecycles-compat@^3.0.0: - version "3.0.4" - resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" - integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== - -react-motion@^0.5.2: - version "0.5.2" - resolved "https://registry.yarnpkg.com/react-motion/-/react-motion-0.5.2.tgz#0dd3a69e411316567927917c6626551ba0607316" - integrity sha512-9q3YAvHoUiWlP3cK0v+w1N5Z23HXMj4IF4YuvjvWegWqNPfLXsOBE/V7UvQGpXxHFKRQQcNcVQE31g9SB/6qgQ== - dependencies: - performance-now "^0.2.0" - prop-types "^15.5.8" - raf "^3.1.0" - -react-redux@^5.0.6: - version "5.1.2" - resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-5.1.2.tgz#b19cf9e21d694422727bf798e934a916c4080f57" - integrity sha512-Ns1G0XXc8hDyH/OcBHOxNgQx9ayH3SPxBnFCOidGKSle8pKihysQw2rG/PmciUQRoclhVBO8HMhiRmGXnDja9Q== - dependencies: - "@babel/runtime" "^7.1.2" - hoist-non-react-statics "^3.3.0" - invariant "^2.2.4" - loose-envify "^1.1.0" - prop-types "^15.6.1" - react-is "^16.6.0" - react-lifecycles-compat "^3.0.0" - react-test-renderer@^16.0.0-0, react-test-renderer@^16.14.0: version "16.14.0" resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.14.0.tgz#e98360087348e260c56d4fe2315e970480c228ae" @@ -6908,21 +6818,6 @@ redent@^3.0.0: indent-string "^4.0.0" strip-indent "^3.0.0" -redux-thunk@^2.2.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622" - integrity sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw== - -redux@^3.7.2: - version "3.7.2" - resolved "https://registry.yarnpkg.com/redux/-/redux-3.7.2.tgz#06b73123215901d25d065be342eb026bc1c8537b" - integrity sha512-pNqnf9q1hI5HHZRBkj3bAngGZW/JMCmexDlOxw4XagXY2o1327nHH54LoTjiPJ0gizoqPDRqWyX/00g0hD6w+A== - dependencies: - lodash "^4.2.1" - lodash-es "^4.2.1" - loose-envify "^1.1.0" - symbol-observable "^1.0.3" - reflect.ownkeys@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz#749aceec7f3fdf8b63f927a04809e90c5c0b3460" @@ -6940,11 +6835,6 @@ regenerate@^1.4.0: resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a" integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== -regenerator-runtime@^0.11.0: - version "0.11.1" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" - integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg== - regenerator-runtime@^0.13.4: version "0.13.7" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55" @@ -7102,11 +6992,6 @@ require-main-filename@^2.0.0: resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== -reselect@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/reselect/-/reselect-3.0.1.tgz#efdaa98ea7451324d092b2b2163a6a1d7a9a2147" - integrity sha1-79qpjqdFEyTQkrKyFjpqHXqaIUc= - resize-observer-polyfill@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464" @@ -7813,11 +7698,6 @@ svg-tags@^1.0.0: resolved "https://registry.yarnpkg.com/svg-tags/-/svg-tags-1.0.0.tgz#58f71cee3bd519b59d4b2a843b6c7de64ac04764" integrity sha1-WPcc7jvVGbWdSyqEO2x95krAR2Q= -symbol-observable@^1.0.3: - version "1.2.0" - resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804" - integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ== - symbol-tree@^3.2.4: version "3.2.4" resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" From f0f82107909a120c6d31b3a6f0bdef46a2b04ea2 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 1 Jun 2021 19:04:52 +0100 Subject: [PATCH 322/999] Log when we ignore a second call in a room What's more useful than a comment? A log line. --- src/CallHandler.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx index 90a631ab7f..a05d3a25c8 100644 --- a/src/CallHandler.tsx +++ b/src/CallHandler.tsx @@ -802,7 +802,10 @@ export default class CallHandler extends EventEmitter { const mappedRoomId = CallHandler.sharedInstance().roomIdForCall(call); if (this.getCallForRoom(mappedRoomId)) { - // ignore multiple incoming calls to the same room + console.log( + "Got incoming call for room " + mappedRoomId + + " but there's already a call for this room: ignoring", + ); return; } From b032422c6a536b856d7d105dd593a804b245c933 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Tue, 1 Jun 2021 17:37:31 -0400 Subject: [PATCH 323/999] Fix whitespace lints Signed-off-by: Robin Townsend --- src/components/views/dialogs/ForwardDialog.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/views/dialogs/ForwardDialog.tsx b/src/components/views/dialogs/ForwardDialog.tsx index 59475b1b51..58759d7d42 100644 --- a/src/components/views/dialogs/ForwardDialog.tsx +++ b/src/components/views/dialogs/ForwardDialog.tsx @@ -184,13 +184,13 @@ const ForwardDialog: React.FC = ({ matrixClient: cli, event, permalinkCr const previewLayout = SettingsStore.getValue("layout"); return -

      { _t("Message preview") }

      +

      {_t("Message preview")}

      = ({ matrixClient: cli, event, permalinkCr
      Date: Tue, 1 Jun 2021 17:56:46 -0400 Subject: [PATCH 324/999] Match forward dialog send failed indicator color with button Signed-off-by: Robin Townsend --- res/css/views/dialogs/_ForwardDialog.scss | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/res/css/views/dialogs/_ForwardDialog.scss b/res/css/views/dialogs/_ForwardDialog.scss index 593fe12531..fc59259ca2 100644 --- a/res/css/views/dialogs/_ForwardDialog.scss +++ b/res/css/views/dialogs/_ForwardDialog.scss @@ -124,7 +124,8 @@ limitations under the License. } .mx_NotificationBadge { - opacity: 0.4; + // Match the failed to send indicator's color with the disabled button + background-color: $button-danger-disabled-fg-color; } &.mx_ForwardList_sending .mx_ForwardList_sendIcon { From c78167977a49421156f62d995cf5aab97801a758 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Tue, 1 Jun 2021 17:57:26 -0400 Subject: [PATCH 325/999] Remove unused class Signed-off-by: Robin Townsend --- src/components/views/context_menus/MessageContextMenu.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/context_menus/MessageContextMenu.js b/src/components/views/context_menus/MessageContextMenu.js index 08fc5844ec..442470bb52 100644 --- a/src/components/views/context_menus/MessageContextMenu.js +++ b/src/components/views/context_menus/MessageContextMenu.js @@ -161,7 +161,7 @@ export default class MessageContextMenu extends React.Component { matrixClient: MatrixClientPeg.get(), event: this.props.mxEvent, permalinkCreator: this.props.permalinkCreator, - }, 'mx_Dialog_forwardMessage'); + }); this.closeMenu(); }; From 4ef69fcbf6cd5bb90261759eff4cd3e720a96f14 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Tue, 1 Jun 2021 18:09:51 -0400 Subject: [PATCH 326/999] Use settings hooks in forward dialog ...to dynamically watch for layout changes. Signed-off-by: Robin Townsend --- src/components/views/dialogs/ForwardDialog.tsx | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/components/views/dialogs/ForwardDialog.tsx b/src/components/views/dialogs/ForwardDialog.tsx index 58759d7d42..2c4920da4b 100644 --- a/src/components/views/dialogs/ForwardDialog.tsx +++ b/src/components/views/dialogs/ForwardDialog.tsx @@ -22,7 +22,7 @@ import {MatrixClient} from "matrix-js-sdk/src/client"; import {_t} from "../../../languageHandler"; import dis from "../../../dispatcher/dispatcher"; -import SettingsStore from "../../../settings/SettingsStore"; +import {useSettingValue, useFeatureEnabled} from "../../../hooks/useSettings"; import {UIFeature} from "../../../settings/UIFeature"; import {Layout} from "../../../settings/Layout"; import {IDialogProps} from "./IDialogProps"; @@ -174,14 +174,16 @@ const ForwardDialog: React.FC = ({ matrixClient: cli, event, permalinkCr const [query, setQuery] = useState(""); const lcQuery = query.toLowerCase(); + const spacesEnabled = useFeatureEnabled("feature_spaces"); + const flairEnabled = useFeatureEnabled(UIFeature.Flair); + const previewLayout = useSettingValue("layout"); + const rooms = useMemo(() => sortRooms( cli.getVisibleRooms().filter( room => room.getMyMembership() === "join" && - !(SettingsStore.getValue("feature_spaces") && room.isSpaceRoom()), + !(spacesEnabled && room.isSpaceRoom()), ), - ), [cli]).filter(room => room.name.toLowerCase().includes(lcQuery)); - - const previewLayout = SettingsStore.getValue("layout"); + ), [cli, spacesEnabled]).filter(room => room.name.toLowerCase().includes(lcQuery)); return = ({ matrixClient: cli, event, permalinkCr
      From 59660df0cbe74dbe39ada90e4296d427e5375bbc Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Tue, 1 Jun 2021 20:17:20 -0400 Subject: [PATCH 327/999] Use a QueryMatcher for forward dialog filtering This also allows us to filter by room aliases. Signed-off-by: Robin Townsend --- src/components/views/dialogs/ForwardDialog.tsx | 15 ++++++++++++--- src/hooks/useSettings.ts | 4 ++-- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/components/views/dialogs/ForwardDialog.tsx b/src/components/views/dialogs/ForwardDialog.tsx index 2c4920da4b..cba292ea25 100644 --- a/src/components/views/dialogs/ForwardDialog.tsx +++ b/src/components/views/dialogs/ForwardDialog.tsx @@ -38,6 +38,7 @@ import {StaticNotificationState} from "../../../stores/notifications/StaticNotif import NotificationBadge from "../rooms/NotificationBadge"; import {RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks"; import {sortRooms} from "../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm"; +import QueryMatcher from "../../../autocomplete/QueryMatcher"; const AVATAR_SIZE = 30; @@ -176,14 +177,22 @@ const ForwardDialog: React.FC = ({ matrixClient: cli, event, permalinkCr const spacesEnabled = useFeatureEnabled("feature_spaces"); const flairEnabled = useFeatureEnabled(UIFeature.Flair); - const previewLayout = useSettingValue("layout"); + const previewLayout = useSettingValue("layout"); - const rooms = useMemo(() => sortRooms( + let rooms = useMemo(() => sortRooms( cli.getVisibleRooms().filter( room => room.getMyMembership() === "join" && !(spacesEnabled && room.isSpaceRoom()), ), - ), [cli, spacesEnabled]).filter(room => room.name.toLowerCase().includes(lcQuery)); + ), [cli, spacesEnabled]); + + if (lcQuery) { + rooms = new QueryMatcher(rooms, { + keys: ["name"], + funcs: [r => [r.getCanonicalAlias(), ...r.getAltAliases()].filter(Boolean)], + shouldMatchWordsOnly: false, + }).match(lcQuery); + } return (settingName: string, roomId: string = null, e }; // Hook to fetch whether a feature is enabled and dynamically update when that changes -export const useFeatureEnabled = (featureName: string, roomId: string = null) => { - const [enabled, setEnabled] = useState(SettingsStore.getValue(featureName, roomId)); +export const useFeatureEnabled = (featureName: string, roomId: string = null): boolean => { + const [enabled, setEnabled] = useState(SettingsStore.getValue(featureName, roomId)); useEffect(() => { const ref = SettingsStore.watchSetting(featureName, roomId, () => { From 992861a1cd821c2d7564fd644b4294220f54ff96 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Tue, 1 Jun 2021 20:36:28 -0400 Subject: [PATCH 328/999] Fix forward dialog tests Signed-off-by: Robin Townsend --- test/test-utils.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/test-utils.js b/test/test-utils.js index c82885b5f0..6053924103 100644 --- a/test/test-utils.js +++ b/test/test-utils.js @@ -265,6 +265,8 @@ export function mkStubRoom(roomId = null, name) { isSpaceRoom: jest.fn(() => false), getUnreadNotificationCount: jest.fn(() => 0), getEventReadUpTo: jest.fn(() => null), + getCanonicalAlias: jest.fn(), + getAltAliases: jest.fn().mockReturnValue([]), timeline: [], }; } From 5b2dacd99ec7dd09a7dc2fbc76c81d1f44f9dfd8 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 1 Jun 2021 21:36:28 -0600 Subject: [PATCH 329/999] Adapt for js-sdk MatrixClient conversion to TS For https://github.com/matrix-org/matrix-js-sdk/pull/1718 --- src/Searching.js | 10 +++++----- src/SecurityManager.ts | 2 +- src/components/views/dialogs/DevtoolsDialog.tsx | 2 +- src/components/views/settings/CrossSigningPanel.js | 4 ++-- src/components/views/settings/SecureBackupPanel.js | 4 ++-- src/indexing/EventIndex.js | 2 +- src/rageshake/submit-rageshake.ts | 6 +++--- src/stores/CommunityPrototypeStore.ts | 2 +- src/stores/SetupEncryptionStore.js | 2 +- test/test-utils.js | 2 +- 10 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/Searching.js b/src/Searching.js index f65b8920b3..2b17aee054 100644 --- a/src/Searching.js +++ b/src/Searching.js @@ -66,7 +66,7 @@ async function serverSideSearchProcess(term, roomId = undefined) { highlights: [], }; - return client._processRoomEventsSearch(searchResult, result.response); + return client.processRoomEventsSearch(searchResult, result.response); } function compareEvents(a, b) { @@ -131,7 +131,7 @@ async function combinedSearch(searchTerm) { }, }; - const result = client._processRoomEventsSearch(emptyResult, response); + const result = client.processRoomEventsSearch(emptyResult, response); // Restore our encryption info so we can properly re-verify the events. restoreEncryptionInfo(result.results); @@ -185,7 +185,7 @@ async function localSearchProcess(searchTerm, roomId = undefined) { }, }; - const processedResult = MatrixClientPeg.get()._processRoomEventsSearch(emptyResult, response); + const processedResult = MatrixClientPeg.get().processRoomEventsSearch(emptyResult, response); // Restore our encryption info so we can properly re-verify the events. restoreEncryptionInfo(processedResult.results); @@ -210,7 +210,7 @@ async function localPagination(searchResult) { }, }; - const result = MatrixClientPeg.get()._processRoomEventsSearch(searchResult, response); + const result = MatrixClientPeg.get().processRoomEventsSearch(searchResult, response); // Restore our encryption info so we can properly re-verify the events. const newSlice = result.results.slice(Math.max(result.results.length - newResultCount, 0)); @@ -520,7 +520,7 @@ async function combinedPagination(searchResult) { const oldResultCount = searchResult.results ? searchResult.results.length : 0; // Let the client process the combined result. - const result = client._processRoomEventsSearch(searchResult, response); + const result = client.processRoomEventsSearch(searchResult, response); // Restore our encryption info so we can properly re-verify the events. const newResultCount = result.results.length - oldResultCount; diff --git a/src/SecurityManager.ts b/src/SecurityManager.ts index 203830d232..09c8d30614 100644 --- a/src/SecurityManager.ts +++ b/src/SecurityManager.ts @@ -271,7 +271,7 @@ async function onSecretRequested( } return key && encodeBase64(key); } else if (name === "m.megolm_backup.v1") { - const key = await client._crypto.getSessionBackupPrivateKey(); + const key = await client.crypto.getSessionBackupPrivateKey(); if (!key) { console.log( `session backup key requested by ${deviceId}, but not found in cache`, diff --git a/src/components/views/dialogs/DevtoolsDialog.tsx b/src/components/views/dialogs/DevtoolsDialog.tsx index 0c37ea9599..fdbf6a36fc 100644 --- a/src/components/views/dialogs/DevtoolsDialog.tsx +++ b/src/components/views/dialogs/DevtoolsDialog.tsx @@ -766,7 +766,7 @@ class VerificationExplorer extends React.PureComponent { render() { const cli = this.context; const room = this.props.room; - const inRoomChannel = cli._crypto._inRoomVerificationRequests; + const inRoomChannel = cli.crypto._inRoomVerificationRequests; const inRoomRequests = (inRoomChannel._requestsByRoomId || new Map()).get(room.roomId) || new Map(); return (
      diff --git a/src/components/views/settings/CrossSigningPanel.js b/src/components/views/settings/CrossSigningPanel.js index c0f23cb906..0cd1a64ada 100644 --- a/src/components/views/settings/CrossSigningPanel.js +++ b/src/components/views/settings/CrossSigningPanel.js @@ -79,8 +79,8 @@ export default class CrossSigningPanel extends React.PureComponent { async _getUpdatedStatus() { const cli = MatrixClientPeg.get(); const pkCache = cli.getCrossSigningCacheCallbacks(); - const crossSigning = cli._crypto._crossSigningInfo; - const secretStorage = cli._crypto._secretStorage; + const crossSigning = cli.crypto._crossSigningInfo; + const secretStorage = cli.crypto._secretStorage; const crossSigningPublicKeysOnDevice = crossSigning.getId(); const crossSigningPrivateKeysInStorage = await crossSigning.isStoredInSecretStorage(secretStorage); const masterPrivateKeyCached = !!(pkCache && await pkCache.getCrossSigningKeyCache("master")); diff --git a/src/components/views/settings/SecureBackupPanel.js b/src/components/views/settings/SecureBackupPanel.js index 310114c8af..4f3eb0bdf6 100644 --- a/src/components/views/settings/SecureBackupPanel.js +++ b/src/components/views/settings/SecureBackupPanel.js @@ -131,10 +131,10 @@ export default class SecureBackupPanel extends React.PureComponent { async _getUpdatedDiagnostics() { const cli = MatrixClientPeg.get(); - const secretStorage = cli._crypto._secretStorage; + const secretStorage = cli.crypto._secretStorage; const backupKeyStored = !!(await cli.isKeyBackupKeyStored()); - const backupKeyFromCache = await cli._crypto.getSessionBackupPrivateKey(); + const backupKeyFromCache = await cli.crypto.getSessionBackupPrivateKey(); const backupKeyCached = !!(backupKeyFromCache); const backupKeyWellFormed = backupKeyFromCache instanceof Uint8Array; const secretStorageKeyInAccount = await secretStorage.hasKey(); diff --git a/src/indexing/EventIndex.js b/src/indexing/EventIndex.js index ed4418140b..33f2d594ae 100644 --- a/src/indexing/EventIndex.js +++ b/src/indexing/EventIndex.js @@ -453,7 +453,7 @@ export default class EventIndex extends EventEmitter { let res; try { - res = await client._createMessagesRequest( + res = await client.createMessagesRequest( checkpoint.roomId, checkpoint.token, this._eventsPerCrawl, checkpoint.direction); } catch (e) { diff --git a/src/rageshake/submit-rageshake.ts b/src/rageshake/submit-rageshake.ts index f46dd88fba..b2ad9fe6f6 100644 --- a/src/rageshake/submit-rageshake.ts +++ b/src/rageshake/submit-rageshake.ts @@ -92,8 +92,8 @@ async function collectBugReport(opts: IOpts = {}, gzipLogs = true) { body.append('cross_signing_key', client.getCrossSigningId()); // add cross-signing status information - const crossSigning = client._crypto._crossSigningInfo; - const secretStorage = client._crypto._secretStorage; + const crossSigning = client.crypto._crossSigningInfo; + const secretStorage = client.crypto._secretStorage; body.append("cross_signing_ready", String(await client.isCrossSigningReady())); body.append("cross_signing_supported_by_hs", @@ -114,7 +114,7 @@ async function collectBugReport(opts: IOpts = {}, gzipLogs = true) { body.append("secret_storage_key_in_account", String(!!(await secretStorage.hasKey()))); body.append("session_backup_key_in_secret_storage", String(!!(await client.isKeyBackupKeyStored()))); - const sessionBackupKeyFromCache = await client._crypto.getSessionBackupPrivateKey(); + const sessionBackupKeyFromCache = await client.crypto.getSessionBackupPrivateKey(); body.append("session_backup_key_cached", String(!!sessionBackupKeyFromCache)); body.append("session_backup_key_well_formed", String(sessionBackupKeyFromCache instanceof Uint8Array)); } diff --git a/src/stores/CommunityPrototypeStore.ts b/src/stores/CommunityPrototypeStore.ts index 92e094c83b..023845c9ee 100644 --- a/src/stores/CommunityPrototypeStore.ts +++ b/src/stores/CommunityPrototypeStore.ts @@ -126,7 +126,7 @@ export class CommunityPrototypeStore extends AsyncStoreWithClient { if (membership === EffectiveMembership.Invite) { try { const path = utils.encodeUri("/rooms/$roomId/group_info", {$roomId: room.roomId}); - const profile = await this.matrixClient._http.authedRequest( + const profile = await this.matrixClient.http.authedRequest( undefined, "GET", path, undefined, undefined, {prefix: "/_matrix/client/unstable/im.vector.custom"}); diff --git a/src/stores/SetupEncryptionStore.js b/src/stores/SetupEncryptionStore.js index 5f0054ff24..b768ae69df 100644 --- a/src/stores/SetupEncryptionStore.js +++ b/src/stores/SetupEncryptionStore.js @@ -196,7 +196,7 @@ export class SetupEncryptionStore extends EventEmitter { this.phase = PHASE_FINISHED; this.emit("update"); // async - ask other clients for keys, if necessary - MatrixClientPeg.get()._crypto.cancelAndResendAllOutgoingKeyRequests(); + MatrixClientPeg.get().crypto.cancelAndResendAllOutgoingKeyRequests(); } async _setActiveVerificationRequest(request) { diff --git a/test/test-utils.js b/test/test-utils.js index 4faf948178..b9014f4876 100644 --- a/test/test-utils.js +++ b/test/test-utils.js @@ -90,7 +90,7 @@ export function createTestClient() { }), // Used by various internal bits we aren't concerned with (yet) - _sessionStore: { + sessionStore: { store: { getItem: jest.fn(), }, From c9883f346c32d0c5d670b0efcc5702ec18f5ecad Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 1 Jun 2021 22:21:04 -0600 Subject: [PATCH 330/999] Build pass 1 --- src/ContentMessages.tsx | 26 ++++++++++++------- src/Presence.ts | 2 +- src/Terms.ts | 2 +- src/components/structures/LoggedInView.tsx | 2 +- src/components/structures/MatrixChat.tsx | 2 +- .../structures/auth/Registration.tsx | 2 +- .../tabs/user/HelpUserSettingsTab.tsx | 2 +- 7 files changed, 22 insertions(+), 16 deletions(-) diff --git a/src/ContentMessages.tsx b/src/ContentMessages.tsx index 95b45cce4a..b21829ac63 100644 --- a/src/ContentMessages.tsx +++ b/src/ContentMessages.tsx @@ -40,6 +40,7 @@ import { UploadStartedPayload, } from "./dispatcher/payloads/UploadPayload"; import {IUpload} from "./models/IUpload"; +import { IImageInfo } from "matrix-js-sdk/src/@types/partials"; const MAX_WIDTH = 800; const MAX_HEIGHT = 600; @@ -208,12 +209,12 @@ function infoForImageFile(matrixClient, roomId, imageFile) { } let imageInfo; - return loadImageElement(imageFile).then(function(r) { + return loadImageElement(imageFile).then((r) => { return createThumbnail(r.img, r.width, r.height, thumbnailType); - }).then(function(result) { + }).then((result) => { imageInfo = result.info; return uploadFile(matrixClient, roomId, result.thumbnail); - }).then(function(result) { + }).then((result) => { imageInfo.thumbnail_url = result.url; imageInfo.thumbnail_file = result.file; return imageInfo; @@ -264,12 +265,12 @@ function infoForVideoFile(matrixClient, roomId, videoFile) { const thumbnailType = "image/jpeg"; let videoInfo; - return loadVideoElement(videoFile).then(function(video) { + return loadVideoElement(videoFile).then((video) => { return createThumbnail(video, video.videoWidth, video.videoHeight, thumbnailType); - }).then(function(result) { + }).then((result) => { videoInfo = result.info; return uploadFile(matrixClient, roomId, result.thumbnail); - }).then(function(result) { + }).then((result) => { videoInfo.thumbnail_url = result.url; videoInfo.thumbnail_file = result.file; return videoInfo; @@ -308,7 +309,12 @@ function readFileAsArrayBuffer(file: File | Blob): Promise { * If the file is unencrypted then the object will have a "url" key. * If the file is encrypted then the object will have a "file" key. */ -function uploadFile(matrixClient: MatrixClient, roomId: string, file: File | Blob, progressHandler?: any) { +function uploadFile( + matrixClient: MatrixClient, + roomId: string, + file: File | Blob, + progressHandler?: any, // TODO: Types +): Promise<{url?: string, file?: any}> { // TODO: Types let canceled = false; if (matrixClient.isRoomEncrypted(roomId)) { // If the room is encrypted then encrypt the file before uploading it. @@ -355,7 +361,7 @@ function uploadFile(matrixClient: MatrixClient, roomId: string, file: File | Blo // If the attachment isn't encrypted then include the URL directly. return {"url": url}; }); - promise1.abort = () => { + (promise1 as any).abort = () => { canceled = true; MatrixClientPeg.get().cancelUpload(basePromise); }; @@ -367,7 +373,7 @@ 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: IImageInfo, text: string, matrixClient: MatrixClient) { const startTime = CountlyAnalytics.getTimestamp(); const prom = MatrixClientPeg.get().sendStickerMessage(roomId, url, info, text).catch((e) => { console.warn(`Failed to send content with URL ${url} to room ${roomId}`, e); @@ -441,7 +447,7 @@ export default class ContentMessages { 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 - let promBefore = Promise.resolve(); + let promBefore: Promise = Promise.resolve(); for (let i = 0; i < okFiles.length; ++i) { const file = okFiles[i]; if (!uploadAll) { diff --git a/src/Presence.ts b/src/Presence.ts index eb56c5714e..8f2e127cb4 100644 --- a/src/Presence.ts +++ b/src/Presence.ts @@ -98,7 +98,7 @@ class Presence { } try { - await MatrixClientPeg.get().setPresence(this.state); + await MatrixClientPeg.get().setPresence({presence: this.state}); console.info("Presence:", newState); } catch (err) { console.error("Failed to set presence:", err); diff --git a/src/Terms.ts b/src/Terms.ts index 1b1c152fdd..a6ea40a6e8 100644 --- a/src/Terms.ts +++ b/src/Terms.ts @@ -103,7 +103,7 @@ export async function startTermsFlow( // fetch the set of agreed policy URLs from account data const currentAcceptedTerms = await MatrixClientPeg.get().getAccountData('m.accepted_terms'); - let agreedUrlSet; + let agreedUrlSet: Set; if (!currentAcceptedTerms || !currentAcceptedTerms.getContent() || !currentAcceptedTerms.getContent().accepted) { agreedUrlSet = new Set(); } else { diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx index ad5c759f0d..3091278e3a 100644 --- a/src/components/structures/LoggedInView.tsx +++ b/src/components/structures/LoggedInView.tsx @@ -358,7 +358,7 @@ class LoggedInView extends React.Component { const pinnedEventIds = pinStateEvent.getContent().pinned.slice(0, MAX_PINNED_NOTICES_PER_ROOM); for (const eventId of pinnedEventIds) { - const timeline = await this._matrixClient.getEventTimeline(room.getUnfilteredTimelineSet(), eventId, 0); + const timeline = await this._matrixClient.getEventTimeline(room.getUnfilteredTimelineSet(), eventId); const event = timeline.getEvents().find(ev => ev.getId() === eventId); if (event) events.push(event); } diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index d5e8e6a1f8..16da9321e2 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -378,7 +378,7 @@ export default class MatrixChat extends React.PureComponent { this.onLoggedIn(); } - const promisesList = [this.firstSyncPromise.promise]; + const promisesList: Promise[] = [this.firstSyncPromise.promise]; if (cryptoEnabled) { // wait for the client to finish downloading cross-signing keys for us so we // know whether or not we have keys set up on this account diff --git a/src/components/structures/auth/Registration.tsx b/src/components/structures/auth/Registration.tsx index e4515dd627..6feb1e34f7 100644 --- a/src/components/structures/auth/Registration.tsx +++ b/src/components/structures/auth/Registration.tsx @@ -61,7 +61,7 @@ interface IProps { is_url?: string; session_id: string; /* eslint-enable camelcase */ - }): void; + }): string; // registration shouldn't know or care how login is done. onLoginClick(): void; onServerConfigChange(config: ValidatedServerConfig): void; diff --git a/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx b/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx index 3fa0be478c..74c28e35fc 100644 --- a/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx @@ -227,7 +227,7 @@ export default class HelpUserSettingsTab extends React.Component const appVersion = this.state.appVersion || 'unknown'; - let olmVersion = MatrixClientPeg.get().olmVersion; + let olmVersion: string = MatrixClientPeg.get().olmVersion?.toString(); olmVersion = olmVersion ? `${olmVersion[0]}.${olmVersion[1]}.${olmVersion[2]}` : ''; let updateButton = null; From 3dc6cfbf341dd40e2500399f5957de53630f0333 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 1 Jun 2021 22:31:08 -0600 Subject: [PATCH 331/999] Undo olmVersion handling --- src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx b/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx index 74c28e35fc..3fa0be478c 100644 --- a/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx @@ -227,7 +227,7 @@ export default class HelpUserSettingsTab extends React.Component const appVersion = this.state.appVersion || 'unknown'; - let olmVersion: string = MatrixClientPeg.get().olmVersion?.toString(); + let olmVersion = MatrixClientPeg.get().olmVersion; olmVersion = olmVersion ? `${olmVersion[0]}.${olmVersion[1]}.${olmVersion[2]}` : ''; let updateButton = null; From 234c65b3316d7c5e575a2fb375ec74fba85808e7 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Wed, 2 Jun 2021 10:25:29 +0100 Subject: [PATCH 332/999] Tweak event border radius to match action bar This adjusts the `border-radius` used when hovering on an event to match the recently changed value for the action bar (changed to `8px` in https://github.com/matrix-org/matrix-react-sdk/pull/6066). --- res/css/views/rooms/_EventTile.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 5d1dd04383..b974bfd887 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -104,7 +104,7 @@ $left-gutter: 64px; .mx_EventTile_line, .mx_EventTile_reply { position: relative; padding-left: $left-gutter; - border-radius: 4px; + border-radius: 8px; } .mx_RoomView_timeline_rr_enabled, From d7a5547d8055ecf5584c104df8157428944a6246 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Wed, 2 Jun 2021 10:42:17 +0100 Subject: [PATCH 333/999] use Intl.Collator over String.prototype.localeCompare for better performance --- src/components/views/dialogs/InviteDialog.tsx | 3 +- .../views/directory/NetworkDropdown.tsx | 3 +- src/components/views/rooms/MemberList.js | 29 +++++++++---------- .../views/rooms/WhoIsTypingTile.tsx | 3 +- .../tabs/room/RolesRoomSettingsTab.tsx | 3 +- .../tabs/user/AppearanceUserSettingsTab.tsx | 9 +++--- src/integrations/IntegrationManagers.ts | 3 +- .../tag-sorting/AlphabeticAlgorithm.ts | 3 +- src/stores/widgets/WidgetLayoutStore.ts | 3 +- src/utils/strings.ts | 13 +++++++++ .../components/views/rooms/MemberList-test.js | 4 ++- 11 files changed, 49 insertions(+), 27 deletions(-) diff --git a/src/components/views/dialogs/InviteDialog.tsx b/src/components/views/dialogs/InviteDialog.tsx index b00b45bf60..b006205f11 100644 --- a/src/components/views/dialogs/InviteDialog.tsx +++ b/src/components/views/dialogs/InviteDialog.tsx @@ -49,6 +49,7 @@ import {mediaFromMxc} from "../../../customisations/Media"; import {getAddressType} from "../../../UserAddress"; import BaseAvatar from '../avatars/BaseAvatar'; import AccessibleButton from '../elements/AccessibleButton'; +import { compare } from '../../../utils/strings'; // we have a number of types defined from the Matrix spec which can't reasonably be altered here. /* eslint-disable camelcase */ @@ -578,7 +579,7 @@ export default class InviteDialog extends React.PureComponent { if (a.score === b.score) { if (a.numRooms === b.numRooms) { - return a.member.userId.localeCompare(b.member.userId); + return compare(a.member.userId, b.member.userId); } return b.numRooms - a.numRooms; diff --git a/src/components/views/directory/NetworkDropdown.tsx b/src/components/views/directory/NetworkDropdown.tsx index 08787812f6..c805ee42e7 100644 --- a/src/components/views/directory/NetworkDropdown.tsx +++ b/src/components/views/directory/NetworkDropdown.tsx @@ -39,6 +39,7 @@ import { SettingLevel } from "../../../settings/SettingLevel"; import TextInputDialog from "../dialogs/TextInputDialog"; import QuestionDialog from "../dialogs/QuestionDialog"; import UIStore from "../../../stores/UIStore"; +import { compare } from "../../../utils/strings"; export const ALL_ROOMS = Symbol("ALL_ROOMS"); @@ -187,7 +188,7 @@ const NetworkDropdown = ({ onOptionChange, protocols = {}, selectedServerName, s protocolsList.forEach(({instances=[]}) => { [...instances].sort((b, a) => { - return a.desc.localeCompare(b.desc); + return compare(a.desc, b.desc); }).forEach(({desc, instance_id: instanceId}) => { entries.push( { @@ -422,7 +421,7 @@ export default class MemberList extends React.Component { } else { // Is a 3pid invite return this._onPending3pidInviteClick(m)} />; + onClick={() => this._onPending3pidInviteClick(m)} />; } }); } @@ -484,10 +483,10 @@ export default class MemberList extends React.Component { if (this._getChildCountInvited() > 0) { invitedHeader =

      { _t("Invited") }

      ; invitedSection = ; + createOverflowElement={this._createOverflowTileInvited} + getChildren={this._getChildrenInvited} + getChildCount={this._getChildCountInvited} + />; } const footer = ( @@ -520,9 +519,9 @@ export default class MemberList extends React.Component { >
      + createOverflowElement={this._createOverflowTileJoined} + getChildren={this._getChildrenJoined} + getChildCount={this._getChildCountJoined} /> { invitedHeader } { invitedSection }
      diff --git a/src/components/views/rooms/WhoIsTypingTile.tsx b/src/components/views/rooms/WhoIsTypingTile.tsx index 21afbc30f4..c5d5929546 100644 --- a/src/components/views/rooms/WhoIsTypingTile.tsx +++ b/src/components/views/rooms/WhoIsTypingTile.tsx @@ -25,6 +25,7 @@ import Timer from '../../../utils/Timer'; import { MatrixClientPeg } from '../../../MatrixClientPeg'; import MemberAvatar from '../avatars/MemberAvatar'; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { compare } from "../../../utils/strings"; interface IProps { // the room this statusbar is representing. @@ -207,7 +208,7 @@ export default class WhoIsTypingTile extends React.Component { usersTyping = usersTyping.concat(stoppedUsersOnTimer); // sort them so the typing members don't change order when // moved to delayedStopTypingTimers - usersTyping.sort((a, b) => a.name.localeCompare(b.name)); + usersTyping.sort((a, b) => compare(a.name, b.name)); const typingString = WhoIsTyping.whoIsTypingString( usersTyping, diff --git a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx index 4fa521f598..19ebe2a77e 100644 --- a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx @@ -25,6 +25,7 @@ import {EventType} from "matrix-js-sdk/src/@types/event"; 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"; const plEventsToLabels = { // These will be translated for us later. @@ -312,7 +313,7 @@ export default class RolesRoomSettingsTab extends React.Component { // comparator for sorting PL users lexicographically on PL descending, MXID ascending. (case-insensitive) const comparator = (a, b) => { const plDiff = userLevels[b.key] - userLevels[a.key]; - return plDiff !== 0 ? plDiff : a.key.toLocaleLowerCase().localeCompare(b.key.toLocaleLowerCase()); + return plDiff !== 0 ? plDiff : compare(a.key.toLocaleLowerCase(), b.key.toLocaleLowerCase()); }; privilegedUsers.sort(comparator); diff --git a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx index bc40c36bda..9e27ed968e 100644 --- a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx @@ -35,9 +35,10 @@ import Field from '../../../elements/Field'; import EventTilePreview from '../../../elements/EventTilePreview'; import StyledRadioGroup from "../../../elements/StyledRadioGroup"; import { SettingLevel } from "../../../../../settings/SettingLevel"; -import {UIFeature} from "../../../../../settings/UIFeature"; -import {Layout} from "../../../../../settings/Layout"; -import {replaceableComponent} from "../../../../../utils/replaceableComponent"; +import { UIFeature } from "../../../../../settings/UIFeature"; +import { Layout } from "../../../../../settings/Layout"; +import { replaceableComponent } from "../../../../../utils/replaceableComponent"; +import { compare } from "../../../../../utils/strings"; interface IProps { } @@ -295,7 +296,7 @@ export default class AppearanceUserSettingsTab extends React.Component ({id: p[0], name: p[1]})); // convert pairs to objects for code readability const builtInThemes = themes.filter(p => !p.id.startsWith("custom-")); const customThemes = themes.filter(p => !builtInThemes.includes(p)) - .sort((a, b) => a.name.localeCompare(b.name)); + .sort((a, b) => compare(a.name, b.name)); const orderedThemes = [...builtInThemes, ...customThemes]; return (
      diff --git a/src/integrations/IntegrationManagers.ts b/src/integrations/IntegrationManagers.ts index a29c74c5eb..780f6d4660 100644 --- a/src/integrations/IntegrationManagers.ts +++ b/src/integrations/IntegrationManagers.ts @@ -28,6 +28,7 @@ import WidgetUtils from "../utils/WidgetUtils"; import {MatrixClientPeg} from "../MatrixClientPeg"; import SettingsStore from "../settings/SettingsStore"; import url from 'url'; +import { compare } from "../utils/strings"; const KIND_PREFERENCE = [ // Ordered: first is most preferred, last is least preferred. @@ -152,7 +153,7 @@ export class IntegrationManagers { if (kind === Kind.Account) { // Order by state_keys (IDs) - managers.sort((a, b) => a.id.localeCompare(b.id)); + managers.sort((a, b) => compare(a.id, b.id)); } ordered.push(...managers); diff --git a/src/stores/room-list/algorithms/tag-sorting/AlphabeticAlgorithm.ts b/src/stores/room-list/algorithms/tag-sorting/AlphabeticAlgorithm.ts index d909fb6288..b016a4256c 100644 --- a/src/stores/room-list/algorithms/tag-sorting/AlphabeticAlgorithm.ts +++ b/src/stores/room-list/algorithms/tag-sorting/AlphabeticAlgorithm.ts @@ -17,6 +17,7 @@ limitations under the License. import { Room } from "matrix-js-sdk/src/models/room"; import { TagID } from "../../models"; import { IAlgorithm } from "./IAlgorithm"; +import { compare } from "../../../../utils/strings"; /** * Sorts rooms according to the browser's determination of alphabetic. @@ -24,7 +25,7 @@ import { IAlgorithm } from "./IAlgorithm"; export class AlphabeticAlgorithm implements IAlgorithm { public async sortRooms(rooms: Room[], tagId: TagID): Promise { return rooms.sort((a, b) => { - return a.name.localeCompare(b.name); + return compare(a.name, b.name); }); } } diff --git a/src/stores/widgets/WidgetLayoutStore.ts b/src/stores/widgets/WidgetLayoutStore.ts index e6ef534202..f5734d74c5 100644 --- a/src/stores/widgets/WidgetLayoutStore.ts +++ b/src/stores/widgets/WidgetLayoutStore.ts @@ -25,6 +25,7 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { SettingLevel } from "../../settings/SettingLevel"; import { arrayFastClone } from "../../utils/arrays"; import { UPDATE_EVENT } from "../AsyncStore"; +import { compare } from "../../utils/strings"; export const WIDGET_LAYOUT_EVENT_TYPE = "io.element.widgets.layout"; @@ -240,7 +241,7 @@ export class WidgetLayoutStore extends ReadyWatchingStore { if (orderA === orderB) { // We just need a tiebreak - return a.id.localeCompare(b.id); + return compare(a.id, b.id); } return orderA - orderB; diff --git a/src/utils/strings.ts b/src/utils/strings.ts index 5856682445..34375aabc0 100644 --- a/src/utils/strings.ts +++ b/src/utils/strings.ts @@ -14,6 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { memoize } from "lodash"; + /** * Copy plaintext to user's clipboard * It will overwrite user's selection range @@ -73,3 +75,14 @@ export function copyNode(ref: Element): boolean { selectText(ref); return document.execCommand('copy'); } + + +const collator = new Intl.Collator(); +/** + * Performant language-sensitive string comparison + * @param a the first string to compare + * @param b the second string to compare + */ +export function compare(a: string, b: string): number { + return collator.compare(a, b); +} diff --git a/test/components/views/rooms/MemberList-test.js b/test/components/views/rooms/MemberList-test.js index 50b40dea20..28fead770c 100644 --- a/test/components/views/rooms/MemberList-test.js +++ b/test/components/views/rooms/MemberList-test.js @@ -9,6 +9,8 @@ import sdk from '../../../skinned-sdk'; import {Room, RoomMember, User} from 'matrix-js-sdk'; +import { compare } from "../../../../src/utils/strings"; + function generateRoomId() { return '!' + Math.random().toString().slice(2, 10) + ':domain'; } @@ -173,7 +175,7 @@ describe('MemberList', () => { if (!groupChange) { const nameA = memberA.name[0] === '@' ? memberA.name.substr(1) : memberA.name; const nameB = memberB.name[0] === '@' ? memberB.name.substr(1) : memberB.name; - const nameCompare = nameB.localeCompare(nameA); + const nameCompare = compare(nameB, nameA); console.log("Comparing name"); expect(nameCompare).toBeGreaterThanOrEqual(0); } else { From 82fe9a5c7b56f83211705862ce7a47dd0af97085 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Wed, 2 Jun 2021 10:48:18 +0100 Subject: [PATCH 334/999] remove unused import --- src/components/views/rooms/MemberList.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/views/rooms/MemberList.js b/src/components/views/rooms/MemberList.js index 02da758d58..cb50f0fff3 100644 --- a/src/components/views/rooms/MemberList.js +++ b/src/components/views/rooms/MemberList.js @@ -31,7 +31,6 @@ import RoomAvatar from "../avatars/RoomAvatar"; import RoomName from "../elements/RoomName"; import {replaceableComponent} from "../../../utils/replaceableComponent"; import SettingsStore from "../../../settings/SettingsStore"; -import { compare } from '../../../utils/strings'; const INITIAL_LOAD_NUM_MEMBERS = 30; const INITIAL_LOAD_NUM_INVITED = 5; From f3431feaffe913ad792152f631e2cd7bb9f89b2e Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Wed, 2 Jun 2021 11:25:32 +0100 Subject: [PATCH 335/999] Remove unused memoize import --- src/utils/strings.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/utils/strings.ts b/src/utils/strings.ts index 34375aabc0..beb9f31ddd 100644 --- a/src/utils/strings.ts +++ b/src/utils/strings.ts @@ -14,8 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { memoize } from "lodash"; - /** * Copy plaintext to user's clipboard * It will overwrite user's selection range From bc3c759feb2d3f062d0dbc9489e5741fa7d8af13 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 2 Jun 2021 11:33:25 +0100 Subject: [PATCH 336/999] Add temporary mechanism for managing communities without dnd --- .../context_menus/_TagTileContextMenu.scss | 9 ++++ src/components/structures/MyGroups.js | 3 +- .../views/context_menus/TagTileContextMenu.js | 49 ++++++++++++++----- src/components/views/elements/DNDTagTile.js | 2 +- src/components/views/groups/GroupTile.js | 23 +++++++++ src/i18n/strings/en_EN.json | 4 +- 6 files changed, 74 insertions(+), 16 deletions(-) diff --git a/res/css/views/context_menus/_TagTileContextMenu.scss b/res/css/views/context_menus/_TagTileContextMenu.scss index 8929c8906e..d707f4ce7c 100644 --- a/res/css/views/context_menus/_TagTileContextMenu.scss +++ b/res/css/views/context_menus/_TagTileContextMenu.scss @@ -38,6 +38,15 @@ limitations under the License. mask-image: url('$(res)/img/element-icons/view-community.svg'); } +.mx_TagTileContextMenu_moveUp::before { + transform: rotate(180deg); + mask-image: url('$(res)/img/feather-customised/chevron-down.svg'); +} + +.mx_TagTileContextMenu_moveDown::before { + mask-image: url('$(res)/img/feather-customised/chevron-down.svg'); +} + .mx_TagTileContextMenu_hideCommunity::before { mask-image: url('$(res)/img/element-icons/hide.svg'); } diff --git a/src/components/structures/MyGroups.js b/src/components/structures/MyGroups.js index 1fab6c4348..d0a2fbff41 100644 --- a/src/components/structures/MyGroups.js +++ b/src/components/structures/MyGroups.js @@ -82,8 +82,7 @@ export default class MyGroups extends React.Component {

      { _t( - "To set up a filter, drag a community avatar over to the filter panel on " + - "the far left hand side of the screen. You can click on an avatar in the " + + "You can click on an avatar in the " + "filter panel at any time to see only the rooms and people associated " + "with that community.", ) } diff --git a/src/components/views/context_menus/TagTileContextMenu.js b/src/components/views/context_menus/TagTileContextMenu.js index 8dea62690c..4e381643ba 100644 --- a/src/components/views/context_menus/TagTileContextMenu.js +++ b/src/components/views/context_menus/TagTileContextMenu.js @@ -23,45 +23,70 @@ import TagOrderActions from '../../../actions/TagOrderActions'; import {MenuItem} from "../../structures/ContextMenu"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import GroupFilterOrderStore from "../../../stores/GroupFilterOrderStore"; @replaceableComponent("views.context_menus.TagTileContextMenu") export default class TagTileContextMenu extends React.Component { static propTypes = { tag: PropTypes.string.isRequired, + index: PropTypes.number.isRequired, /* callback called when the menu is dismissed */ onFinished: PropTypes.func.isRequired, }; static contextType = MatrixClientContext; - constructor() { - super(); - - this._onViewCommunityClick = this._onViewCommunityClick.bind(this); - this._onRemoveClick = this._onRemoveClick.bind(this); - } - - _onViewCommunityClick() { + _onViewCommunityClick = () => { dis.dispatch({ action: 'view_group', group_id: this.props.tag, }); this.props.onFinished(); - } + }; - _onRemoveClick() { + _onRemoveClick = () => { dis.dispatch(TagOrderActions.removeTag(this.context, this.props.tag)); this.props.onFinished(); - } + }; + + _onMoveUp = () => { + dis.dispatch(TagOrderActions.moveTag(this.context, this.props.tag, this.props.index - 1)); + this.props.onFinished(); + }; + + _onMoveDown = () => { + dis.dispatch(TagOrderActions.moveTag(this.context, this.props.tag, this.props.index + 1)); + this.props.onFinished(); + }; render() { + let moveUp; + let moveDown; + if (this.props.index > 0) { + moveUp = ( + + { _t("Move up") } + + ); + } + if (this.props.index < (GroupFilterOrderStore.getOrderedTags() || []).length - 1) { + moveDown = ( + + { _t("Move down") } + + ); + } + return

      { _t('View Community') } + { (moveUp || moveDown) ?
      : null } + { moveUp } + { moveDown }
      - { _t('Hide') } + { _t("Unpin") }
      ; } diff --git a/src/components/views/elements/DNDTagTile.js b/src/components/views/elements/DNDTagTile.js index eaaa0f183b..2e88d37882 100644 --- a/src/components/views/elements/DNDTagTile.js +++ b/src/components/views/elements/DNDTagTile.js @@ -30,7 +30,7 @@ export default function DNDTagTile(props) { const TagTileContextMenu = sdk.getComponent('context_menus.TagTileContextMenu'); contextMenu = ( - + ); } diff --git a/src/components/views/groups/GroupTile.js b/src/components/views/groups/GroupTile.js index ce42662462..dd8366bbe0 100644 --- a/src/components/views/groups/GroupTile.js +++ b/src/components/views/groups/GroupTile.js @@ -22,6 +22,9 @@ import FlairStore from '../../../stores/FlairStore'; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import {replaceableComponent} from "../../../utils/replaceableComponent"; import {mediaFromMxc} from "../../../customisations/Media"; +import { _t } from "../../../languageHandler"; +import TagOrderActions from "../../../actions/TagOrderActions"; +import GroupFilterOrderStore from "../../../stores/GroupFilterOrderStore"; @replaceableComponent("views.groups.GroupTile") class GroupTile extends React.Component { @@ -60,6 +63,18 @@ class GroupTile extends React.Component { }); }; + onPinClick = e => { + e.preventDefault(); + e.stopPropagation(); + dis.dispatch(TagOrderActions.moveTag(this.context, this.props.groupId, 0)); + }; + + onUnpinClick = e => { + e.preventDefault(); + e.stopPropagation(); + dis.dispatch(TagOrderActions.removeTag(this.context, this.props.groupId)); + }; + render() { const BaseAvatar = sdk.getComponent('avatars.BaseAvatar'); const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); @@ -90,6 +105,14 @@ class GroupTile extends React.Component {
      { name }
      { descElement }
      { this.props.groupId }
      + { !(GroupFilterOrderStore.getOrderedTags() || []).includes(this.props.groupId) + ? + { _t("Pin") } + + : + { _t("Unpin") } + + }
      ; } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 3d6fcb8643..85647a17e5 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2477,6 +2477,8 @@ "Update status": "Update status", "Set status": "Set status", "Set a new status...": "Set a new status...", + "Move up": "Move up", + "Move down": "Move down", "View Community": "View Community", "Unable to start audio streaming.": "Unable to start audio streaming.", "Failed to start livestream": "Failed to start livestream", @@ -2623,7 +2625,7 @@ "%(count)s messages deleted.|one": "%(count)s message deleted.", "Your Communities": "Your Communities", "Did you know: you can use communities to filter your %(brand)s experience!": "Did you know: you can use communities to filter your %(brand)s experience!", - "To set up a filter, drag a community avatar over to the filter panel on the far left hand side of the screen. You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.": "To set up a filter, drag a community avatar over to the filter panel on the far left hand side of the screen. You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.", + "You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.": "You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.", "Error whilst fetching joined communities": "Error whilst fetching joined communities", "Create a new community": "Create a new community", "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.": "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.", From bc89cf14dd49b30183625fd78a1b989dbcb26953 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 2 Jun 2021 11:53:47 +0100 Subject: [PATCH 337/999] ignore hash/fragment when de-duplicating links for url previews --- src/components/views/messages/TextualBody.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js index dc644f1009..3adfea6ee6 100644 --- a/src/components/views/messages/TextualBody.js +++ b/src/components/views/messages/TextualBody.js @@ -278,15 +278,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._content.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; - }); + // 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(); + }))); - this.setState({ links: links }); + this.setState({ links }); // lazy-load the hidden state of the preview widget from localstorage if (global.localStorage) { From 35948374e91d4cf6e3110de4218d043be9ff4db0 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 2 Jun 2021 11:56:49 +0100 Subject: [PATCH 338/999] remove unused imports --- src/components/structures/LoggedInView.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx index f5df99d8c9..388616c55e 100644 --- a/src/components/structures/LoggedInView.tsx +++ b/src/components/structures/LoggedInView.tsx @@ -29,8 +29,6 @@ import dis from '../../dispatcher/dispatcher'; import { IMatrixClientCreds } from '../../MatrixClientPeg'; import SettingsStore from "../../settings/SettingsStore"; -import TagOrderActions from '../../actions/TagOrderActions'; -import RoomListActions from '../../actions/RoomListActions'; import ResizeHandle from '../views/elements/ResizeHandle'; import {Resizer, CollapseDistributor} from '../../resizer'; import MatrixClientContext from "../../contexts/MatrixClientContext"; From 079a5c10ad8191d6d3c357400c948253b0738997 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 2 Jun 2021 16:43:38 +0100 Subject: [PATCH 339/999] Respect space ordering field in m.tag for top level spaces --- .../structures/SpaceRoomDirectory.tsx | 4 +-- src/stores/SpaceStore.tsx | 33 ++++++++++++++++--- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/src/components/structures/SpaceRoomDirectory.tsx b/src/components/structures/SpaceRoomDirectory.tsx index 8d59fe6c68..2b4fb24c1b 100644 --- a/src/components/structures/SpaceRoomDirectory.tsx +++ b/src/components/structures/SpaceRoomDirectory.tsx @@ -39,7 +39,7 @@ import {mediaFromMxc} from "../../customisations/Media"; import InfoTooltip from "../views/elements/InfoTooltip"; import TextWithTooltip from "../views/elements/TextWithTooltip"; import {useStateToggle} from "../../hooks/useStateToggle"; -import {getOrder} from "../../stores/SpaceStore"; +import {getChildOrder} from "../../stores/SpaceStore"; import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton"; import {linkifyElement} from "../../HtmlUtils"; @@ -286,7 +286,7 @@ export const HierarchyLevel = ({ const children = Array.from(relations.get(spaceId)?.values() || []); const sortedChildren = sortBy(children, ev => { // XXX: Space Summary API doesn't give the child origin_server_ts but once it does we should use it for sorting - return getOrder(ev.content.order, null, ev.state_key); + return getChildOrder(ev.content.order, null, ev.state_key); }); const [subspaces, childRooms] = sortedChildren.reduce((result, ev: ISpaceSummaryEvent) => { const roomId = ev.state_key; diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index 40997d30a8..1333fc5d37 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -33,6 +33,7 @@ import {EnhancedMap, mapDiff} from "../utils/maps"; import {setHasDiff} from "../utils/sets"; import {ISpaceSummaryEvent, ISpaceSummaryRoom} from "../components/structures/SpaceRoomDirectory"; import RoomViewStore from "./RoomViewStore"; +import { arrayHasOrderChange } from "../utils/arrays"; interface IState {} @@ -60,8 +61,16 @@ const partitionSpacesAndRooms = (arr: Room[]): [Room[], Room[]] => { // [spaces, }, [[], []]); }; +const SpaceTagOrderingField = "org.matrix.mscXXXX.space"; + +const getSpaceTagOrdering = (space: Room): number | undefined => { + return space?.getAccountData(EventType.Tag)?.getContent()?.tags?.[SpaceTagOrderingField]?.order; +}; + +const sortRootSpaces = (spaces: Room[]): Room[] => sortBy(spaces, [getSpaceTagOrdering, "roomId"]); + // For sorting space children using a validated `order`, `m.room.create`'s `origin_server_ts`, `room_id` -export const getOrder = (order: string, creationTs: number, roomId: string): Array>> => { +export const getChildOrder = (order: string, creationTs: number, roomId: string): Array>> => { let validatedOrder: string = null; if (typeof order === "string" && Array.from(order).every((c: string) => { @@ -214,7 +223,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { const roomId = ev.getStateKey(); const childRoom = this.matrixClient?.getRoom(roomId); const createTs = childRoom?.currentState.getStateEvents(EventType.RoomCreate, "")?.getTs(); - return getOrder(ev.getContent().order, createTs, roomId); + return getChildOrder(ev.getContent().order, createTs, roomId); }).map(ev => { return this.matrixClient.getRoom(ev.getStateKey()); }).filter(room => { @@ -326,7 +335,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { // rootSpaces.push(space); // }); - this.rootSpaces = rootSpaces; + this.rootSpaces = sortRootSpaces(rootSpaces); this.parentMap = backrefs; // if the currently selected space no longer exists, remove its selection @@ -338,7 +347,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { this.emit(UPDATE_TOP_LEVEL_SPACES, this.spacePanelSpaces); // build initial state of invited spaces as we would have missed the emitted events about the room at launch - this._invitedSpaces = new Set(invitedSpaces); + this._invitedSpaces = new Set(sortRootSpaces(invitedSpaces)); this.emit(UPDATE_INVITED_SPACES, this.invitedSpaces); }, 100, {trailing: true, leading: true}); @@ -472,6 +481,20 @@ export class SpaceStoreClass extends AsyncStoreWithClient { } }; + private onRoomAccountData = (ev: MatrixEvent, room: Room, lastEv?: MatrixEvent) => { + if (!room.isSpaceRoom() || ev.getType() !== EventType.Tag) return; + + const order = ev.getContent()?.tags?.[SpaceTagOrderingField]?.order; + const lastOrder = lastEv?.getContent()?.tags?.[SpaceTagOrderingField]?.order; + if (order !== lastOrder) { + const rootSpaces = sortRootSpaces(this.rootSpaces); + if (arrayHasOrderChange(this.rootSpaces, rootSpaces)) { + this.rootSpaces = rootSpaces; + this.emit(UPDATE_TOP_LEVEL_SPACES, this.spacePanelSpaces); + } + } + }; + private onRoomState = (ev: MatrixEvent) => { const room = this.matrixClient.getRoom(ev.getRoomId()); if (!room) return; @@ -516,6 +539,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { if (this.matrixClient) { this.matrixClient.removeListener("Room", this.onRoom); this.matrixClient.removeListener("Room.myMembership", this.onRoom); + this.matrixClient.removeListener("Room.accountData", this.onRoomAccountData); this.matrixClient.removeListener("RoomState.events", this.onRoomState); } await this.reset(); @@ -525,6 +549,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { if (!SettingsStore.getValue("feature_spaces")) return; this.matrixClient.on("Room", this.onRoom); this.matrixClient.on("Room.myMembership", this.onRoom); + this.matrixClient.on("Room.accountData", this.onRoomAccountData); this.matrixClient.on("RoomState.events", this.onRoomState); await this.onSpaceUpdate(); // trigger an initial update From 2c4fa73a4571ff3da3ae55e60ae8facc7748006e Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 2 Jun 2021 17:39:13 +0100 Subject: [PATCH 340/999] Map phone number lookup results to their native rooms When dialing a phone number, also look to see if there's a corresponding native user for the resulting user, and if so, go to the native room for that user. --- src/CallHandler.tsx | 33 +++++++++- src/VoipUserMapper.ts | 6 +- src/components/views/voip/DialPadModal.tsx | 23 ++----- src/dispatcher/actions.ts | 8 +++ test/CallHandler-test.ts | 73 ++++++++++++++-------- 5 files changed, 95 insertions(+), 48 deletions(-) diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx index a05d3a25c8..ba0178fa59 100644 --- a/src/CallHandler.tsx +++ b/src/CallHandler.tsx @@ -521,7 +521,9 @@ export default class CallHandler extends EventEmitter { let newNativeAssertedIdentity = newAssertedIdentity; if (newAssertedIdentity) { const response = await this.sipNativeLookup(newAssertedIdentity); - if (response.length) newNativeAssertedIdentity = response[0].userid; + if (response.length && response[0].fields.lookup_success) { + newNativeAssertedIdentity = response[0].userid; + } } console.log(`Asserted identity ${newAssertedIdentity} mapped to ${newNativeAssertedIdentity}`); @@ -862,9 +864,38 @@ export default class CallHandler extends EventEmitter { }); break; } + case Action.DialNumber: + this.dialNumber(payload.number); + break; } } + private async dialNumber(number: string) { + const results = await this.pstnLookup(number); + if (!results || results.length === 0 || !results[0].userid) { + Modal.createTrackedDialog('', '', ErrorDialog, { + title: _t("Unable to look up phone number"), + description: _t("There was an error looking up the phone number"), + }); + return; + } + const userId = results[0].userid; + + // Now check to see if this is a virtual user, in which case we should find the + // native user + const nativeLookupResults = await this.sipNativeLookup(userId); + const lookupSuccess = nativeLookupResults.length > 0 && nativeLookupResults[0].fields.lookup_success; + const nativeUserId = lookupSuccess ? nativeLookupResults[0].userid : userId; + console.log("Looked up " + number + " to " + userId + " and mapped to native user " + nativeUserId); + + const roomId = await ensureDMExists(MatrixClientPeg.get(), nativeUserId); + + dis.dispatch({ + action: 'view_room', + room_id: roomId, + }); + } + setActiveCallRoomId(activeCallRoomId: string) { logger.info("Setting call in room " + activeCallRoomId + " active"); diff --git a/src/VoipUserMapper.ts b/src/VoipUserMapper.ts index e5bed2e812..d576a5434c 100644 --- a/src/VoipUserMapper.ts +++ b/src/VoipUserMapper.ts @@ -33,7 +33,7 @@ export default class VoipUserMapper { private async userToVirtualUser(userId: string): Promise { const results = await CallHandler.sharedInstance().sipVirtualLookup(userId); - if (results.length === 0) return null; + if (results.length === 0 || !results[0].fields.lookup_success) return null; return results[0].userid; } @@ -82,14 +82,14 @@ export default class VoipUserMapper { return Boolean(claimedNativeRoomId); } - public async onNewInvitedRoom(invitedRoom: Room) { + public async onNewInvitedRoom(invitedRoom: Room): Promise { if (!CallHandler.sharedInstance().getSupportsVirtualRooms()) return; const inviterId = invitedRoom.getDMInviter(); console.log(`Checking virtual-ness of room ID ${invitedRoom.roomId}, invited by ${inviterId}`); const result = await CallHandler.sharedInstance().sipNativeLookup(inviterId); if (result.length === 0) { - return true; + return; } if (result[0].fields.is_virtual) { diff --git a/src/components/views/voip/DialPadModal.tsx b/src/components/views/voip/DialPadModal.tsx index cdd5bc6641..f3ee49e4ac 100644 --- a/src/components/views/voip/DialPadModal.tsx +++ b/src/components/views/voip/DialPadModal.tsx @@ -15,17 +15,13 @@ limitations under the License. */ import * as React from "react"; -import { ensureDMExists } from "../../../createRoom"; import { _t } from "../../../languageHandler"; -import { MatrixClientPeg } from "../../../MatrixClientPeg"; import AccessibleButton from "../elements/AccessibleButton"; import Field from "../elements/Field"; import DialPad from './DialPad'; import dis from '../../../dispatcher/dispatcher'; -import Modal from "../../../Modal"; -import ErrorDialog from "../../views/dialogs/ErrorDialog"; -import CallHandler from "../../../CallHandler"; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { Action } from "../../../dispatcher/actions"; interface IProps { onFinished: (boolean) => void; @@ -67,21 +63,10 @@ export default class DialpadModal extends React.PureComponent { } onDialPress = async () => { - const results = await CallHandler.sharedInstance().pstnLookup(this.state.value); - if (!results || results.length === 0 || !results[0].userid) { - Modal.createTrackedDialog('', '', ErrorDialog, { - title: _t("Unable to look up phone number"), - description: _t("There was an error looking up the phone number"), - }); - } - const userId = results[0].userid; - - const roomId = await ensureDMExists(MatrixClientPeg.get(), userId); - dis.dispatch({ - action: 'view_room', - room_id: roomId, - }); + action: Action.DialNumber, + number: this.state.value, + }) this.props.onFinished(true); } diff --git a/src/dispatcher/actions.ts b/src/dispatcher/actions.ts index 9fc0b54eea..073e82bef6 100644 --- a/src/dispatcher/actions.ts +++ b/src/dispatcher/actions.ts @@ -100,6 +100,14 @@ export enum Action { */ OpenDialPad = "open_dial_pad", + /** + * Dial the phone number in the payload + * payload: { + * number: , + * } + */ + DialNumber = "dial_number", + /** * Fired when CallHandler has checked for PSTN protocol support * payload: none diff --git a/test/CallHandler-test.ts b/test/CallHandler-test.ts index 1e3f92e788..a93a134133 100644 --- a/test/CallHandler-test.ts +++ b/test/CallHandler-test.ts @@ -23,8 +23,8 @@ import dis from '../src/dispatcher/dispatcher'; import { CallEvent, CallState } from 'matrix-js-sdk/src/webrtc/call'; import DMRoomMap from '../src/utils/DMRoomMap'; import EventEmitter from 'events'; -import { Action } from '../src/dispatcher/actions'; import SdkConfig from '../src/SdkConfig'; +import { ActionPayload } from '../src/dispatcher/payloads'; const REAL_ROOM_ID = '$room1:example.org'; const MAPPED_ROOM_ID = '$room2:example.org'; @@ -75,6 +75,18 @@ class FakeCall extends EventEmitter { } } +function untilDispatch(waitForAction: string): Promise { + let dispatchHandle; + return new Promise(resolve => { + dispatchHandle = dis.register(payload => { + if (payload.action === waitForAction) { + dis.unregister(dispatchHandle); + resolve(payload); + } + }); + }); +} + describe('CallHandler', () => { let dmRoomMap; let callHandler; @@ -94,6 +106,21 @@ describe('CallHandler', () => { callHandler = new CallHandler(); callHandler.start(); + const realRoom = mkStubDM(REAL_ROOM_ID, '@user1:example.org'); + const mappedRoom = mkStubDM(MAPPED_ROOM_ID, '@user2:example.org'); + const mappedRoom2 = mkStubDM(MAPPED_ROOM_ID_2, '@user3:example.org'); + + MatrixClientPeg.get().getRoom = roomId => { + switch (roomId) { + case REAL_ROOM_ID: + return realRoom; + case MAPPED_ROOM_ID: + return mappedRoom; + case MAPPED_ROOM_ID_2: + return mappedRoom2; + } + }; + dmRoomMap = { getUserIdForRoomId: roomId => { if (roomId === REAL_ROOM_ID) { @@ -134,38 +161,34 @@ describe('CallHandler', () => { SdkConfig.unset(); }); + it('should look up the correct user and open the room when a phone number is dialled', async () => { + MatrixClientPeg.get().getThirdpartyUser = jest.fn().mockResolvedValue([{ + userid: '@user2:example.org', + protocol: "im.vector.protocol.sip_native", + fields: { + is_native: true, + lookup_success: true, + }, + }]); + + dis.dispatch({ + action: 'dial_number', + number: '01818118181', + }, true); + + const viewRoomPayload = await untilDispatch('view_room'); + expect(viewRoomPayload.room_id).toEqual(MAPPED_ROOM_ID); + }); + it('should move calls between rooms when remote asserted identity changes', async () => { - const realRoom = mkStubDM(REAL_ROOM_ID, '@user1:example.org'); - const mappedRoom = mkStubDM(MAPPED_ROOM_ID, '@user2:example.org'); - const mappedRoom2 = mkStubDM(MAPPED_ROOM_ID_2, '@user3:example.org'); - - MatrixClientPeg.get().getRoom = roomId => { - switch (roomId) { - case REAL_ROOM_ID: - return realRoom; - case MAPPED_ROOM_ID: - return mappedRoom; - case MAPPED_ROOM_ID_2: - return mappedRoom2; - } - }; - dis.dispatch({ action: 'place_call', type: PlaceCallType.Voice, room_id: REAL_ROOM_ID, }, true); - let dispatchHandle; // wait for the call to be set up - await new Promise(resolve => { - dispatchHandle = dis.register(payload => { - if (payload.action === 'call_state') { - resolve(); - } - }); - }); - dis.unregister(dispatchHandle); + await untilDispatch('call_state'); // should start off in the actual room ID it's in at the protocol level expect(callHandler.getCallForRoom(REAL_ROOM_ID)).toBe(fakeCall); From 0aeddea30f2f54d7cb06ae0a1c58be52b654d75a Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 2 Jun 2021 17:47:29 +0100 Subject: [PATCH 341/999] Only do native lookup if it's supported Also fix as bug where we were checking the wrong field to check for native/virtual support: oops. --- src/CallHandler.tsx | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx index ba0178fa59..0d87451b5f 100644 --- a/src/CallHandler.tsx +++ b/src/CallHandler.tsx @@ -264,7 +264,7 @@ export default class CallHandler extends EventEmitter { } public getSupportsVirtualRooms() { - return this.supportsPstnProtocol; + return this.supportsSipNativeVirtual; } public pstnLookup(phoneNumber: string): Promise { @@ -883,10 +883,15 @@ export default class CallHandler extends EventEmitter { // Now check to see if this is a virtual user, in which case we should find the // native user - const nativeLookupResults = await this.sipNativeLookup(userId); - const lookupSuccess = nativeLookupResults.length > 0 && nativeLookupResults[0].fields.lookup_success; - const nativeUserId = lookupSuccess ? nativeLookupResults[0].userid : userId; - console.log("Looked up " + number + " to " + userId + " and mapped to native user " + nativeUserId); + let nativeUserId; + if (this.getSupportsVirtualRooms()) { + const nativeLookupResults = await this.sipNativeLookup(userId); + const lookupSuccess = nativeLookupResults.length > 0 && nativeLookupResults[0].fields.lookup_success; + nativeUserId = lookupSuccess ? nativeLookupResults[0].userid : userId; + console.log("Looked up " + number + " to " + userId + " and mapped to native user " + nativeUserId); + } else { + nativeUserId = userId; + } const roomId = await ensureDMExists(MatrixClientPeg.get(), nativeUserId); From 58652152c13039c485ccd4a72a92a7915698a6e2 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 2 Jun 2021 17:52:26 +0100 Subject: [PATCH 342/999] Strings are now in a different place... --- src/i18n/strings/en_EN.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 3d6fcb8643..ada264d110 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -63,6 +63,8 @@ "Already in call": "Already in call", "You're already in a call with this person.": "You're already in a call with this person.", "You cannot place a call with yourself.": "You cannot place a call with yourself.", + "Unable to look up phone number": "Unable to look up phone number", + "There was an error looking up the phone number": "There was an error looking up the phone number", "Call in Progress": "Call in Progress", "A call is currently being placed!": "A call is currently being placed!", "Permission Required": "Permission Required", @@ -898,8 +900,6 @@ "Fill Screen": "Fill Screen", "Return to call": "Return to call", "%(name)s on hold": "%(name)s on hold", - "Unable to look up phone number": "Unable to look up phone number", - "There was an error looking up the phone number": "There was an error looking up the phone number", "Dial pad": "Dial pad", "Unknown caller": "Unknown caller", "Incoming voice call": "Incoming voice call", From 53fc47553906b662d6b27796b3bca6e44a04bfaa Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 3 Jun 2021 08:03:31 +0100 Subject: [PATCH 343/999] Update src/components/views/rooms/PinnedEventTile.tsx Co-authored-by: Travis Ralston --- src/components/views/rooms/PinnedEventTile.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/PinnedEventTile.tsx b/src/components/views/rooms/PinnedEventTile.tsx index 83eee7d81d..774dea70c8 100644 --- a/src/components/views/rooms/PinnedEventTile.tsx +++ b/src/components/views/rooms/PinnedEventTile.tsx @@ -40,7 +40,7 @@ const AVATAR_SIZE = 24; @replaceableComponent("views.rooms.PinnedEventTile") export default class PinnedEventTile extends React.Component { - static contextType = MatrixClientContext; + public static contextType = MatrixClientContext; private onTileClicked = () => { dis.dispatch({ From c1a763fb4f866268bc70c8ef44905cc75563703d Mon Sep 17 00:00:00 2001 From: zopieux Date: Wed, 2 Jun 2021 23:47:21 +0000 Subject: [PATCH 344/999] Translated using Weblate (French) Currently translated at 100.0% (2979 of 2979 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/fr/ --- src/i18n/strings/fr.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index 5a8208f50b..ff48785427 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -2868,7 +2868,7 @@ "The %(capability)s capability": "La capacité %(capability)s", "See %(eventType)s events posted to your active room": "Voir les événements %(eventType)s publiés dans votre salon actuel", "Send %(eventType)s events as you in your active room": "Envoie des événements %(eventType)s sous votre nom dans votre salon actuel", - "See %(eventType)s events posted to this room": "Voir les événements %(eventType)s publiés dans ce salon", + "See %(eventType)s events posted to this room": "Voir les événements %(eventType)s envoyés dans ce salon", "Send %(eventType)s events as you in this room": "Envoie des événements %(eventType)s sous votre nom dans ce salon", "Send stickers to your active room as you": "Envoie des autocollants sous votre nom dans le salon actuel", "Continue with %(ssoButtons)s": "Continuer avec %(ssoButtons)s", @@ -2930,7 +2930,7 @@ "Don't miss a reply": "Ne ratez pas une réponse", "See %(msgtype)s messages posted to your active room": "Voir les messages de type %(msgtype)s publiés dans le salon actuel", "See %(msgtype)s messages posted to this room": "Voir les messages de type %(msgtype)s publiés dans ce salon", - "Send %(msgtype)s messages as you in this room": "Envoie des messages de type%(msgtype)s sous votre nom dans ce salon", + "Send %(msgtype)s messages as you in this room": "Envoie les messages de type %(msgtype)s sous votre nom dans ce salon", "Send %(msgtype)s messages as you in your active room": "Envoie des messages de type %(msgtype)s sous votre nom dans votre salon actif", "See general files posted to your active room": "Voir les fichiers postés dans votre salon actuel", "See general files posted to this room": "Voir les fichiers postés dans ce salon", From b4f8c66f7b2645a83b660478b006b7d0414f4b00 Mon Sep 17 00:00:00 2001 From: Miquel Lionel Date: Wed, 2 Jun 2021 23:37:33 +0000 Subject: [PATCH 345/999] Translated using Weblate (French) Currently translated at 100.0% (2979 of 2979 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/fr/ --- src/i18n/strings/fr.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index ff48785427..e94dbd6c1f 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -2842,7 +2842,7 @@ "Change the topic of your active room": "Changer le sujet dans le salon actuel", "Change the topic of this room": "Changer le sujet de ce salon", "Send stickers into this room": "Envoyer des autocollants dans ce salon", - "Remain on your screen when viewing another room, when running": "Reste sur votre écran quand vous regardez un autre salon lors de l’appel", + "Remain on your screen when viewing another room, when running": "Reste sur votre écran lors de l'appel quand vous regardez un autre salon", "Takes the call in the current room off hold": "Reprend l’appel en attente dans ce salon", "Places the call in the current room on hold": "Met l’appel dans ce salon en attente", "Prepends (╯°□°)╯︵ ┻━┻ to a plain-text message": "Ajoute (╯°□°)╯︵ ┻━┻ en préfixe du message", @@ -2861,7 +2861,7 @@ "Send videos as you in this room": "Envoie des vidéos sous votre nom dans ce salon", "See images posted to this room": "Voir les images publiées dans ce salon", "See images posted to your active room": "Voir les images publiées dans votre salon actif", - "See messages posted to your active room": "Voir les messages publiés dans votre salon actif", + "See messages posted to your active room": "Voir les messages envoyés dans le salon actuel", "See messages posted to this room": "Voir les messages publiés dans ce salon", "Send messages as you in your active room": "Envoie des messages sous votre nom dans votre salon actif", "Send messages as you in this room": "Envoie des messages sous votre nom dans ce salon", @@ -3034,7 +3034,7 @@ "Send text messages as you in this room": "Envoyez des messages textuels sous votre nom dans ce salon", "See when the name changes in your active room": "Suivre les changements de nom dans le salon actif", "Change which room, message, or user you're viewing": "Changer le salon, message, ou la personne que vous visualisez", - "Change which room you're viewing": "Changer le salon que vous visualisez", + "Change which room you're viewing": "Changer le salon que vous êtes en train de lire", "Remain on your screen while running": "Reste sur votre écran pendant l’exécution", "%(senderName)s has updated the widget layout": "%(senderName)s a mis à jour la disposition du widget", "Converts the DM to a room": "Transforme la conversation privée en salon", From 9f1e1c24205fdc76125f7f1e4351b939b6b7d7b6 Mon Sep 17 00:00:00 2001 From: c-cal Date: Tue, 1 Jun 2021 15:37:08 +0000 Subject: [PATCH 346/999] Translated using Weblate (French) Currently translated at 100.0% (2979 of 2979 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/fr/ --- src/i18n/strings/fr.json | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index e94dbd6c1f..7f3614bf90 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -1873,7 +1873,7 @@ "This user has not verified all of their sessions.": "Cet utilisateur n’a pas vérifié toutes ses sessions.", "You have verified this user. This user has verified all of their sessions.": "Vous avez vérifié cet utilisateur. Cet utilisateur a vérifié toutes ses sessions.", "Someone is using an unknown session": "Quelqu’un utilise une session inconnue", - "Mod": "Modo", + "Mod": "Modérateur", "Your key share request has been sent - please check your other sessions for key share requests.": "Votre demande de partage de clé a été envoyée − vérifiez les demandes de partage de clé sur vos autres sessions.", "Key share requests are sent to your other sessions automatically. If you rejected or dismissed the key share request on your other sessions, click here to request the keys for this session again.": "Les demandes de partage de clé sont envoyées à vos autres sessions automatiquement. Si vous avez rejeté ou ignoré la demande de partage de clé sur vos autres sessions, cliquez ici pour redemander les clés pour cette session.", "If your other sessions do not have the key for this message you will not be able to decrypt them.": "Si vos autres sessions n’ont pas la clé pour ce message vous ne pourrez pas le déchiffrer.", @@ -3111,7 +3111,7 @@ "No permissions": "Aucune permission", "Remove from Space": "Supprimer de l’espace", "Undo": "Annuler", - "Your message wasn't sent because this homeserver has been blocked by it's administrator. Please contact your service administrator to continue using the service.": "Votre message n’a pas été envoyé car ce serveur d’accueil a été banni par son administrateur. Merci de contacter votre administrateur de service pour poursuivre l’usage de ce service.", + "Your message wasn't sent because this homeserver has been blocked by it's administrator. Please contact your service administrator to continue using the service.": "Votre message n’a pas été envoyé car ce serveur d’accueil a été bloqué par son administrateur. Merci de contacter votre administrateur de service pour continuer à utiliser le service.", "Are you sure you want to leave the space '%(spaceName)s'?": "Êtes-vous sûr de vouloir quitter l’espace « %(spaceName)s » ?", "This space is not public. You will not be able to rejoin without an invite.": "Cet espace n’est pas public. Vous ne pourrez pas le rejoindre sans invitation.", "Start audio stream": "Démarrer une diffusion audio", @@ -3352,5 +3352,11 @@ "sends space invaders": "Envoie les Space Invaders", "Sends the given message with a space themed effect": "Envoyer le message avec un effet lié au thème de l’espace", "See when people join, leave, or are invited to your active room": "Afficher quand des personnes rejoignent, partent, ou sont invités dans votre salon actif", - "Kick, ban, or invite people to your active room, and make you leave": "Expulser, bannir ou inviter des personnes dans votre salon actif et en partir" + "Kick, ban, or invite people to your active room, and make you leave": "Expulser, bannir ou inviter des personnes dans votre salon actif et en partir", + "Currently joining %(count)s rooms|one": "Vous êtes en train de rejoindre %(count)s salon", + "Currently joining %(count)s rooms|other": "Vous êtes en train de rejoindre %(count)s salons", + "Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "Essayez d'autres mots ou vérifiez les fautes de frappe. Certains salons peuvent ne pas être visibles car ils sont privés et vous devez être invité pour les rejoindre.", + "No results for \"%(query)s\"": "Aucun résultat pour « %(query)s »", + "The user you called is busy.": "L’utilisateur que vous avez appelé est indisponible.", + "User Busy": "Utilisateur indisponible" } From 3f12b7280d801ac505caec71d2a2c095ab68d3c9 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 3 Jun 2021 08:31:06 +0100 Subject: [PATCH 347/999] Make AutoHideScrollbar pass through all unknown props --- .../structures/AutoHideScrollbar.tsx | 18 +++++++++++------- .../structures/IndicatorScrollbar.js | 11 +++++++---- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/src/components/structures/AutoHideScrollbar.tsx b/src/components/structures/AutoHideScrollbar.tsx index 66f998b616..e5fa124fed 100644 --- a/src/components/structures/AutoHideScrollbar.tsx +++ b/src/components/structures/AutoHideScrollbar.tsx @@ -15,9 +15,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from "react"; +import React, {HTMLAttributes} from "react"; -interface IProps { +interface IProps extends HTMLAttributes { className?: string; onScroll?: () => void; onWheel?: () => void; @@ -52,14 +52,18 @@ export default class AutoHideScrollbar extends React.Component { } public render() { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { className, onScroll, onWheel, style, tabIndex, wrappedRef, children, ...otherProps } = this.props; + return (
      - { this.props.children } + { children }
      ); } } diff --git a/src/components/structures/IndicatorScrollbar.js b/src/components/structures/IndicatorScrollbar.js index 51a3b287f0..25dcaeed39 100644 --- a/src/components/structures/IndicatorScrollbar.js +++ b/src/components/structures/IndicatorScrollbar.js @@ -185,21 +185,24 @@ export default class IndicatorScrollbar extends React.Component { }; render() { + // eslint-disable-next-line no-unused-vars + const { children, trackHorizontalOverflow, verticalScrollsHorizontally, ...otherProps } = this.props; + const leftIndicatorStyle = {left: this.state.leftIndicatorOffset}; const rightIndicatorStyle = {right: this.state.rightIndicatorOffset}; - const leftOverflowIndicator = this.props.trackHorizontalOverflow + const leftOverflowIndicator = trackHorizontalOverflow ?
      : null; - const rightOverflowIndicator = this.props.trackHorizontalOverflow + const rightOverflowIndicator = trackHorizontalOverflow ?
      : null; return ( { leftOverflowIndicator } - { this.props.children } + { children } { rightOverflowIndicator } ); } From e334ce81920723832c9260b0f009df88805e22a1 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 3 Jun 2021 08:32:36 +0100 Subject: [PATCH 348/999] First cut of space panel drag-and-drop ordering --- package.json | 2 + res/css/structures/_SpacePanel.scss | 7 +- src/components/views/spaces/SpacePanel.tsx | 136 +++++++++++------- .../views/spaces/SpaceTreeLevel.tsx | 27 ++-- src/stores/SpaceStore.tsx | 71 +++++++-- yarn.lock | 100 ++++++++++++- 6 files changed, 263 insertions(+), 80 deletions(-) diff --git a/package.json b/package.json index 270c86ddba..2d2506e1df 100644 --- a/package.json +++ b/package.json @@ -91,6 +91,7 @@ "qs": "^6.9.6", "re-resizable": "^6.9.0", "react": "^16.14.0", + "react-beautiful-dnd": "^13.1.0", "react-dom": "^16.14.0", "react-focus-lock": "^2.5.0", "react-transition-group": "^4.4.1", @@ -135,6 +136,7 @@ "@types/parse5": "^6.0.0", "@types/qrcode": "^1.3.5", "@types/react": "^16.9", + "@types/react-beautiful-dnd": "^13.0.0", "@types/react-dom": "^16.9.10", "@types/react-transition-group": "^4.4.0", "@types/sanitize-html": "^2.3.1", diff --git a/res/css/structures/_SpacePanel.scss b/res/css/structures/_SpacePanel.scss index c433ccf275..e64057d16c 100644 --- a/res/css/structures/_SpacePanel.scss +++ b/res/css/structures/_SpacePanel.scss @@ -31,7 +31,6 @@ $activeBorderColor: $secondary-fg-color; // Create another flexbox so the Panel fills the container display: flex; flex-direction: column; - overflow-y: auto; .mx_SpacePanel_spaceTreeWrapper { flex: 1; @@ -69,6 +68,12 @@ $activeBorderColor: $secondary-fg-color; cursor: pointer; } + .mx_SpaceItem_dragging { + .mx_SpaceButton_toggleCollapse { + visibility: hidden; + } + } + .mx_SpaceTreeLevel { display: flex; flex-direction: column; diff --git a/src/components/views/spaces/SpacePanel.tsx b/src/components/views/spaces/SpacePanel.tsx index eb63b21f0e..27f097e9d4 100644 --- a/src/components/views/spaces/SpacePanel.tsx +++ b/src/components/views/spaces/SpacePanel.tsx @@ -15,8 +15,9 @@ limitations under the License. */ import React, { useEffect, useState } from "react"; +import { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd"; import classNames from "classnames"; -import {Room} from "matrix-js-sdk/src/models/room"; +import { Room } from "matrix-js-sdk/src/models/room"; import {_t} from "../../../languageHandler"; import RoomAvatar from "../avatars/RoomAvatar"; @@ -204,58 +205,89 @@ const SpacePanel = () => { }; const activeSpaces = activeSpace ? [activeSpace] : []; - const expandCollapseButtonTitle = isPanelCollapsed ? _t("Expand space panel") : _t("Collapse space panel"); - // TODO drag and drop for re-arranging order - return - {({onKeyDownHandler}) => ( -
        - -
        - SpaceStore.instance.setActiveSpace(null)} - selected={!activeSpace} - tooltip={_t("All rooms")} - notificationState={RoomNotificationStateStore.instance.globalState} - isNarrow={isPanelCollapsed} + return ( + { + if (!result.destination) return; // dropped outside the list + SpaceStore.instance.moveRootSpace(result.source.index, result.destination.index); + }}> + + {({onKeyDownHandler}) => ( +
          + + {(provided, snapshot) => ( + +
          + SpaceStore.instance.setActiveSpace(null)} + selected={!activeSpace} + tooltip={_t("All rooms")} + notificationState={RoomNotificationStateStore.instance.globalState} + isNarrow={isPanelCollapsed} + /> + { invites.map(s => ( + setPanelCollapsed(false)} + /> + )) } + { spaces.map((s, i) => ( + + {(provided, snapshot) => ( + setPanelCollapsed(false)} + /> + )} + + )) } + { provided.placeholder } +
          + { + if (!isPanelCollapsed) setPanelCollapsed(true); + openMenu(); + }} + isNarrow={isPanelCollapsed} + /> +
          + )} +
          + setPanelCollapsed(!isPanelCollapsed)} + title={isPanelCollapsed ? _t("Expand space panel") : _t("Collapse space panel")} /> - { invites.map(s => setPanelCollapsed(false)} - />) } - { spaces.map(s => setPanelCollapsed(false)} - />) } -
        - { - if (!isPanelCollapsed) setPanelCollapsed(true); - openMenu(); - }} - isNarrow={isPanelCollapsed} - /> -
        - setPanelCollapsed(!isPanelCollapsed)} - title={expandCollapseButtonTitle} - /> - { contextMenu } -
      - )} -
      + { contextMenu } + + )} + + + ); }; export default SpacePanel; diff --git a/src/components/views/spaces/SpaceTreeLevel.tsx b/src/components/views/spaces/SpaceTreeLevel.tsx index f34baf256b..7ac863b239 100644 --- a/src/components/views/spaces/SpaceTreeLevel.tsx +++ b/src/components/views/spaces/SpaceTreeLevel.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from "react"; +import React, {InputHTMLAttributes, LegacyRef} from "react"; import classNames from "classnames"; import {Room} from "matrix-js-sdk/src/models/room"; @@ -49,13 +49,14 @@ import {EventType} from "matrix-js-sdk/src/@types/event"; import {StaticNotificationState} from "../../../stores/notifications/StaticNotificationState"; import {NotificationColor} from "../../../stores/notifications/NotificationColor"; -interface IItemProps { +interface IItemProps extends InputHTMLAttributes { space?: Room; activeSpaces: Room[]; isNested?: boolean; isPanelCollapsed?: boolean; onExpand?: Function; parents?: Set; + innerRef?: LegacyRef; } interface IItemState { @@ -300,18 +301,18 @@ export class SpaceItem extends React.PureComponent { } render() { - const {space, activeSpaces, isNested} = this.props; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { space, activeSpaces, isNested, isPanelCollapsed, onExpand, parents, innerRef, + ...otherProps } = this.props; - const forceCollapsed = this.props.isPanelCollapsed; - const isNarrow = this.props.isPanelCollapsed; - const collapsed = this.state.collapsed || forceCollapsed; + const collapsed = this.state.collapsed || isPanelCollapsed; const childSpaces = SpaceStore.instance.getChildSpaces(space.roomId) - .filter(s => !this.props.parents?.has(s.roomId)); + .filter(s => !parents?.has(s.roomId)); const isActive = activeSpaces.includes(space); - const itemClasses = classNames({ + const itemClasses = classNames(this.props.className, { "mx_SpaceItem": true, - "mx_SpaceItem_narrow": isNarrow, + "mx_SpaceItem_narrow": isPanelCollapsed, "collapsed": collapsed, "hasSubSpaces": childSpaces && childSpaces.length, }); @@ -320,7 +321,7 @@ export class SpaceItem extends React.PureComponent { const classes = classNames("mx_SpaceButton", { mx_SpaceButton_active: isActive, mx_SpaceButton_hasMenuOpen: !!this.state.contextMenuPosition, - mx_SpaceButton_narrow: isNarrow, + mx_SpaceButton_narrow: isPanelCollapsed, mx_SpaceButton_invite: isInvite, }); const notificationState = isInvite @@ -333,7 +334,7 @@ export class SpaceItem extends React.PureComponent { spaces={childSpaces} activeSpaces={activeSpaces} isNested={true} - parents={new Set(this.props.parents).add(this.props.space.roomId)} + parents={new Set(parents).add(space.roomId)} />; } @@ -353,7 +354,7 @@ export class SpaceItem extends React.PureComponent { /> : null; let button; - if (isNarrow) { + if (isPanelCollapsed) { button = ( { } return ( -
    10. +
    11. { button } { childItems }
    12. diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index 1333fc5d37..9ef961ce2d 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -63,12 +63,6 @@ const partitionSpacesAndRooms = (arr: Room[]): [Room[], Room[]] => { // [spaces, const SpaceTagOrderingField = "org.matrix.mscXXXX.space"; -const getSpaceTagOrdering = (space: Room): number | undefined => { - return space?.getAccountData(EventType.Tag)?.getContent()?.tags?.[SpaceTagOrderingField]?.order; -}; - -const sortRootSpaces = (spaces: Room[]): Room[] => sortBy(spaces, [getSpaceTagOrdering, "roomId"]); - // For sorting space children using a validated `order`, `m.room.create`'s `origin_server_ts`, `room_id` export const getChildOrder = (order: string, creationTs: number, roomId: string): Array>> => { let validatedOrder: string = null; @@ -104,6 +98,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { private _activeSpace?: Room = null; private _suggestedRooms: ISuggestedRoom[] = []; private _invitedSpaces = new Set(); + private spaceOrderLocalEchoMap = new Map(); public get invitedSpaces(): Room[] { return Array.from(this._invitedSpaces); @@ -335,7 +330,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { // rootSpaces.push(space); // }); - this.rootSpaces = sortRootSpaces(rootSpaces); + this.rootSpaces = this.sortRootSpaces(rootSpaces); this.parentMap = backrefs; // if the currently selected space no longer exists, remove its selection @@ -347,7 +342,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { this.emit(UPDATE_TOP_LEVEL_SPACES, this.spacePanelSpaces); // build initial state of invited spaces as we would have missed the emitted events about the room at launch - this._invitedSpaces = new Set(sortRootSpaces(invitedSpaces)); + this._invitedSpaces = new Set(this.sortRootSpaces(invitedSpaces)); this.emit(UPDATE_INVITED_SPACES, this.invitedSpaces); }, 100, {trailing: true, leading: true}); @@ -484,17 +479,22 @@ export class SpaceStoreClass extends AsyncStoreWithClient { private onRoomAccountData = (ev: MatrixEvent, room: Room, lastEv?: MatrixEvent) => { if (!room.isSpaceRoom() || ev.getType() !== EventType.Tag) return; + this.spaceOrderLocalEchoMap.delete(room.roomId); // clear any local echo const order = ev.getContent()?.tags?.[SpaceTagOrderingField]?.order; const lastOrder = lastEv?.getContent()?.tags?.[SpaceTagOrderingField]?.order; if (order !== lastOrder) { - const rootSpaces = sortRootSpaces(this.rootSpaces); - if (arrayHasOrderChange(this.rootSpaces, rootSpaces)) { - this.rootSpaces = rootSpaces; - this.emit(UPDATE_TOP_LEVEL_SPACES, this.spacePanelSpaces); - } + this.notifyIfOrderChanged(); } }; + private notifyIfOrderChanged(): void { + const rootSpaces = this.sortRootSpaces(this.rootSpaces); + if (arrayHasOrderChange(this.rootSpaces, rootSpaces)) { + this.rootSpaces = rootSpaces; + this.emit(UPDATE_TOP_LEVEL_SPACES, this.spacePanelSpaces); + } + } + private onRoomState = (ev: MatrixEvent) => { const room = this.matrixClient.getRoom(ev.getRoomId()); if (!room) return; @@ -624,6 +624,51 @@ export class SpaceStoreClass extends AsyncStoreWithClient { } childSpaces.forEach(s => this.traverseSpace(s.roomId, fn, includeRooms, newPath)); } + + private getSpaceTagOrdering = (space: Room): number | undefined => { + if (this.spaceOrderLocalEchoMap.has(space.roomId)) return this.spaceOrderLocalEchoMap.get(space.roomId); + return space.tags?.[SpaceTagOrderingField]?.order; + }; + + private sortRootSpaces(spaces: Room[]): Room[] { + return sortBy(spaces, [this.getSpaceTagOrdering, "roomId"]); + } + + public moveRootSpace(fromIndex: number, toIndex: number): void { + if ( + fromIndex < 0 || toIndex < 0 || + fromIndex > this.rootSpaces.length || toIndex > this.rootSpaces.length || + fromIndex === toIndex + ) { + return; + } + const space = this.rootSpaces[fromIndex]; + const orders = this.rootSpaces.map(this.getSpaceTagOrdering); + + let prevOrder = orders[toIndex - 1]; + let nextOrder = orders[toIndex]; // accounts for downwards displacement of existing inhabitant of this index + + if (prevOrder === undefined && nextOrder === undefined) { + // TODO WHAT A PAIN + } + + prevOrder = prevOrder || 0.0; + nextOrder = nextOrder || 1.0; + + if (prevOrder !== nextOrder) { + const order = prevOrder + ((nextOrder - prevOrder) / 2); + this.spaceOrderLocalEchoMap.set(space.roomId, order); + this.matrixClient.setRoomAccountData(space.roomId, EventType.Tag, { + tags: { + ...space.tags, + [SpaceTagOrderingField]: { order }, + }, + }); + this.notifyIfOrderChanged(); + } else { + // TODO REBUILD + } + } } export default class SpaceStore { diff --git a/yarn.lock b/yarn.lock index 2c84237730..7e24c220e5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1024,6 +1024,13 @@ dependencies: regenerator-runtime "^0.13.4" +"@babel/runtime@^7.12.1", "@babel/runtime@^7.9.2": + version "7.14.0" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.14.0.tgz#46794bc20b612c5f75e62dd071e24dfd95f1cbe6" + integrity sha512-JELkvo/DlpNdJ7dlyw/eY7E0suy5i5GQH+Vlxaq1nsNJ+H7f4Vtv3jMeCEgRhZZQFXTjldYfQgv2qmM6M1v5wA== + dependencies: + regenerator-runtime "^0.13.4" + "@babel/template@^7.10.4", "@babel/template@^7.12.7", "@babel/template@^7.3.3": version "7.12.7" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.12.7.tgz#c817233696018e39fbb6c491d2fb684e05ed43bc" @@ -1504,6 +1511,14 @@ dependencies: "@types/node" "*" +"@types/hoist-non-react-statics@^3.3.0": + version "3.3.1" + resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f" + integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA== + dependencies: + "@types/react" "*" + hoist-non-react-statics "^3.3.0" + "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": version "2.0.3" resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz#4ba8ddb720221f432e443bd5f9117fd22cfd4762" @@ -1620,6 +1635,13 @@ dependencies: "@types/node" "*" +"@types/react-beautiful-dnd@^13.0.0": + version "13.0.0" + resolved "https://registry.yarnpkg.com/@types/react-beautiful-dnd/-/react-beautiful-dnd-13.0.0.tgz#e60d3d965312fcf1516894af92dc3e9249587db4" + integrity sha512-by80tJ8aTTDXT256Gl+RfLRtFjYbUWOnZuEigJgNsJrSEGxvFe5eY6k3g4VIvf0M/6+xoLgfYWoWonlOo6Wqdg== + dependencies: + "@types/react" "*" + "@types/react-dom@^16.9.10": version "16.9.10" resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.9.10.tgz#4485b0bec3d41f856181b717f45fd7831101156f" @@ -1627,6 +1649,16 @@ dependencies: "@types/react" "^16" +"@types/react-redux@^7.1.16": + version "7.1.16" + resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.16.tgz#0fbd04c2500c12105494c83d4a3e45c084e3cb21" + integrity sha512-f/FKzIrZwZk7YEO9E1yoxIuDNRiDducxkFlkw/GNMGEnK9n4K8wJzlJBghpSuOVDgEUHoDkDF7Gi9lHNQR4siw== + dependencies: + "@types/hoist-non-react-statics" "^3.3.0" + "@types/react" "*" + hoist-non-react-statics "^3.3.0" + redux "^4.0.0" + "@types/react-transition-group@^4.4.0": version "4.4.0" resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.0.tgz#882839db465df1320e4753e6e9f70ca7e9b4d46d" @@ -2696,6 +2728,13 @@ cross-spawn@^7.0.0, cross-spawn@^7.0.2: shebang-command "^2.0.0" which "^2.0.1" +css-box-model@^1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/css-box-model/-/css-box-model-1.2.1.tgz#59951d3b81fd6b2074a62d49444415b0d2b4d7c1" + integrity sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw== + dependencies: + tiny-invariant "^1.0.6" + css-select@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.1.2.tgz#8b52b6714ed3a80d8221ec971c543f3b12653286" @@ -4202,6 +4241,13 @@ highlight.js@^10.5.0: resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.5.0.tgz#3f09fede6a865757378f2d9ebdcbc15ba268f98f" integrity sha512-xTmvd9HiIHR6L53TMC7TKolEj65zG1XU+Onr8oi86mYa+nLcIbxTTWkpW7CsEwv/vK7u1zb8alZIMLDqqN6KTw== +hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" + integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== + dependencies: + react-is "^16.7.0" + hosted-git-info@^2.1.4: version "2.8.9" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9" @@ -5717,6 +5763,11 @@ mdurl@~1.0.1: resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" integrity sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4= +memoize-one@^5.1.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e" + integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q== + meow@^9.0.0: version "9.0.0" resolved "https://registry.yarnpkg.com/meow/-/meow-9.0.0.tgz#cd9510bc5cac9dee7d03c73ee1f9ad959f4ea364" @@ -6632,6 +6683,11 @@ quick-lru@^4.0.1: resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f" integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g== +raf-schd@^4.0.2: + version "4.0.3" + resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.3.tgz#5d6c34ef46f8b2a0e880a8fcdb743efc5bfdbc1a" + integrity sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ== + raf@^3.4.1: version "3.4.1" resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39" @@ -6659,6 +6715,19 @@ re-resizable@^6.9.0: dependencies: fast-memoize "^2.5.1" +react-beautiful-dnd@^13.1.0: + version "13.1.0" + resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-13.1.0.tgz#ec97c81093593526454b0de69852ae433783844d" + integrity sha512-aGvblPZTJowOWUNiwd6tNfEpgkX5OxmpqxHKNW/4VmvZTNTbeiq7bA3bn5T+QSF2uibXB0D1DmJsb1aC/+3cUA== + dependencies: + "@babel/runtime" "^7.9.2" + css-box-model "^1.2.0" + memoize-one "^5.1.1" + raf-schd "^4.0.2" + react-redux "^7.2.0" + redux "^4.0.4" + use-memo-one "^1.1.1" + 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" @@ -6688,7 +6757,7 @@ react-focus-lock@^2.5.0: use-callback-ref "^1.2.1" use-sidecar "^1.0.1" -react-is@^16.13.1, react-is@^16.8.1, react-is@^16.8.6: +react-is@^16.13.1, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.6: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== @@ -6698,6 +6767,18 @@ react-is@^17.0.1: resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.1.tgz#5b3531bd76a645a4c9fb6e693ed36419e3301339" integrity sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA== +react-redux@^7.2.0: + version "7.2.4" + resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.4.tgz#1ebb474032b72d806de2e0519cd07761e222e225" + integrity sha512-hOQ5eOSkEJEXdpIKbnRyl04LhaWabkDPV+Ix97wqQX3T3d2NQ8DUblNXXtNMavc7DpswyQM6xfaN4HQDKNY2JA== + dependencies: + "@babel/runtime" "^7.12.1" + "@types/react-redux" "^7.1.16" + hoist-non-react-statics "^3.3.2" + loose-envify "^1.4.0" + prop-types "^15.7.2" + react-is "^16.13.1" + react-test-renderer@^16.0.0-0, react-test-renderer@^16.14.0: version "16.14.0" resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.14.0.tgz#e98360087348e260c56d4fe2315e970480c228ae" @@ -6818,6 +6899,13 @@ redent@^3.0.0: indent-string "^4.0.0" strip-indent "^3.0.0" +redux@^4.0.0, redux@^4.0.4: + version "4.1.0" + resolved "https://registry.yarnpkg.com/redux/-/redux-4.1.0.tgz#eb049679f2f523c379f1aff345c8612f294c88d4" + integrity sha512-uI2dQN43zqLWCt6B/BMGRMY6db7TTY4qeHHfGeKb3EOhmOKjU3KdWvNLJyqaHRksv/ErdNH7cFZWg9jXtewy4g== + dependencies: + "@babel/runtime" "^7.9.2" + reflect.ownkeys@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz#749aceec7f3fdf8b63f927a04809e90c5c0b3460" @@ -7765,6 +7853,11 @@ through@^2.3.6: resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= +tiny-invariant@^1.0.6: + version "1.1.0" + resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875" + integrity sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw== + tmatch@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/tmatch/-/tmatch-2.0.1.tgz#0c56246f33f30da1b8d3d72895abaf16660f38cf" @@ -8070,6 +8163,11 @@ use-callback-ref@^1.2.1: resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.2.5.tgz#6115ed242cfbaed5915499c0a9842ca2912f38a5" integrity sha512-gN3vgMISAgacF7sqsLPByqoePooY3n2emTH59Ur5d/M8eg4WTWu1xp8i8DHjohftIyEx0S08RiYxbffr4j8Peg== +use-memo-one@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/use-memo-one/-/use-memo-one-1.1.2.tgz#0c8203a329f76e040047a35a1197defe342fab20" + integrity sha512-u2qFKtxLsia/r8qG0ZKkbytbztzRb317XCkT7yP8wxL0tZ/CzK2G+WWie5vWvpyeP7+YoPIwbJoIHJ4Ba4k0oQ== + use-sidecar@^1.0.1: version "1.0.4" resolved "https://registry.yarnpkg.com/use-sidecar/-/use-sidecar-1.0.4.tgz#38398c3723727f9f924bed2343dfa3db6aaaee46" From 42a3ace82af9fec69948e2816346c79b9c472f7f Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 3 Jun 2021 08:35:12 +0100 Subject: [PATCH 349/999] Iterate PR based on feedback --- src/components/structures/NotificationPanel.tsx | 4 +--- src/components/structures/RightPanel.tsx | 8 ++++---- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/components/structures/NotificationPanel.tsx b/src/components/structures/NotificationPanel.tsx index 7e1566521d..3bfadf7110 100644 --- a/src/components/structures/NotificationPanel.tsx +++ b/src/components/structures/NotificationPanel.tsx @@ -31,7 +31,7 @@ interface IProps { * Component which shows the global notification list using a TimelinePanel */ @replaceableComponent("structures.NotificationPanel") -class NotificationPanel extends React.Component { +export default class NotificationPanel extends React.PureComponent { render() { const emptyState = (

      {_t('You’re all caught up')}

      @@ -62,5 +62,3 @@ class NotificationPanel extends React.Component { ; } } - -export default NotificationPanel; diff --git a/src/components/structures/RightPanel.tsx b/src/components/structures/RightPanel.tsx index e3237d98a6..353f50ad34 100644 --- a/src/components/structures/RightPanel.tsx +++ b/src/components/structures/RightPanel.tsx @@ -295,13 +295,13 @@ export default class RightPanel extends React.Component { break; case RightPanelPhases.NotificationPanel: - if (SettingsStore.getValue("feature_pinning")) { - panel = ; - } + panel = ; break; case RightPanelPhases.PinnedMessages: - panel = ; + if (SettingsStore.getValue("feature_pinning")) { + panel = ; + } break; case RightPanelPhases.FilePanel: From a34f8a29f4a58ce11ff8bbbf160ae292ac752ed0 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Thu, 3 Jun 2021 08:41:12 +0100 Subject: [PATCH 350/999] fix mx_Event containment rules and empty read avatar row --- res/css/structures/_RoomView.scss | 1 - src/components/views/rooms/EventTile.tsx | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/res/css/structures/_RoomView.scss b/res/css/structures/_RoomView.scss index b019fe7e01..09e1d58617 100644 --- a/res/css/structures/_RoomView.scss +++ b/res/css/structures/_RoomView.scss @@ -222,7 +222,6 @@ limitations under the License. .mx_RoomView_MessageList li { clear: both; - contain: content; } li.mx_RoomView_myReadMarker_container { diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 67df5a84ba..7b652cbba2 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -631,7 +631,7 @@ export default class EventTile extends React.Component { // return early if there are no read receipts if (!this.props.readReceipts || this.props.readReceipts.length === 0) { - return (); + return null; } const ReadReceiptMarker = sdk.getComponent('rooms.ReadReceiptMarker'); From 83d223475bb98af6e08587c701de14b525f25de9 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 3 Jun 2021 08:41:22 +0100 Subject: [PATCH 351/999] delint imports --- .../structures/NotificationPanel.tsx | 4 +-- src/components/structures/RightPanel.tsx | 16 ++++----- src/components/structures/RoomView.tsx | 4 +-- .../views/context_menus/MessageContextMenu.js | 10 +++--- .../views/right_panel/GroupHeaderButtons.tsx | 12 +++---- .../views/right_panel/HeaderButton.tsx | 2 +- .../views/right_panel/HeaderButtons.tsx | 8 ++--- .../views/right_panel/RoomHeaderButtons.tsx | 14 ++++---- src/components/views/right_panel/UserInfo.tsx | 36 +++++++++---------- src/components/views/rooms/RoomHeader.js | 8 ++--- src/contexts/RoomContext.ts | 4 +-- src/hooks/useAsyncMemo.ts | 2 +- 12 files changed, 60 insertions(+), 60 deletions(-) diff --git a/src/components/structures/NotificationPanel.tsx b/src/components/structures/NotificationPanel.tsx index 3bfadf7110..b4f13f6b37 100644 --- a/src/components/structures/NotificationPanel.tsx +++ b/src/components/structures/NotificationPanel.tsx @@ -17,9 +17,9 @@ limitations under the License. import React from "react"; import { _t } from '../../languageHandler'; -import {MatrixClientPeg} from "../../MatrixClientPeg"; +import { MatrixClientPeg } from "../../MatrixClientPeg"; import BaseCard from "../views/right_panel/BaseCard"; -import {replaceableComponent} from "../../utils/replaceableComponent"; +import { replaceableComponent } from "../../utils/replaceableComponent"; import TimelinePanel from "./TimelinePanel"; import Spinner from "../views/elements/Spinner"; diff --git a/src/components/structures/RightPanel.tsx b/src/components/structures/RightPanel.tsx index 353f50ad34..294865fe08 100644 --- a/src/components/structures/RightPanel.tsx +++ b/src/components/structures/RightPanel.tsx @@ -16,11 +16,11 @@ limitations under the License. */ import React from 'react'; -import {Room} from "matrix-js-sdk/src/models/room"; -import {User} from "matrix-js-sdk/src/models/user"; -import {RoomMember} from "matrix-js-sdk/src/models/room-member"; -import {MatrixEvent} from "matrix-js-sdk/src/models/event"; -import {VerificationRequest} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; +import { Room } from "matrix-js-sdk/src/models/room"; +import { User } from "matrix-js-sdk/src/models/user"; +import { RoomMember } from "matrix-js-sdk/src/models/room-member"; +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'; @@ -32,12 +32,12 @@ import { } from "../../stores/RightPanelStorePhases"; import RightPanelStore from "../../stores/RightPanelStore"; import MatrixClientContext from "../../contexts/MatrixClientContext"; -import {Action} from "../../dispatcher/actions"; +import { Action } from "../../dispatcher/actions"; import RoomSummaryCard from "../views/right_panel/RoomSummaryCard"; import WidgetCard from "../views/right_panel/WidgetCard"; -import {replaceableComponent} from "../../utils/replaceableComponent"; +import { replaceableComponent } from "../../utils/replaceableComponent"; import SettingsStore from "../../settings/SettingsStore"; -import {ActionPayload} from "../../dispatcher/payloads"; +import { ActionPayload } from "../../dispatcher/payloads"; import MemberList from "../views/rooms/MemberList"; import GroupMemberList from "../views/groups/GroupMemberList"; import GroupRoomList from "../views/groups/GroupRoomList"; diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 482f4a45a7..fdfc5c2ada 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -46,7 +46,7 @@ import RoomViewStore from '../../stores/RoomViewStore'; import RoomScrollStateStore from '../../stores/RoomScrollStateStore'; import WidgetEchoStore from '../../stores/WidgetEchoStore'; import SettingsStore from "../../settings/SettingsStore"; -import {Layout} from "../../settings/Layout"; +import { Layout } from "../../settings/Layout"; import AccessibleButton from "../views/elements/AccessibleButton"; import RightPanelStore from "../../stores/RightPanelStore"; import { haveTileForEvent } from "../views/rooms/EventTile"; @@ -80,7 +80,7 @@ import { getKeyBindingsManager, RoomAction } from '../../KeyBindingsManager'; import { objectHasDiff } from "../../utils/objects"; import SpaceRoomView from "./SpaceRoomView"; import { IOpts } from "../../createRoom"; -import {replaceableComponent} from "../../utils/replaceableComponent"; +import { replaceableComponent } from "../../utils/replaceableComponent"; const DEBUG = false; let debuglog = function(msg: string) {}; diff --git a/src/components/views/context_menus/MessageContextMenu.js b/src/components/views/context_menus/MessageContextMenu.js index d9f21906fe..594b98b1f5 100644 --- a/src/components/views/context_menus/MessageContextMenu.js +++ b/src/components/views/context_menus/MessageContextMenu.js @@ -17,9 +17,9 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; -import {EventStatus} from 'matrix-js-sdk/src/models/event'; +import { EventStatus } from 'matrix-js-sdk/src/models/event'; -import {MatrixClientPeg} from '../../../MatrixClientPeg'; +import { MatrixClientPeg } from '../../../MatrixClientPeg'; import dis from '../../../dispatcher/dispatcher'; import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; @@ -28,9 +28,9 @@ import Resend from '../../../Resend'; import SettingsStore from '../../../settings/SettingsStore'; import { isUrlPermitted } from '../../../HtmlUtils'; import { isContentActionable } from '../../../utils/EventUtils'; -import {MenuItem} from "../../structures/ContextMenu"; -import {EventType} from "matrix-js-sdk/src/@types/event"; -import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { MenuItem } from "../../structures/ContextMenu"; +import { EventType } from "matrix-js-sdk/src/@types/event"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; import { ReadPinsEventId } from "../right_panel/PinnedMessagesCard"; export function canCancel(eventStatus) { diff --git a/src/components/views/right_panel/GroupHeaderButtons.tsx b/src/components/views/right_panel/GroupHeaderButtons.tsx index 5ae5709d44..3c93cf6470 100644 --- a/src/components/views/right_panel/GroupHeaderButtons.tsx +++ b/src/components/views/right_panel/GroupHeaderButtons.tsx @@ -21,12 +21,12 @@ limitations under the License. import React from 'react'; import { _t } from '../../../languageHandler'; import HeaderButton from './HeaderButton'; -import HeaderButtons, {HeaderKind} from './HeaderButtons'; -import {RightPanelPhases} from "../../../stores/RightPanelStorePhases"; -import {Action} from "../../../dispatcher/actions"; -import {ActionPayload} from "../../../dispatcher/payloads"; -import {ViewUserPayload} from "../../../dispatcher/payloads/ViewUserPayload"; -import {replaceableComponent} from "../../../utils/replaceableComponent"; +import HeaderButtons, { HeaderKind } from './HeaderButtons'; +import { RightPanelPhases } from "../../../stores/RightPanelStorePhases"; +import { Action } from "../../../dispatcher/actions"; +import { ActionPayload } from "../../../dispatcher/payloads"; +import { ViewUserPayload } from "../../../dispatcher/payloads/ViewUserPayload"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; const GROUP_PHASES = [ RightPanelPhases.GroupMemberInfo, diff --git a/src/components/views/right_panel/HeaderButton.tsx b/src/components/views/right_panel/HeaderButton.tsx index 8451f69fd3..cdf4f44e06 100644 --- a/src/components/views/right_panel/HeaderButton.tsx +++ b/src/components/views/right_panel/HeaderButton.tsx @@ -22,7 +22,7 @@ import React from 'react'; import classNames from 'classnames'; import Analytics from '../../../Analytics'; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; -import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; interface IProps { // Whether this button is highlighted diff --git a/src/components/views/right_panel/HeaderButtons.tsx b/src/components/views/right_panel/HeaderButtons.tsx index 0c7d9ee299..6d44a081d9 100644 --- a/src/components/views/right_panel/HeaderButtons.tsx +++ b/src/components/views/right_panel/HeaderButtons.tsx @@ -21,14 +21,14 @@ limitations under the License. import React from 'react'; import dis from '../../../dispatcher/dispatcher'; import RightPanelStore from "../../../stores/RightPanelStore"; -import {RightPanelPhases} from "../../../stores/RightPanelStorePhases"; -import {Action} from '../../../dispatcher/actions'; +import { RightPanelPhases } from "../../../stores/RightPanelStorePhases"; +import { Action } from '../../../dispatcher/actions'; import { SetRightPanelPhasePayload, SetRightPanelPhaseRefireParams, } from '../../../dispatcher/payloads/SetRightPanelPhasePayload'; -import {EventSubscription} from "fbemitter"; -import {replaceableComponent} from "../../../utils/replaceableComponent"; +import type { EventSubscription } from "fbemitter"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; export enum HeaderKind { Room = "room", diff --git a/src/components/views/right_panel/RoomHeaderButtons.tsx b/src/components/views/right_panel/RoomHeaderButtons.tsx index 7ff7207fb6..54e18e4529 100644 --- a/src/components/views/right_panel/RoomHeaderButtons.tsx +++ b/src/components/views/right_panel/RoomHeaderButtons.tsx @@ -21,15 +21,15 @@ limitations under the License. import React from "react"; import { Room } from "matrix-js-sdk/src/models/room"; -import {_t} from '../../../languageHandler'; +import { _t } from '../../../languageHandler'; import HeaderButton from './HeaderButton'; -import HeaderButtons, {HeaderKind} from './HeaderButtons'; -import {RightPanelPhases} from "../../../stores/RightPanelStorePhases"; -import {Action} from "../../../dispatcher/actions"; -import {ActionPayload} from "../../../dispatcher/payloads"; +import HeaderButtons, { HeaderKind } from './HeaderButtons'; +import { RightPanelPhases } from "../../../stores/RightPanelStorePhases"; +import { Action } from "../../../dispatcher/actions"; +import { ActionPayload } from "../../../dispatcher/payloads"; import RightPanelStore from "../../../stores/RightPanelStore"; -import {replaceableComponent} from "../../../utils/replaceableComponent"; -import {useSettingValue} from "../../../hooks/useSettings"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { useSettingValue } from "../../../hooks/useSettings"; import { useReadPinnedEvents, usePinnedEvents } from './PinnedMessagesCard'; const ROOM_INFO_PHASES = [ diff --git a/src/components/views/right_panel/UserInfo.tsx b/src/components/views/right_panel/UserInfo.tsx index 5a40b60111..cdbca59db9 100644 --- a/src/components/views/right_panel/UserInfo.tsx +++ b/src/components/views/right_panel/UserInfo.tsx @@ -17,19 +17,19 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, {useCallback, useContext, useEffect, useMemo, useState} from 'react'; +import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react'; import classNames from 'classnames'; -import {MatrixClient} from 'matrix-js-sdk/src/client'; -import {RoomMember} from 'matrix-js-sdk/src/models/room-member'; -import {User} from 'matrix-js-sdk/src/models/user'; -import {Room} from 'matrix-js-sdk/src/models/room'; -import {EventTimeline} from 'matrix-js-sdk/src/models/event-timeline'; -import {MatrixEvent} from 'matrix-js-sdk/src/models/event'; -import {VerificationRequest} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; +import { MatrixClient } from 'matrix-js-sdk/src/client'; +import { RoomMember } from 'matrix-js-sdk/src/models/room-member'; +import { User } from 'matrix-js-sdk/src/models/user'; +import { Room } from 'matrix-js-sdk/src/models/room'; +import { EventTimeline } from 'matrix-js-sdk/src/models/event-timeline'; +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 Modal from '../../../Modal'; -import {_t} from '../../../languageHandler'; +import { _t } from '../../../languageHandler'; import createRoom, { findDMForUser, privateShouldBeEncrypted } from '../../../createRoom'; import DMRoomMap from '../../../utils/DMRoomMap'; import AccessibleButton from '../elements/AccessibleButton'; @@ -40,18 +40,18 @@ import MultiInviter from "../../../utils/MultiInviter"; import GroupStore from "../../../stores/GroupStore"; import {MatrixClientPeg} from "../../../MatrixClientPeg"; import E2EIcon from "../rooms/E2EIcon"; -import {useEventEmitter} from "../../../hooks/useEventEmitter"; -import {textualPowerLevel} from '../../../Roles'; +import { useEventEmitter } from "../../../hooks/useEventEmitter"; +import { textualPowerLevel } from '../../../Roles'; import MatrixClientContext from "../../../contexts/MatrixClientContext"; -import {RightPanelPhases} from "../../../stores/RightPanelStorePhases"; +import { RightPanelPhases } from "../../../stores/RightPanelStorePhases"; import EncryptionPanel from "./EncryptionPanel"; -import {useAsyncMemo} from '../../../hooks/useAsyncMemo'; -import {legacyVerifyUser, verifyDevice, verifyUser} from '../../../verification'; -import {Action} from "../../../dispatcher/actions"; +import { useAsyncMemo } from '../../../hooks/useAsyncMemo'; +import { legacyVerifyUser, verifyDevice, verifyUser } from '../../../verification'; +import { Action } from "../../../dispatcher/actions"; import { USER_SECURITY_TAB } from "../dialogs/UserSettingsDialog"; -import {useIsEncrypted} from "../../../hooks/useIsEncrypted"; +import { useIsEncrypted } from "../../../hooks/useIsEncrypted"; import BaseCard from "./BaseCard"; -import {E2EStatus} from "../../../utils/ShieldUtils"; +import { E2EStatus } from "../../../utils/ShieldUtils"; import ImageView from "../elements/ImageView"; import Spinner from "../elements/Spinner"; import PowerSelector from "../elements/PowerSelector"; @@ -66,7 +66,7 @@ import { EventType } from "matrix-js-sdk/src/@types/event"; import { SetRightPanelPhasePayload } from "../../../dispatcher/payloads/SetRightPanelPhasePayload"; import RoomAvatar from "../avatars/RoomAvatar"; import RoomName from "../elements/RoomName"; -import {mediaFromMxc} from "../../../customisations/Media"; +import { mediaFromMxc } from "../../../customisations/Media"; export interface IDevice { deviceId: string; diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js index 217b3ea5c2..cb6bb0afca 100644 --- a/src/components/views/rooms/RoomHeader.js +++ b/src/components/views/rooms/RoomHeader.js @@ -19,10 +19,10 @@ import React from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import { _t } from '../../../languageHandler'; -import {MatrixClientPeg} from '../../../MatrixClientPeg'; +import { MatrixClientPeg } from '../../../MatrixClientPeg'; import RateLimitedFunc from '../../../ratelimitedfunc'; -import {CancelButton} from './SimpleRoomHeader'; +import { CancelButton } from './SimpleRoomHeader'; import SettingsStore from "../../../settings/SettingsStore"; import RoomHeaderButtons from '../right_panel/RoomHeaderButtons'; import E2EIcon from './E2EIcon'; @@ -30,8 +30,8 @@ import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar"; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import RoomTopic from "../elements/RoomTopic"; import RoomName from "../elements/RoomName"; -import {PlaceCallType} from "../../../CallHandler"; -import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { PlaceCallType } from "../../../CallHandler"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; @replaceableComponent("views.rooms.RoomHeader") export default class RoomHeader extends React.Component { diff --git a/src/contexts/RoomContext.ts b/src/contexts/RoomContext.ts index 7f2c0bb157..e925f8624b 100644 --- a/src/contexts/RoomContext.ts +++ b/src/contexts/RoomContext.ts @@ -16,8 +16,8 @@ limitations under the License. import { createContext } from "react"; -import {IState} from "../components/structures/RoomView"; -import {Layout} from "../settings/Layout"; +import { IState } from "../components/structures/RoomView"; +import { Layout } from "../settings/Layout"; const RoomContext = createContext({ roomLoading: true, diff --git a/src/hooks/useAsyncMemo.ts b/src/hooks/useAsyncMemo.ts index eda44b1ae4..1776ec1d36 100644 --- a/src/hooks/useAsyncMemo.ts +++ b/src/hooks/useAsyncMemo.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {useState, useEffect, DependencyList} from 'react'; +import { useState, useEffect, DependencyList } from 'react'; type Fn = () => Promise; From dbaa394d65c640581f3ef89f52aba13e4801cc3a Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 3 Jun 2021 08:54:30 +0100 Subject: [PATCH 352/999] i18n --- src/i18n/strings/en_EN.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 85647a17e5..2a5297122f 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1018,9 +1018,9 @@ "You can change these anytime.": "You can change these anytime.", "Creating...": "Creating...", "Create": "Create", + "All rooms": "All rooms", "Expand space panel": "Expand space panel", "Collapse space panel": "Collapse space panel", - "All rooms": "All rooms", "Click to copy": "Click to copy", "Copied!": "Copied!", "Failed to copy": "Failed to copy", From 8ef95a62378a8fca563aa094ee897da06e05c438 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 3 Jun 2021 14:38:13 +0100 Subject: [PATCH 353/999] Interface dispatcher payload & use constant in test --- src/components/views/voip/DialPadModal.tsx | 6 +++-- src/dispatcher/actions.ts | 4 +--- src/dispatcher/payloads/DialNumberPayload.ts | 23 ++++++++++++++++++++ test/CallHandler-test.ts | 4 +++- 4 files changed, 31 insertions(+), 6 deletions(-) create mode 100644 src/dispatcher/payloads/DialNumberPayload.ts diff --git a/src/components/views/voip/DialPadModal.tsx b/src/components/views/voip/DialPadModal.tsx index f3ee49e4ac..8c0af5e81a 100644 --- a/src/components/views/voip/DialPadModal.tsx +++ b/src/components/views/voip/DialPadModal.tsx @@ -21,6 +21,7 @@ import Field from "../elements/Field"; import DialPad from './DialPad'; import dis from '../../../dispatcher/dispatcher'; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { DialNumberPayload } from "../../../dispatcher/payloads/DialNumberPayload"; import { Action } from "../../../dispatcher/actions"; interface IProps { @@ -63,10 +64,11 @@ export default class DialpadModal extends React.PureComponent { } onDialPress = async () => { - dis.dispatch({ + const payload: DialNumberPayload = { action: Action.DialNumber, number: this.state.value, - }) + }; + dis.dispatch(payload); this.props.onFinished(true); } diff --git a/src/dispatcher/actions.ts b/src/dispatcher/actions.ts index 073e82bef6..300eed2b98 100644 --- a/src/dispatcher/actions.ts +++ b/src/dispatcher/actions.ts @@ -102,9 +102,7 @@ export enum Action { /** * Dial the phone number in the payload - * payload: { - * number: , - * } + * payload: DialNumberPayload */ DialNumber = "dial_number", diff --git a/src/dispatcher/payloads/DialNumberPayload.ts b/src/dispatcher/payloads/DialNumberPayload.ts new file mode 100644 index 0000000000..1b591b9f6b --- /dev/null +++ b/src/dispatcher/payloads/DialNumberPayload.ts @@ -0,0 +1,23 @@ +/* +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 { ActionPayload } from "../payloads"; +import { Action } from "../actions"; + +export interface DialNumberPayload extends ActionPayload { + action: Action.DialNumber; + number: string; +} diff --git a/test/CallHandler-test.ts b/test/CallHandler-test.ts index a93a134133..12316ac01c 100644 --- a/test/CallHandler-test.ts +++ b/test/CallHandler-test.ts @@ -25,6 +25,8 @@ import DMRoomMap from '../src/utils/DMRoomMap'; import EventEmitter from 'events'; import SdkConfig from '../src/SdkConfig'; import { ActionPayload } from '../src/dispatcher/payloads'; +import { Actions } from '../src/notifications/types'; +import { Action } from '../src/dispatcher/actions'; const REAL_ROOM_ID = '$room1:example.org'; const MAPPED_ROOM_ID = '$room2:example.org'; @@ -172,7 +174,7 @@ describe('CallHandler', () => { }]); dis.dispatch({ - action: 'dial_number', + action: Action.DialNumber, number: '01818118181', }, true); From 5ef78f43a42005ed09f3f1bc02eb55170d8c0487 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Thu, 3 Jun 2021 16:26:20 +0100 Subject: [PATCH 354/999] fix containment rule to keep height when resizing vertically --- res/css/views/rooms/_RoomTile.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/views/rooms/_RoomTile.scss b/res/css/views/rooms/_RoomTile.scss index 421fbfe39b..03146e0325 100644 --- a/res/css/views/rooms/_RoomTile.scss +++ b/res/css/views/rooms/_RoomTile.scss @@ -19,7 +19,7 @@ limitations under the License. margin-bottom: 4px; padding: 4px; - contain: strict; + contain: content; // Not strict as it will break when resizing a sublist vertically height: 40px; box-sizing: border-box; From 0c97d90fb98f08204c1d7853e0c0f6c30820e5a3 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 3 Jun 2021 16:44:28 +0100 Subject: [PATCH 355/999] Iterate PR based on feedback --- res/css/views/dialogs/_InviteDialog.scss | 6 +++++- src/components/views/dialogs/InviteDialog.tsx | 16 +++++++++++----- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/res/css/views/dialogs/_InviteDialog.scss b/res/css/views/dialogs/_InviteDialog.scss index 8c0421b989..2e48b5d8e9 100644 --- a/res/css/views/dialogs/_InviteDialog.scss +++ b/res/css/views/dialogs/_InviteDialog.scss @@ -302,7 +302,11 @@ limitations under the License. margin-top: 4px; overflow-y: auto; padding: 0 45px 4px 0; - height: calc(100% - 175px); // mx_InviteDialog's height minus some for the upper and lower elements + height: calc(100% - 115px); // mx_InviteDialog's height minus some for the upper and lower elements +} + +.mx_InviteDialog_hasFooter .mx_InviteDialog_userSections { + height: calc(100% - 175px); } .mx_InviteDialog_helpText { diff --git a/src/components/views/dialogs/InviteDialog.tsx b/src/components/views/dialogs/InviteDialog.tsx index 5cbcb12c4f..557ea416a8 100644 --- a/src/components/views/dialogs/InviteDialog.tsx +++ b/src/components/views/dialogs/InviteDialog.tsx @@ -14,7 +14,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, {createRef} from 'react'; +import React, { createRef } from 'react'; +import classNames from 'classnames'; + import {_t, _td} from "../../../languageHandler"; import * as sdk from "../../../index"; import {MatrixClientPeg} from "../../../MatrixClientPeg"; @@ -1252,7 +1254,7 @@ export default class InviteDialog extends React.PureComponent { e.preventDefault(); const target = e.target; // copy target before we go async and React throws it away @@ -1264,7 +1266,7 @@ export default class InviteDialog extends React.PureComponent + > +
      +
      } else if (this.props.kind === KIND_INVITE) { @@ -1437,7 +1441,9 @@ export default class InviteDialog extends React.PureComponent Date: Thu, 3 Jun 2021 19:37:26 +0100 Subject: [PATCH 356/999] not sure how I butchered this merge conflict resolution this much. --- src/components/structures/RoomView.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 4390d21783..5ffc2fd0da 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -79,8 +79,8 @@ import { Container, WidgetLayoutStore } from "../../stores/widgets/WidgetLayoutS import { getKeyBindingsManager, RoomAction } from '../../KeyBindingsManager'; import { objectHasDiff } from "../../utils/objects"; import SpaceRoomView from "./SpaceRoomView"; -import { IOpts } from "../../createRoom -import { replaceableComponent } from "../../utils/replaceableComponent +import { IOpts } from "../../createRoom"; +import { replaceableComponent } from "../../utils/replaceableComponent"; import { omit } from 'lodash'; import UIStore from "../../stores/UIStore"; From 48bd6583ed3e1431a995074dcb50ca47146f61a0 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 4 Jun 2021 11:34:54 +0100 Subject: [PATCH 357/999] Fix unpinning of pinned messages --- src/components/views/right_panel/PinnedMessagesCard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/right_panel/PinnedMessagesCard.tsx b/src/components/views/right_panel/PinnedMessagesCard.tsx index a3f1f2d9df..55809ee02b 100644 --- a/src/components/views/right_panel/PinnedMessagesCard.tsx +++ b/src/components/views/right_panel/PinnedMessagesCard.tsx @@ -155,7 +155,7 @@ const PinnedMessagesCard = ({ room, onClose }: IProps) => { // show them in reverse, with latest pinned at the top content = pinnedEvents.filter(Boolean).reverse().map(ev => ( - + onUnpinClicked(ev)} /> )); } else { content =
      From 93f41ce0ab484172a76f0e07e4036eace2fe9bce Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 4 Jun 2021 11:35:17 +0100 Subject: [PATCH 358/999] Actually finish the empty state for the pinned messages card --- .../right_panel/_PinnedMessagesCard.scss | 55 +++++++++++++++++++ .../views/right_panel/PinnedMessagesCard.tsx | 17 +++++- src/i18n/strings/en_EN.json | 6 +- 3 files changed, 73 insertions(+), 5 deletions(-) diff --git a/res/css/views/right_panel/_PinnedMessagesCard.scss b/res/css/views/right_panel/_PinnedMessagesCard.scss index b6b8238bed..785aee09ca 100644 --- a/res/css/views/right_panel/_PinnedMessagesCard.scss +++ b/res/css/views/right_panel/_PinnedMessagesCard.scss @@ -32,4 +32,59 @@ limitations under the License. margin-right: 6px; } } + + .mx_PinnedMessagesCard_empty { + display: flex; + height: 100%; + + > div { + height: max-content; + text-align: center; + margin: auto 40px; + + .mx_PinnedMessagesCard_MessageActionBar { + pointer-events: none; + display: flex; + height: 32px; + line-height: $font-24px; + border-radius: 8px; + background: $primary-bg-color; + border: 1px solid $input-border-color; + padding: 1px; + width: max-content; + margin: 0 auto; + box-sizing: border-box; + + .mx_MessageActionBar_maskButton { + display: inline-block; + position: relative; + } + + .mx_MessageActionBar_optionsButton { + background: $roomlist-button-bg-color; + border-radius: 6px; + z-index: 1; + + &::after { + background-color: $primary-fg-color; + } + } + } + + > h2 { + font-weight: $font-semi-bold; + font-size: $font-15px; + line-height: $font-24px; + color: $primary-fg-color; + margin-top: 24px; + margin-bottom: 20px; + } + + > span { + font-size: $font-12px; + line-height: $font-15px; + color: $secondary-fg-color; + } + } + } } diff --git a/src/components/views/right_panel/PinnedMessagesCard.tsx b/src/components/views/right_panel/PinnedMessagesCard.tsx index 55809ee02b..3a4dc9cc92 100644 --- a/src/components/views/right_panel/PinnedMessagesCard.tsx +++ b/src/components/views/right_panel/PinnedMessagesCard.tsx @@ -158,9 +158,20 @@ const PinnedMessagesCard = ({ room, onClose }: IProps) => { onUnpinClicked(ev)} /> )); } else { - content =
      -

      {_t("You’re all caught up")}

      -

      {_t("You have no visible notifications.")}

      + content =
      +
      +
      +
      +
      +
      +
      + +

      { _t("Nothing pinned, yet") }

      + { _t("If you have permissions, open the menu on any message and select " + + "Pin to stick them here.", {}, { + b: sub => { sub }, + }) } +
      ; } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 9e85ea28c8..302c8709fa 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1717,8 +1717,8 @@ "The homeserver the user you’re verifying is connected to": "The homeserver the user you’re verifying is connected to", "Yours, or the other users’ internet connection": "Yours, or the other users’ internet connection", "Yours, or the other users’ session": "Yours, or the other users’ session", - "You’re all caught up": "You’re all caught up", - "You have no visible notifications.": "You have no visible notifications.", + "Nothing pinned, yet": "Nothing pinned, yet", + "If you have permissions, open the menu on any message and select Pin to stick them here.": "If you have permissions, open the menu on any message and select Pin to stick them here.", "Pinned messages": "Pinned messages", "Room Info": "Room Info", "You can only pin up to %(count)s widgets|other": "You can only pin up to %(count)s widgets", @@ -2628,6 +2628,8 @@ "Create a new community": "Create a new community", "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.": "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.", "Communities are changing to Spaces": "Communities are changing to Spaces", + "You’re all caught up": "You’re all caught up", + "You have no visible notifications.": "You have no visible notifications.", "%(brand)s failed to get the protocol list from the homeserver. The homeserver may be too old to support third party networks.": "%(brand)s failed to get the protocol list from the homeserver. The homeserver may be too old to support third party networks.", "%(brand)s failed to get the public room list.": "%(brand)s failed to get the public room list.", "The homeserver may be unavailable or overloaded.": "The homeserver may be unavailable or overloaded.", From ab96d5f8af9d4b5afa01f355d4ec7e68d41239bb Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 4 Jun 2021 11:54:58 +0100 Subject: [PATCH 359/999] Repair event status position in timeline https://github.com/matrix-org/matrix-react-sdk/pull/6079 caused a regression in the event status indicator. The `mx_EventTile_msgOption` container was folded into the avatars code path, but the event status is a special case of this, so it now needs to also have this container to preserve its positioning. Fixes https://github.com/vector-im/element-web/issues/17552 --- src/components/views/rooms/EventTile.tsx | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index d3057fed62..8cec067c39 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -1340,11 +1340,15 @@ class SentReceipt extends React.PureComponent; } - return - - {nonCssBadge} - {tooltip} - - ; + return ( +
      + + + {nonCssBadge} + {tooltip} + + +
      + ); } } From 31604c13c04f1a0738038725718889ca9d47ca0a Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 4 Jun 2021 16:52:50 +0100 Subject: [PATCH 360/999] Lint the typescript tests Turns out we hadn't told eslint to lint .ts in tests/ Also fix all the lint errors, including removing a use of assert that had randomly crept in. --- .eslintrc.js | 2 +- test/CallHandler-test.ts | 1 - test/KeyBindingsManager-test.ts | 98 ++++++++++++++++----------------- test/stores/SpaceStore-test.ts | 2 +- 4 files changed, 50 insertions(+), 53 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 9ae51f9bc5..c759eae9e3 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -18,7 +18,7 @@ module.exports = { }, overrides: [{ - "files": ["src/**/*.{ts,tsx}"], + "files": ["src/**/*.{ts,tsx}", "test/**/*.{ts,tsx}"], "extends": ["matrix-org/ts"], "rules": { // We're okay being explicit at the moment diff --git a/test/CallHandler-test.ts b/test/CallHandler-test.ts index 12316ac01c..4c3dd25fb4 100644 --- a/test/CallHandler-test.ts +++ b/test/CallHandler-test.ts @@ -25,7 +25,6 @@ import DMRoomMap from '../src/utils/DMRoomMap'; import EventEmitter from 'events'; import SdkConfig from '../src/SdkConfig'; import { ActionPayload } from '../src/dispatcher/payloads'; -import { Actions } from '../src/notifications/types'; import { Action } from '../src/dispatcher/actions'; const REAL_ROOM_ID = '$room1:example.org'; diff --git a/test/KeyBindingsManager-test.ts b/test/KeyBindingsManager-test.ts index 41614b61fa..694efac7b5 100644 --- a/test/KeyBindingsManager-test.ts +++ b/test/KeyBindingsManager-test.ts @@ -15,7 +15,6 @@ limitations under the License. */ import { isKeyComboMatch, KeyCombo } from '../src/KeyBindingsManager'; -const assert = require('assert'); function mockKeyEvent(key: string, modifiers?: { ctrlKey?: boolean, @@ -28,7 +27,7 @@ function mockKeyEvent(key: string, modifiers?: { ctrlKey: modifiers?.ctrlKey ?? false, altKey: modifiers?.altKey ?? false, shiftKey: modifiers?.shiftKey ?? false, - metaKey: modifiers?.metaKey ?? false + metaKey: modifiers?.metaKey ?? false, } as KeyboardEvent; } @@ -37,9 +36,8 @@ describe('KeyBindingsManager', () => { const combo1: KeyCombo = { key: 'k', }; - assert.strictEqual(isKeyComboMatch(mockKeyEvent('k'), combo1, false), true); - assert.strictEqual(isKeyComboMatch(mockKeyEvent('n'), combo1, false), false); - + expect(isKeyComboMatch(mockKeyEvent('k'), combo1, false)).toBe(true); + expect(isKeyComboMatch(mockKeyEvent('n'), combo1, false)).toBe(false); }); it('should match key + modifier key combo', () => { @@ -47,38 +45,38 @@ describe('KeyBindingsManager', () => { key: 'k', ctrlKey: true, }; - assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true }), combo, false), true); - assert.strictEqual(isKeyComboMatch(mockKeyEvent('n', { ctrlKey: true }), combo, false), false); - assert.strictEqual(isKeyComboMatch(mockKeyEvent('k'), combo, false), false); - assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { shiftKey: true }), combo, false), false); - assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { shiftKey: true, metaKey: true }), combo, false), false); + expect(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true }), combo, false)).toBe(true); + expect(isKeyComboMatch(mockKeyEvent('n', { ctrlKey: true }), combo, false)).toBe(false); + expect(isKeyComboMatch(mockKeyEvent('k'), combo, false)).toBe(false); + expect(isKeyComboMatch(mockKeyEvent('k', { shiftKey: true }), combo, false)).toBe(false); + expect(isKeyComboMatch(mockKeyEvent('k', { shiftKey: true, metaKey: true }), combo, false)).toBe(false); const combo2: KeyCombo = { key: 'k', metaKey: true, }; - assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { metaKey: true }), combo2, false), true); - assert.strictEqual(isKeyComboMatch(mockKeyEvent('n', { metaKey: true }), combo2, false), false); - assert.strictEqual(isKeyComboMatch(mockKeyEvent('k'), combo2, false), false); - assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { altKey: true, metaKey: true }), combo2, false), false); + expect(isKeyComboMatch(mockKeyEvent('k', { metaKey: true }), combo2, false)).toBe(true); + expect(isKeyComboMatch(mockKeyEvent('n', { metaKey: true }), combo2, false)).toBe(false); + expect(isKeyComboMatch(mockKeyEvent('k'), combo2, false)).toBe(false); + expect(isKeyComboMatch(mockKeyEvent('k', { altKey: true, metaKey: true }), combo2, false)).toBe(false); const combo3: KeyCombo = { key: 'k', altKey: true, }; - assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { altKey: true }), combo3, false), true); - assert.strictEqual(isKeyComboMatch(mockKeyEvent('n', { altKey: true }), combo3, false), false); - assert.strictEqual(isKeyComboMatch(mockKeyEvent('k'), combo3, false), false); - assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, metaKey: true }), combo3, false), false); + expect(isKeyComboMatch(mockKeyEvent('k', { altKey: true }), combo3, false)).toBe(true); + expect(isKeyComboMatch(mockKeyEvent('n', { altKey: true }), combo3, false)).toBe(false); + expect(isKeyComboMatch(mockKeyEvent('k'), combo3, false)).toBe(false); + expect(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, metaKey: true }), combo3, false)).toBe(false); const combo4: KeyCombo = { key: 'k', shiftKey: true, }; - assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { shiftKey: true }), combo4, false), true); - assert.strictEqual(isKeyComboMatch(mockKeyEvent('n', { shiftKey: true }), combo4, false), false); - assert.strictEqual(isKeyComboMatch(mockKeyEvent('k'), combo4, false), false); - assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { shiftKey: true, ctrlKey: true }), combo4, false), false); + expect(isKeyComboMatch(mockKeyEvent('k', { shiftKey: true }), combo4, false)).toBe(true); + expect(isKeyComboMatch(mockKeyEvent('n', { shiftKey: true }), combo4, false)).toBe(false); + expect(isKeyComboMatch(mockKeyEvent('k'), combo4, false)).toBe(false); + expect(isKeyComboMatch(mockKeyEvent('k', { shiftKey: true, ctrlKey: true }), combo4, false)).toBe(false); }); it('should match key + multiple modifiers key combo', () => { @@ -87,11 +85,11 @@ describe('KeyBindingsManager', () => { ctrlKey: true, altKey: true, }; - assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, altKey: true }), combo, false), true); - assert.strictEqual(isKeyComboMatch(mockKeyEvent('n', { ctrlKey: true, altKey: true }), combo, false), false); - assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, metaKey: true }), combo, false), false); - assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, metaKey: true, shiftKey: true }), combo, - false), false); + expect(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, altKey: true }), combo, false)).toBe(true); + expect(isKeyComboMatch(mockKeyEvent('n', { ctrlKey: true, altKey: true }), combo, false)).toBe(false); + expect(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, metaKey: true }), combo, false)).toBe(false); + expect(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, metaKey: true, shiftKey: true }), combo, + false)).toBe(false); const combo2: KeyCombo = { key: 'k', @@ -99,13 +97,13 @@ describe('KeyBindingsManager', () => { shiftKey: true, altKey: true, }; - assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, shiftKey: true, altKey: true }), combo2, - false), true); - assert.strictEqual(isKeyComboMatch(mockKeyEvent('n', { ctrlKey: true, shiftKey: true, altKey: true }), combo2, - false), false); - assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, metaKey: true }), combo2, false), false); - assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', - { ctrlKey: true, shiftKey: true, altKey: true, metaKey: true }), combo2, false), false); + expect(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, shiftKey: true, altKey: true }), combo2, + false)).toBe(true); + expect(isKeyComboMatch(mockKeyEvent('n', { ctrlKey: true, shiftKey: true, altKey: true }), combo2, + false)).toBe(false); + expect(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, metaKey: true }), combo2, false)).toBe(false); + expect(isKeyComboMatch(mockKeyEvent('k', + { ctrlKey: true, shiftKey: true, altKey: true, metaKey: true }), combo2, false)).toBe(false); const combo3: KeyCombo = { key: 'k', @@ -114,12 +112,12 @@ describe('KeyBindingsManager', () => { altKey: true, metaKey: true, }; - assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', - { ctrlKey: true, shiftKey: true, altKey: true, metaKey: true }), combo3, false), true); - assert.strictEqual(isKeyComboMatch(mockKeyEvent('n', - { ctrlKey: true, shiftKey: true, altKey: true, metaKey: true }), combo3, false), false); - assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', - { ctrlKey: true, shiftKey: true, altKey: true }), combo3, false), false); + expect(isKeyComboMatch(mockKeyEvent('k', + { ctrlKey: true, shiftKey: true, altKey: true, metaKey: true }), combo3, false)).toBe(true); + expect(isKeyComboMatch(mockKeyEvent('n', + { ctrlKey: true, shiftKey: true, altKey: true, metaKey: true }), combo3, false)).toBe(false); + expect(isKeyComboMatch(mockKeyEvent('k', + { ctrlKey: true, shiftKey: true, altKey: true }), combo3, false)).toBe(false); }); it('should match ctrlOrMeta key combo', () => { @@ -128,13 +126,13 @@ describe('KeyBindingsManager', () => { ctrlOrCmd: true, }; // PC: - assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true }), combo, false), true); - assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { metaKey: true }), combo, false), false); - assert.strictEqual(isKeyComboMatch(mockKeyEvent('n', { ctrlKey: true }), combo, false), false); + expect(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true }), combo, false)).toBe(true); + expect(isKeyComboMatch(mockKeyEvent('k', { metaKey: true }), combo, false)).toBe(false); + expect(isKeyComboMatch(mockKeyEvent('n', { ctrlKey: true }), combo, false)).toBe(false); // MAC: - assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { metaKey: true }), combo, true), true); - assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true }), combo, true), false); - assert.strictEqual(isKeyComboMatch(mockKeyEvent('n', { ctrlKey: true }), combo, true), false); + expect(isKeyComboMatch(mockKeyEvent('k', { metaKey: true }), combo, true)).toBe(true); + expect(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true }), combo, true)).toBe(false); + expect(isKeyComboMatch(mockKeyEvent('n', { ctrlKey: true }), combo, true)).toBe(false); }); it('should match advanced ctrlOrMeta key combo', () => { @@ -144,10 +142,10 @@ describe('KeyBindingsManager', () => { altKey: true, }; // PC: - assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, altKey: true }), combo, false), true); - assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { metaKey: true, altKey: true }), combo, false), false); + expect(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, altKey: true }), combo, false)).toBe(true); + expect(isKeyComboMatch(mockKeyEvent('k', { metaKey: true, altKey: true }), combo, false)).toBe(false); // MAC: - assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { metaKey: true, altKey: true }), combo, true), true); - assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, altKey: true }), combo, true), false); + expect(isKeyComboMatch(mockKeyEvent('k', { metaKey: true, altKey: true }), combo, true)).toBe(true); + expect(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, altKey: true }), combo, true)).toBe(false); }); }); diff --git a/test/stores/SpaceStore-test.ts b/test/stores/SpaceStore-test.ts index 20c48c29db..01bd528b87 100644 --- a/test/stores/SpaceStore-test.ts +++ b/test/stores/SpaceStore-test.ts @@ -21,7 +21,7 @@ import "../skinned-sdk"; // Must be first for skinning to work import SpaceStore, { UPDATE_INVITED_SPACES, UPDATE_SELECTED_SPACE, - UPDATE_TOP_LEVEL_SPACES + UPDATE_TOP_LEVEL_SPACES, } from "../../src/stores/SpaceStore"; import { resetAsyncStoreWithClient, setupAsyncStoreWithClient } from "../utils/test-utils"; import { mkEvent, mkStubRoom, stubClient } from "../test-utils"; From 40d2f8228f4d2a3f7f386d320c42ae1970932ee4 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Sat, 5 Jun 2021 01:16:52 -0400 Subject: [PATCH 361/999] Fix watching settings An accidental variable shadowing was preventing setting watcher callbacks from being fired. Signed-off-by: Robin Townsend --- src/settings/WatchManager.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/settings/WatchManager.ts b/src/settings/WatchManager.ts index 56f911f180..744d75b136 100644 --- a/src/settings/WatchManager.ts +++ b/src/settings/WatchManager.ts @@ -63,8 +63,7 @@ export class WatchManager { if (!inRoomId) { // Fire updates to all the individual room watchers too, as they probably care about the change higher up. - const callbacks = Array.from(roomWatchers.values()).flat(1); - callbacks.push(...callbacks); + callbacks.push(...Array.from(roomWatchers.values()).flat(1)); } else if (roomWatchers.has(IRRELEVANT_ROOM)) { callbacks.push(...roomWatchers.get(IRRELEVANT_ROOM)); } From 48d3e41351bd869c232176d956d8254d17f5dc77 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Sat, 5 Jun 2021 01:23:51 -0400 Subject: [PATCH 362/999] Cache frequently used settings values in RoomContext Signed-off-by: Robin Townsend --- src/components/structures/RoomView.tsx | 64 +++++++++++++++++++++----- src/contexts/RoomContext.ts | 6 ++- 2 files changed, 57 insertions(+), 13 deletions(-) diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 5ffc2fd0da..fa67013ccb 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -155,7 +155,6 @@ export interface IState { canPeek: boolean; showApps: boolean; isPeeking: boolean; - showReadReceipts: boolean; showRightPanel: boolean; // error object, as from the matrix client/server API // If we failed to load information about the room, @@ -183,6 +182,11 @@ export interface IState { canReact: boolean; canReply: boolean; layout: Layout; + showReadReceipts: boolean; + showRedactions: boolean; + showJoinLeaves: boolean; + showAvatarChanges: boolean; + showDisplaynameChanges: boolean; matrixClientIsReady: boolean; showUrlPreview?: boolean; e2eStatus?: E2EStatus; @@ -200,8 +204,7 @@ export default class RoomView extends React.Component { private readonly dispatcherRef: string; private readonly roomStoreToken: EventSubscription; private readonly rightPanelStoreToken: EventSubscription; - private readonly showReadReceiptsWatchRef: string; - private readonly layoutWatcherRef: string; + private settingWatchers: string[]; private unmounted = false; private permalinkCreators: Record = {}; @@ -232,7 +235,6 @@ export default class RoomView extends React.Component { canPeek: false, showApps: false, isPeeking: false, - showReadReceipts: true, showRightPanel: RightPanelStore.getSharedInstance().isOpenForRoom, joining: false, atEndOfLiveTimeline: true, @@ -242,6 +244,11 @@ export default class RoomView extends React.Component { canReact: false, canReply: false, layout: SettingsStore.getValue("layout"), + showReadReceipts: true, + showRedactions: true, + showJoinLeaves: true, + showAvatarChanges: true, + showDisplaynameChanges: true, matrixClientIsReady: this.context && this.context.isInitialSyncComplete(), dragCounter: 0, }; @@ -268,9 +275,11 @@ export default class RoomView extends React.Component { WidgetEchoStore.on(UPDATE_EVENT, this.onWidgetEchoStoreUpdate); WidgetStore.instance.on(UPDATE_EVENT, this.onWidgetStoreUpdate); - this.showReadReceiptsWatchRef = SettingsStore.watchSetting("showReadReceipts", null, - this.onReadReceiptsChange); - this.layoutWatcherRef = SettingsStore.watchSetting("layout", null, this.onLayoutChange); + this.settingWatchers = [ + SettingsStore.watchSetting("layout", null, () => + this.setState({ layout: SettingsStore.getValue("layout") }), + ), + ]; } private onWidgetStoreUpdate = () => { @@ -327,9 +336,42 @@ export default class RoomView extends React.Component { // we should only peek once we have a ready client shouldPeek: this.state.matrixClientIsReady && RoomViewStore.shouldPeek(), showReadReceipts: SettingsStore.getValue("showReadReceipts", roomId), + showRedactions: SettingsStore.getValue("showRedactions", roomId), + showJoinLeaves: SettingsStore.getValue("showJoinLeaves", roomId), + showAvatarChanges: SettingsStore.getValue("showAvatarChanges", roomId), + showDisplaynameChanges: SettingsStore.getValue("showDisplaynameChanges", roomId), wasContextSwitch: RoomViewStore.getWasContextSwitch(), }; + // Add watchers for each of the settings we just looked up + this.settingWatchers = this.settingWatchers.concat([ + SettingsStore.watchSetting("showReadReceipts", null, () => + this.setState({ + showReadReceipts: SettingsStore.getValue("showReadReceipts", roomId), + }), + ), + SettingsStore.watchSetting("showRedactions", null, () => + this.setState({ + showRedactions: SettingsStore.getValue("showRedactions", roomId), + }), + ), + SettingsStore.watchSetting("showJoinLeaves", null, () => + this.setState({ + showJoinLeaves: SettingsStore.getValue("showJoinLeaves", roomId), + }), + ), + SettingsStore.watchSetting("showAvatarChanges", null, () => + this.setState({ + showAvatarChanges: SettingsStore.getValue("showAvatarChanges", roomId), + }), + ), + SettingsStore.watchSetting("showDisplaynameChanges", null, () => + this.setState({ + showDisplaynameChanges: SettingsStore.getValue("showDisplaynameChanges", roomId), + }), + ), + ]); + if (!initial && this.state.shouldPeek && !newState.shouldPeek) { // Stop peeking because we have joined this room now this.context.stopPeeking(); @@ -638,10 +680,6 @@ export default class RoomView extends React.Component { ); } - if (this.showReadReceiptsWatchRef) { - SettingsStore.unwatchSetting(this.showReadReceiptsWatchRef); - } - // cancel any pending calls to the rate_limited_funcs this.updateRoomMembers.cancelPendingCall(); @@ -649,7 +687,9 @@ export default class RoomView extends React.Component { // console.log("Tinter.tint from RoomView.unmount"); // Tinter.tint(); // reset colourscheme - SettingsStore.unwatchSetting(this.layoutWatcherRef); + for (const watcher of this.settingWatchers) { + SettingsStore.unwatchSetting(watcher); + } } private onUserScroll = () => { diff --git a/src/contexts/RoomContext.ts b/src/contexts/RoomContext.ts index e925f8624b..1efa1c03e7 100644 --- a/src/contexts/RoomContext.ts +++ b/src/contexts/RoomContext.ts @@ -31,7 +31,6 @@ const RoomContext = createContext({ canPeek: false, showApps: false, isPeeking: false, - showReadReceipts: true, showRightPanel: true, joining: false, atEndOfLiveTimeline: true, @@ -41,6 +40,11 @@ const RoomContext = createContext({ canReact: false, canReply: false, layout: Layout.Group, + showReadReceipts: true, + showRedactions: true, + showJoinLeaves: true, + showAvatarChanges: true, + showDisplaynameChanges: true, matrixClientIsReady: false, dragCounter: 0, }); From 13196b8146919f8ef781904d02c1edfd5f376dfe Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Sat, 5 Jun 2021 01:25:01 -0400 Subject: [PATCH 363/999] Prefer cached settings values in shouldHideEvent Signed-off-by: Robin Townsend --- src/shouldHideEvent.ts | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/shouldHideEvent.ts b/src/shouldHideEvent.ts index 2a47b9c417..31d610b28b 100644 --- a/src/shouldHideEvent.ts +++ b/src/shouldHideEvent.ts @@ -17,6 +17,7 @@ import {MatrixEvent} from "matrix-js-sdk/src/models/event"; import SettingsStore from "./settings/SettingsStore"; +import {IState} from "./components/structures/RoomView"; interface IDiff { isMemberEvent: boolean; @@ -47,11 +48,18 @@ function memberEventDiff(ev: MatrixEvent): IDiff { return diff; } -export default function shouldHideEvent(ev: MatrixEvent): boolean { - // Wrap getValue() for readability. Calling the SettingsStore can be - // fairly resource heavy, so the checks below should avoid hitting it - // where possible. - const isEnabled = (name) => SettingsStore.getValue(name, ev.getRoomId()); +/** + * Determines whether the given event should be hidden from timelines. + * @param ev The event + * @param ctx An optional RoomContext to pull cached settings values from to avoid + * hitting the settings store + */ +export default function shouldHideEvent(ev: MatrixEvent, ctx?: IState): boolean { + // Accessing the settings store directly can be expensive if done frequently, + // so we should prefer using cached values if a RoomContext is available + const isEnabled = ctx ? + name => ctx[name] : + name => SettingsStore.getValue(name, ev.getRoomId()); // Hide redacted events if (ev.isRedacted() && !isEnabled('showRedactions')) return true; From 3bf8e54d7f37477868c1fa229449749bd02f2894 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Sat, 5 Jun 2021 01:25:43 -0400 Subject: [PATCH 364/999] Use cached RoomContext settings values throughout rooms Signed-off-by: Robin Townsend --- src/components/structures/MessagePanel.js | 5 ++++- src/components/structures/RoomView.tsx | 2 +- src/components/structures/TimelinePanel.js | 5 ++++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 6709fef814..bca62e7b2f 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -26,6 +26,7 @@ import * as sdk from '../../index'; import {MatrixClientPeg} from '../../MatrixClientPeg'; import SettingsStore from '../../settings/SettingsStore'; +import RoomContext from "../../contexts/RoomContext"; import {Layout, LayoutPropType} from "../../settings/Layout"; import {_t} from "../../languageHandler"; import {haveTileForEvent} from "../views/rooms/EventTile"; @@ -152,6 +153,8 @@ export default class MessagePanel extends React.Component { enableFlair: PropTypes.bool, }; + static contextType = RoomContext; + constructor(props) { super(props); @@ -381,7 +384,7 @@ export default class MessagePanel extends React.Component { // Always show highlighted event if (this.props.highlightedEventId === mxEv.getId()) return true; - return !shouldHideEvent(mxEv); + return !shouldHideEvent(mxEv, this.context); } _readMarkerForEvent(eventId, isLastEvent) { diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index fa67013ccb..b80d909a94 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -859,7 +859,7 @@ export default class RoomView extends React.Component { // update unread count when scrolled up if (!this.state.searchResults && this.state.atEndOfLiveTimeline) { // no change - } else if (!shouldHideEvent(ev)) { + } else if (!shouldHideEvent(ev, this.state)) { this.setState((state, props) => { return {numUnreadMessages: state.numUnreadMessages + 1}; }); diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 6300c7532e..c5ae2e87ba 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -26,6 +26,7 @@ import {EventTimeline} from "matrix-js-sdk/src/models/event-timeline"; import {TimelineWindow} from "matrix-js-sdk/src/timeline-window"; import { _t } from '../../languageHandler'; import {MatrixClientPeg} from "../../MatrixClientPeg"; +import RoomContext from "../../contexts/RoomContext"; import UserActivity from "../../UserActivity"; import Modal from "../../Modal"; import dis from "../../dispatcher/dispatcher"; @@ -122,6 +123,8 @@ class TimelinePanel extends React.Component { layout: LayoutPropType, } + static contextType = RoomContext; + // a map from room id to read marker event timestamp static roomReadMarkerTsMap = {}; @@ -1285,7 +1288,7 @@ class TimelinePanel extends React.Component { const shouldIgnore = !!ev.status || // local echo (ignoreOwn && ev.sender && ev.sender.userId == myUserId); // own message - const isWithoutTile = !haveTileForEvent(ev) || shouldHideEvent(ev); + const isWithoutTile = !haveTileForEvent(ev) || shouldHideEvent(ev, this.context); if (isWithoutTile || !node) { // don't start counting if the event should be ignored, From c24b239478aef6e4e19e3651a9f8376753f17f12 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 5 Jun 2021 13:36:25 +0000 Subject: [PATCH 365/999] Bump ws from 6.2.1 to 6.2.2 in /test/end-to-end-tests Bumps [ws](https://github.com/websockets/ws) from 6.2.1 to 6.2.2. - [Release notes](https://github.com/websockets/ws/releases) - [Commits](https://github.com/websockets/ws/commits) --- updated-dependencies: - dependency-name: ws dependency-type: indirect ... Signed-off-by: dependabot[bot] --- test/end-to-end-tests/yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/end-to-end-tests/yarn.lock b/test/end-to-end-tests/yarn.lock index 97b348fe50..bc942c4f51 100644 --- a/test/end-to-end-tests/yarn.lock +++ b/test/end-to-end-tests/yarn.lock @@ -760,9 +760,9 @@ wrappy@1: integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= ws@^6.1.0: - version "6.2.1" - resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.1.tgz#442fdf0a47ed64f59b6a5d8ff130f4748ed524fb" - integrity sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA== + version "6.2.2" + resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.2.tgz#dd5cdbd57a9979916097652d78f1cc5faea0c32e" + integrity sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw== dependencies: async-limiter "~1.0.0" From f63a92b5cf4c420ddcc652d587dee5fc446bfdd2 Mon Sep 17 00:00:00 2001 From: random Date: Fri, 4 Jun 2021 09:24:41 +0000 Subject: [PATCH 366/999] Translated using Weblate (Italian) Currently translated at 100.0% (2979 of 2979 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/it/ --- src/i18n/strings/it.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/it.json b/src/i18n/strings/it.json index 585ee8ba3a..c83800e82a 100644 --- a/src/i18n/strings/it.json +++ b/src/i18n/strings/it.json @@ -3375,5 +3375,11 @@ "Kick, ban, or invite people to your active room, and make you leave": "Buttare fuori, bandire o invitare persone nella tua stanza attiva e farti uscire", "See when people join, leave, or are invited to this room": "Vedere quando le persone entrano, escono o sono invitate in questa stanza", "Kick, ban, or invite people to this room, and make you leave": "Buttare fuori, bandire o invitare persone in questa stanza e farti uscire", - "See when people join, leave, or are invited to your active room": "Vedere quando le persone entrano, escono o sono invitate nella tua stanza attiva" + "See when people join, leave, or are invited to your active room": "Vedere quando le persone entrano, escono o sono invitate nella tua stanza attiva", + "Currently joining %(count)s rooms|one": "Stai entrando in %(count)s stanza", + "Currently joining %(count)s rooms|other": "Stai entrando in %(count)s stanze", + "Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "Prova parole diverse o controlla errori di battitura. Alcuni risultati potrebbero non essere visibili dato che sono privati e ti servirebbe un invito per unirti.", + "No results for \"%(query)s\"": "Nessun risultato per \"%(query)s\"", + "The user you called is busy.": "L'utente che hai chiamato è occupato.", + "User Busy": "Utente occupato" } From a85e251f0a6ceb0444e3f2de41471803ba07602c Mon Sep 17 00:00:00 2001 From: iaiz Date: Tue, 1 Jun 2021 21:21:42 +0000 Subject: [PATCH 367/999] Translated using Weblate (Spanish) Currently translated at 100.0% (2979 of 2979 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/es/ --- src/i18n/strings/es.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/es.json b/src/i18n/strings/es.json index 60f5d06bec..5e8e57bacd 100644 --- a/src/i18n/strings/es.json +++ b/src/i18n/strings/es.json @@ -3315,5 +3315,11 @@ "See when people join, leave, or are invited to your active room": "Ver cuando alguien se una, salga o se le invite a tu sala activa", "Kick, ban, or invite people to this room, and make you leave": "Expulsar, vetar o invitar personas a esta sala, y hacerte salir de ella", "Kick, ban, or invite people to your active room, and make you leave": "Expulsar, vetar o invitar a gente a tu sala activa, o hacerte salir", - "See when people join, leave, or are invited to this room": "Ver cuando alguien se une, sale o se le invita a la sala" + "See when people join, leave, or are invited to this room": "Ver cuando alguien se une, sale o se le invita a la sala", + "Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "Prueba con sinónimos o revisa si te has equivocado al escribir. Puede que algunos resultados no sean visibles si son privados y necesites que te inviten para verlos.", + "Currently joining %(count)s rooms|one": "Entrando en %(count)s sala", + "Currently joining %(count)s rooms|other": "Entrando en %(count)s salas", + "No results for \"%(query)s\"": "Ningún resultado para «%(query)s»", + "The user you called is busy.": "La persona a la que has llamado está ocupada.", + "User Busy": "Persona ocupada" } From d7de5dfe833cd1ea46faa6c0fcd5e273435d5404 Mon Sep 17 00:00:00 2001 From: libexus Date: Fri, 4 Jun 2021 13:47:33 +0000 Subject: [PATCH 368/999] Translated using Weblate (German) Currently translated at 99.4% (2962 of 2979 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/de/ --- src/i18n/strings/de_DE.json | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index dcc5343af4..9d1c1fa071 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -3343,5 +3343,14 @@ "Your feedback will help make spaces better. The more detail you can go into, the better.": "Dein Feedback hilfst uns, die Spaces zu verbessern. Je genauer, desto besser.", "If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "Durchs Verlassen lädt %(brand)s mit deaktivierten Spaces neu. Danach kannst du Communities und Custom Tags wieder verwenden.", "sends space invaders": "sendet Space Invaders", - "Sends the given message with a space themed effect": "Sendet die Nachricht mit Raumschiffen" + "Sends the given message with a space themed effect": "Sendet die Nachricht mit Raumschiffen", + "Space Autocomplete": "Spaces automatisch vervollständigen", + "Currently joining %(count)s rooms|one": "Betrete %(count)s Raum", + "Currently joining %(count)s rooms|other": "Betrete %(count)s Räume", + "Go to my space": "Zu meinem Space", + "Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "Überprüfe auf Tippfehler oder verwende andere Suchbegriffe. Beachte, dass Ergebnisse aus privaten Räumen, in die du nicht eingeladen wurdest, nicht angezeigt werden.", + "See when people join, leave, or are invited to this room": "Anzeigen, wenn Leute eingeladen werden oder den Raum betreten und verlassen", + "The user you called is busy.": "Der angerufene Benutzer ist momentan beschäftigt.", + "User Busy": "Benutzer beschäftigt", + "No results for \"%(query)s\"": "Keine Ergebnisse für \"%(query)s\"" } From 7abd8957a5e2ff0be597d6435aea5f3f16a80e4a Mon Sep 17 00:00:00 2001 From: Jeff Huang Date: Wed, 2 Jun 2021 02:39:11 +0000 Subject: [PATCH 369/999] Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (2979 of 2979 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/zh_Hant/ --- src/i18n/strings/zh_Hant.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json index 5c27fb3878..053839f937 100644 --- a/src/i18n/strings/zh_Hant.json +++ b/src/i18n/strings/zh_Hant.json @@ -3378,5 +3378,11 @@ "See when people join, leave, or are invited to your active room": "檢視人們何時加入、離開或被邀請至您活躍的聊天室", "Kick, ban, or invite people to your active room, and make you leave": "踢除、封鎖或邀請人們到您作用中的聊天室,然後讓您離開", "See when people join, leave, or are invited to this room": "檢視人們何時加入、離開或被邀請至此聊天室", - "Kick, ban, or invite people to this room, and make you leave": "踢除、封鎖或邀請人們到此聊天室,然後讓您離開" + "Kick, ban, or invite people to this room, and make you leave": "踢除、封鎖或邀請人們到此聊天室,然後讓您離開", + "Currently joining %(count)s rooms|one": "目前正在加入 %(count)s 個聊天室", + "Currently joining %(count)s rooms|other": "目前正在加入 %(count)s 個聊天室", + "Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "嘗試不同的詞或是檢查拼字。某些結果可能不可見,因為其為私人的,您必須要有邀請才能加入。", + "No results for \"%(query)s\"": "「%(query)s」沒有結果", + "The user you called is busy.": "您想要通話的使用者目前忙碌中。", + "User Busy": "使用者忙碌" } From 2533a9f52eb334bdb316ad9ebba4824693f940cb Mon Sep 17 00:00:00 2001 From: LinAGKar Date: Tue, 1 Jun 2021 18:33:41 +0000 Subject: [PATCH 370/999] Translated using Weblate (Swedish) Currently translated at 100.0% (2979 of 2979 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/sv/ --- src/i18n/strings/sv.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/sv.json b/src/i18n/strings/sv.json index a50c039e9e..f026e0fe02 100644 --- a/src/i18n/strings/sv.json +++ b/src/i18n/strings/sv.json @@ -3305,5 +3305,11 @@ "See when people join, leave, or are invited to your active room": "Se när folk går med, lämnar eller bjuds in till ditt aktiva rum", "Kick, ban, or invite people to your active room, and make you leave": "Kicka, banna eller bjuda in folk till ditt aktiva rum, och tvinga dig att lämna", "See when people join, leave, or are invited to this room": "Se när folk går med, lämnar eller bjuds in till det här rummet", - "Kick, ban, or invite people to this room, and make you leave": "Kicka, banna eller bjuda in folk till det här rummet, och tvinga dig att lämna" + "Kick, ban, or invite people to this room, and make you leave": "Kicka, banna eller bjuda in folk till det här rummet, och tvinga dig att lämna", + "Currently joining %(count)s rooms|one": "Går just nu med i %(count)s rum", + "Currently joining %(count)s rooms|other": "Går just nu med i %(count)s rum", + "Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "Testa andra ord eller kolla efter felskrivningar. Vissa resultat kanske inte visas för att de är privata och du behöver en inbjudan för att gå med i dem.", + "No results for \"%(query)s\"": "Inga resultat för \"%(query)s\"", + "The user you called is busy.": "Användaren du ringde är upptagen.", + "User Busy": "Användare upptagen" } From 6243e693d6c3a14e28bc4c3ecada911a17859a80 Mon Sep 17 00:00:00 2001 From: jelv Date: Wed, 2 Jun 2021 09:39:49 +0000 Subject: [PATCH 371/999] Translated using Weblate (Dutch) Currently translated at 100.0% (2979 of 2979 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/nl/ --- src/i18n/strings/nl.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/nl.json b/src/i18n/strings/nl.json index 16f74e7b2d..7299e9d161 100644 --- a/src/i18n/strings/nl.json +++ b/src/i18n/strings/nl.json @@ -3261,5 +3261,11 @@ "See when people join, leave, or are invited to your active room": "Zie wanneer personen deelnemen, vertrekken of worden uitgenodigd in uw actieve gesprek", "Kick, ban, or invite people to your active room, and make you leave": "Verwijder, verban of nodig personen uit voor uw actieve gesprek en uzelf laten vertrekken", "See when people join, leave, or are invited to this room": "Zie wanneer personen deelnemen, vertrekken of worden uitgenodigd voor dit gesprek", - "Kick, ban, or invite people to this room, and make you leave": "Verwijder, verban of verwijder personen uit dit gesprek en uzelf laten vertrekken" + "Kick, ban, or invite people to this room, and make you leave": "Verwijder, verban of verwijder personen uit dit gesprek en uzelf laten vertrekken", + "Currently joining %(count)s rooms|one": "Momenteel aan het toetreden tot %(count)s gesprek", + "Currently joining %(count)s rooms|other": "Momenteel aan het toetreden tot %(count)s gesprekken", + "Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "Probeer andere woorden of controleer op typefouten. Sommige resultaten zijn mogelijk niet zichtbaar omdat ze privé zijn of u een uitnodiging nodig heeft om deel te nemen.", + "No results for \"%(query)s\"": "Geen resultaten voor \"%(query)s\"", + "The user you called is busy.": "De gebruiker die u belde is bezet.", + "User Busy": "Gebruiker Bezet" } From fe9644baca95f5e658684eeb5e3dd09d83c4491f Mon Sep 17 00:00:00 2001 From: Ihor Hordiichuk Date: Wed, 2 Jun 2021 00:41:15 +0000 Subject: [PATCH 372/999] Translated using Weblate (Ukrainian) Currently translated at 49.2% (1467 of 2979 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/uk/ --- src/i18n/strings/uk.json | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/i18n/strings/uk.json b/src/i18n/strings/uk.json index db5ce9b360..64ba84b678 100644 --- a/src/i18n/strings/uk.json +++ b/src/i18n/strings/uk.json @@ -267,8 +267,8 @@ "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s": "%(weekDayName)s, %(day)s %(monthName)s %(fullYear)s %(time)s", "Who would you like to add to this community?": "Кого ви хочете додати до цієї спільноти?", "Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Там, де ця сторінка містить ототожненну інформацію, як-от назва кімнати, користувача чи групи, ці дані будуть вилучені перед надсиланням на сервер.", - "Call in Progress": "Іде виклик", - "A call is currently being placed!": "Зараз іде виклик!", + "Call in Progress": "Триває виклик", + "A call is currently being placed!": "Зараз триває виклик!", "A call is already in progress!": "Вже здійснюється дзвінок!", "Permission Required": "Потрібен дозвіл", "You do not have permission to start a conference call in this room": "У вас немає дозволу, щоб розпочати дзвінок-конференцію в цій кімнаті", @@ -1602,5 +1602,7 @@ "Share Link to User": "Поділитися посиланням на користувача", "Messages here are end-to-end encrypted. Verify %(displayName)s in their profile - tap on their avatar.": "Повідомлення тут захищено наскрізним шифруванням. Підтвердьте %(displayName)s у їхньому профілі — натиснувши на їх аватар.", "Open": "Відкрити", - "In reply to ": "У відповідь на " + "In reply to ": "У відповідь на ", + "The user you called is busy.": "Користувач, якого ви викликаєте, зайнятий.", + "User Busy": "Користувач зайнятий" } From f5b02ecbf55323af1dcdd057da79ba0801495193 Mon Sep 17 00:00:00 2001 From: XoseM Date: Thu, 3 Jun 2021 06:53:34 +0000 Subject: [PATCH 373/999] Translated using Weblate (Galician) Currently translated at 100.0% (2979 of 2979 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/gl/ --- src/i18n/strings/gl.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/gl.json b/src/i18n/strings/gl.json index 12a2dcd8c3..abb4776a55 100644 --- a/src/i18n/strings/gl.json +++ b/src/i18n/strings/gl.json @@ -3375,5 +3375,11 @@ "See when people join, leave, or are invited to your active room": "Mira cando alguén se une, sae ou é convidada á túa sala activa", "Kick, ban, or invite people to your active room, and make you leave": "Expulsa, veta ou convida a persoas á túa sala activa, e fai que saias", "See when people join, leave, or are invited to this room": "Mira cando se une alguén, sae ou é convidada a esta sala", - "Kick, ban, or invite people to this room, and make you leave": "Expulsa, veta, ou convida persoas a esta sala, e fai que saias" + "Kick, ban, or invite people to this room, and make you leave": "Expulsa, veta, ou convida persoas a esta sala, e fai que saias", + "Currently joining %(count)s rooms|one": "Neste intre estás en %(count)s sala", + "Currently joining %(count)s rooms|other": "Neste intre estás en %(count)s salas", + "Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "Intentao con outras palabras e fíxate nos erros de escritura. Algúns resultados poderían non ser visibles porque son privados e precisas un convite.", + "No results for \"%(query)s\"": "Sen resultados para \"%(query)s\"", + "The user you called is busy.": "A persoa á que chamas está ocupada.", + "User Busy": "Usuaria ocupada" } From af2d902830c7ab2dd656152bef924602142d4406 Mon Sep 17 00:00:00 2001 From: Besnik Bleta Date: Wed, 2 Jun 2021 08:30:52 +0000 Subject: [PATCH 374/999] Translated using Weblate (Albanian) Currently translated at 99.6% (2970 of 2979 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/sq/ --- src/i18n/strings/sq.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/sq.json b/src/i18n/strings/sq.json index bd294093f9..bbeffefda3 100644 --- a/src/i18n/strings/sq.json +++ b/src/i18n/strings/sq.json @@ -3361,5 +3361,12 @@ "sends space invaders": "dërgon pushtues hapësire", "Sends the given message with a space themed effect": "E dërgon mesazhin e dhënë me një efekt teme hapësinore", "See when people join, leave, or are invited to your active room": "Shihni kur persona vijnë, ikin ose janë ftuar në dhomën tuaj aktive", - "See when people join, leave, or are invited to this room": "Shihni kur persona vijnë, ikin ose janë ftuar në këtë dhomë" + "See when people join, leave, or are invited to this room": "Shihni kur persona vijnë, ikin ose janë ftuar në këtë dhomë", + "Space Autocomplete": "Vetëplotësim Hapësire", + "Currently joining %(count)s rooms|one": "Aktualisht duke hyrë në %(count)s dhomë", + "Currently joining %(count)s rooms|other": "Aktualisht duke hyrë në %(count)s dhoma", + "Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "Provoni fjalë të ndryshme, ose kontrolloni për gabime shkrimi. Disa përfundime mund të mos jenë të dukshme, ngaqë janë private dhe ju duhet një ftesë për të marrë pjesë në to.", + "No results for \"%(query)s\"": "S’ka përfundime për \"%(query)s\"", + "The user you called is busy.": "Përdoruesi që thirrët është i zënë.", + "User Busy": "Përdoruesi Është i Zënë" } From 3bdc84bb5d164e37f69a9f45e7ee34f7dcb51089 Mon Sep 17 00:00:00 2001 From: waclaw66 Date: Wed, 2 Jun 2021 06:54:33 +0000 Subject: [PATCH 375/999] Translated using Weblate (Czech) Currently translated at 100.0% (2979 of 2979 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/cs/ --- src/i18n/strings/cs.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/cs.json b/src/i18n/strings/cs.json index 75472b4d38..a84a10f21c 100644 --- a/src/i18n/strings/cs.json +++ b/src/i18n/strings/cs.json @@ -3292,5 +3292,11 @@ "See when people join, leave, or are invited to your active room": "Zjistěte, kdy se lidé připojí, odejdou nebo jsou pozváni do vaší aktivní místnosti", "Kick, ban, or invite people to this room, and make you leave": "Vykopnout, vykázat, pozvat lidi do této místnosti nebo odejít", "Kick, ban, or invite people to your active room, and make you leave": "Vykopnout, vykázat, pozvat lidi do vaší aktivní místnosti nebo odejít", - "See when people join, leave, or are invited to this room": "Zjistěte, kdy se lidé připojí, odejdou nebo jsou pozváni do této místnosti" + "See when people join, leave, or are invited to this room": "Zjistěte, kdy se lidé připojí, odejdou nebo jsou pozváni do této místnosti", + "Currently joining %(count)s rooms|one": "Momentálně se připojuje %(count)s místnost", + "Currently joining %(count)s rooms|other": "Momentálně se připojuje %(count)s místností", + "Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "Vyzkoušejte jiná slova nebo zkontrolujte překlepy. Některé výsledky nemusí být viditelné, protože jsou soukromé a potřebujete k nim pozvánku.", + "No results for \"%(query)s\"": "Žádné výsledky pro \"%(query)s\"", + "The user you called is busy.": "Volaný uživatel je zaneprázdněn.", + "User Busy": "Uživatel zaneprázdněn" } From 261c91211fa0ad914e59ed27cdbd411ca1de65d5 Mon Sep 17 00:00:00 2001 From: Szimszon Date: Wed, 2 Jun 2021 06:12:00 +0000 Subject: [PATCH 376/999] Translated using Weblate (Hungarian) Currently translated at 100.0% (2979 of 2979 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/hu/ --- src/i18n/strings/hu.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json index a6e9992866..e1ccfb288d 100644 --- a/src/i18n/strings/hu.json +++ b/src/i18n/strings/hu.json @@ -3370,5 +3370,11 @@ "See when people join, leave, or are invited to your active room": "Emberek belépésének, távozásának vagy meghívásának a megjelenítése az aktív szobájában", "Kick, ban, or invite people to your active room, and make you leave": "Kirúgni, kitiltani vagy meghívni embereket az aktív szobába és, hogy ön elhagyja a szobát", "See when people join, leave, or are invited to this room": "Emberek belépésének, távozásának vagy meghívásának a megjelenítése ebben a szobában", - "Kick, ban, or invite people to this room, and make you leave": "Kirúgni, kitiltani vagy meghívni embereket ebbe a szobába és, hogy ön elhagyja a szobát" + "Kick, ban, or invite people to this room, and make you leave": "Kirúgni, kitiltani vagy meghívni embereket ebbe a szobába és, hogy ön elhagyja a szobát", + "Currently joining %(count)s rooms|one": "%(count)s szobába lép be", + "Currently joining %(count)s rooms|other": "%(count)s szobába lép be", + "No results for \"%(query)s\"": "Nincs találat ehhez: %(query)s", + "Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "Próbáljon ki más szavakat vagy keressen elgépelést. Néhány találat azért nem látszik, mert privát és meghívóra van szüksége, hogy csatlakozhasson.", + "The user you called is busy.": "A hívott felhasználó foglalt.", + "User Busy": "Felhasználó foglalt" } From 0de8e9b201107ca76d2271d2091f586ba8a00899 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Tue, 1 Jun 2021 17:36:31 +0000 Subject: [PATCH 377/999] Translated using Weblate (Estonian) Currently translated at 99.8% (2974 of 2979 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/et/ --- src/i18n/strings/et.json | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/et.json b/src/i18n/strings/et.json index 5e8d744cca..a8e267fb01 100644 --- a/src/i18n/strings/et.json +++ b/src/i18n/strings/et.json @@ -3345,5 +3345,14 @@ "Send and receive voice messages": "Saada ja võta vastu häälsõnumeid", "Your feedback will help make spaces better. The more detail you can go into, the better.": "Sinu tagasiside aitab teha kogukonnakeskuseid paremaks. Mida detailsemalt sa oma arvamust kirjeldad, seda parem.", "If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "Kui sa lahkud, siis käivitame %(brand)s uuesti nii, et kogukonnakeskused ei ole kasutusel. Vana tüüpi kogukonnad ja kohandatud sildid saavad jälle olema kasutusel.", - "Message search initialisation failed": "Sõnumite otsingu alustamine ei õnnestunud" + "Message search initialisation failed": "Sõnumite otsingu alustamine ei õnnestunud", + "sends space invaders": "korraldab ühe pisikese tulnukate vallutusretke", + "Sends the given message with a space themed effect": "Saadab antud sõnumi kosmoseteemalise efektiga", + "Go to my space": "Palun vaata minu kogukonnakeskust", + "User Busy": "Kasutaja on hõivatud", + "The user you called is busy.": "Kasutaja, kellele sa helistasid, on hõivatud.", + "No results for \"%(query)s\"": "Päringule „%(query)s“ pole vastuseid", + "Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "Proovi muid otsingusõnu või kontrolli, et neis polnud trükivigu. Kuna mõned otsingutulemused on privaatsed ja sa vajad kutset nende nägemiseks, siis kõiki tulemusi siin ei pruugi näha olla.", + "Currently joining %(count)s rooms|other": "Parasjagu liitun %(count)s jututoaga", + "Currently joining %(count)s rooms|one": "Parasjagu liitun %(count)s jututoaga" } From 0ecc97eb4b830f3cbaa09ee8c21617ca5eb5e067 Mon Sep 17 00:00:00 2001 From: Thibault Martin Date: Thu, 3 Jun 2021 07:09:49 +0000 Subject: [PATCH 378/999] Translated using Weblate (French) Currently translated at 100.0% (2979 of 2979 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/fr/ --- src/i18n/strings/fr.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index 7f3614bf90..e199e094e7 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -2842,7 +2842,7 @@ "Change the topic of your active room": "Changer le sujet dans le salon actuel", "Change the topic of this room": "Changer le sujet de ce salon", "Send stickers into this room": "Envoyer des autocollants dans ce salon", - "Remain on your screen when viewing another room, when running": "Reste sur votre écran lors de l'appel quand vous regardez un autre salon", + "Remain on your screen when viewing another room, when running": "Reste sur votre écran lors de l’appel quand vous regardez un autre salon", "Takes the call in the current room off hold": "Reprend l’appel en attente dans ce salon", "Places the call in the current room on hold": "Met l’appel dans ce salon en attente", "Prepends (╯°□°)╯︵ ┻━┻ to a plain-text message": "Ajoute (╯°□°)╯︵ ┻━┻ en préfixe du message", From 442d7f7a02ee6a0e0c3600e37aed08c289e6b32b Mon Sep 17 00:00:00 2001 From: Trendyne Date: Thu, 3 Jun 2021 09:58:35 +0000 Subject: [PATCH 379/999] Translated using Weblate (Icelandic) Currently translated at 22.4% (668 of 2979 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/is/ --- src/i18n/strings/is.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/is.json b/src/i18n/strings/is.json index 35f5342b30..e8718c941a 100644 --- a/src/i18n/strings/is.json +++ b/src/i18n/strings/is.json @@ -720,5 +720,13 @@ "%(count)s messages deleted.|one": "%(count)s skilaboð eytt.", "%(count)s messages deleted.|other": "%(count)s skilaboðum eytt.", "Message deleted on %(date)s": "Skilaboð eytt á %(date)s", - "Message edits": "Skilaboðs breytingar" + "Message edits": "Skilaboðs breytingar", + "List options": "Lista valkosti", + "Create a Group Chat": "Búa Til Hópspjall", + "Explore Public Rooms": "Kanna Almenningsherbergi", + "Explore public rooms": "Kanna almenningsherbergi", + "Explore all public rooms": "Kanna öll almenningsherbergi", + "Liberate your communication": "Frelsaðu samskipti þín", + "Welcome to ": "Velkomin til ", + "Welcome to %(appName)s": "Velkomin til %(appName)s" } From 0f64f4d692f36287ace222eb13e07244fa6e1c63 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Sat, 5 Jun 2021 10:49:44 -0400 Subject: [PATCH 380/999] Fix MessagePanel tests Signed-off-by: Robin Townsend --- test/components/structures/MessagePanel-test.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/test/components/structures/MessagePanel-test.js b/test/components/structures/MessagePanel-test.js index 5b466b4bb0..4f7fca1759 100644 --- a/test/components/structures/MessagePanel-test.js +++ b/test/components/structures/MessagePanel-test.js @@ -51,8 +51,20 @@ class WrappedMessagePanel extends React.Component { }; render() { + const roomContext = { + room, + roomId: room.roomId, + canReact: true, + canReply: true, + showReadReceipts: true, + showRedactions: false, + showJoinLeaves: false, + showAvatarChanges: false, + showDisplaynameChanges: true, + }; + return - + ; From bbd5fab7b5345f3147f8392907fb114129b6c74e Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Sat, 5 Jun 2021 11:15:06 -0400 Subject: [PATCH 381/999] Fix type check As TypeScript points out, you can only set an id on HTML elements, not arbitrary components. Signed-off-by: Robin Townsend --- src/components/views/dialogs/ForwardDialog.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/dialogs/ForwardDialog.tsx b/src/components/views/dialogs/ForwardDialog.tsx index cba292ea25..355c92240e 100644 --- a/src/components/views/dialogs/ForwardDialog.tsx +++ b/src/components/views/dialogs/ForwardDialog.tsx @@ -214,7 +214,7 @@ const ForwardDialog: React.FC = ({ matrixClient: cli, event, permalinkCr />

      -
      +
      = ({ matrixClient: cli, event, permalinkCr autoComplete={true} autoFocus={true} /> - + { rooms.length > 0 ? (
      { rooms.map(room => From b06da16a8567e74b90ef74b860e23f6c04399d89 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Sat, 5 Jun 2021 13:20:27 -0400 Subject: [PATCH 382/999] Fix jumping to bottom without a highlighted event Signed-off-by: Robin Townsend --- src/components/structures/RoomView.tsx | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 5ffc2fd0da..135057dbb1 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -1526,10 +1526,19 @@ export default class RoomView extends React.Component { // jump down to the bottom of this room, where new events are arriving private jumpToLiveTimeline = () => { - dis.dispatch({ - action: 'view_room', - room_id: this.state.room.roomId, - }); + if (this.state.initialEventId && this.state.isInitialEventHighlighted) { + // If we were viewing a highlighted event, firing view_room without + // an event will take care of both clearing the URL fragment and + // jumping to the bottom + dis.dispatch({ + action: 'view_room', + room_id: this.state.room.roomId, + }); + } else { + // Otherwise we have to jump manually + this.messagePanel.jumpToLiveTimeline(); + dis.fire(Action.FocusComposer); + } }; // jump up to wherever our read marker is From 17edfec3aa5adf594dec54672c3d8c4332c91a8c Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Sat, 5 Jun 2021 21:08:44 -0400 Subject: [PATCH 383/999] Make it easier to pan images in the lightbox Previously, if you were dragging an image and your cursor outpaced the edge of the image as it was moving, panning would abruptly stop. This moves a few of the lightbox event listeners one level up to the image wrapper to ensure that all drag movements are detected, even if they don't end over the image's current position. Signed-off-by: Robin Townsend --- res/css/views/elements/_ImageView.scss | 6 +++--- src/components/views/elements/ImageView.tsx | 10 ++++++---- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/res/css/views/elements/_ImageView.scss b/res/css/views/elements/_ImageView.scss index 71035dadc3..da23957b36 100644 --- a/res/css/views/elements/_ImageView.scss +++ b/res/css/views/elements/_ImageView.scss @@ -22,6 +22,7 @@ limitations under the License. } .mx_ImageView_image_wrapper { + pointer-events: initial; display: flex; justify-content: center; align-items: center; @@ -30,7 +31,6 @@ limitations under the License. } .mx_ImageView_image { - pointer-events: all; flex-shrink: 0; } @@ -43,7 +43,7 @@ limitations under the License. } .mx_ImageView_info_wrapper { - pointer-events: all; + pointer-events: initial; padding-left: 32px; display: flex; flex-direction: row; @@ -63,7 +63,7 @@ limitations under the License. .mx_ImageView_toolbar { padding-right: 16px; - pointer-events: all; + pointer-events: initial; display: flex; align-items: center; } diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index df73e1a8cb..d2f09917ad 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -471,7 +471,12 @@ export default class ImageView extends React.Component {
      + ref={this.imageWrapper} + onMouseDown={this.props.onFinished} + onMouseMove={this.onMoving} + onMouseUp={this.onEndMoving} + onMouseLeave={this.onEndMoving} + > { className="mx_ImageView_image" draggable={true} onMouseDown={this.onStartMoving} - onMouseMove={this.onMoving} - onMouseUp={this.onEndMoving} - onMouseLeave={this.onEndMoving} />
      From e891d18fa264491fe81fbbcd893fa5c9fdc1d51a Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Sat, 5 Jun 2021 21:41:28 -0400 Subject: [PATCH 384/999] Add my email to my copyright notices Signed-off-by: Robin Townsend --- res/css/views/dialogs/_ForwardDialog.scss | 2 +- src/components/views/dialogs/ForwardDialog.tsx | 2 +- test/components/views/dialogs/ForwardDialog-test.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/res/css/views/dialogs/_ForwardDialog.scss b/res/css/views/dialogs/_ForwardDialog.scss index fc59259ca2..7422d39626 100644 --- a/res/css/views/dialogs/_ForwardDialog.scss +++ b/res/css/views/dialogs/_ForwardDialog.scss @@ -1,5 +1,5 @@ /* -Copyright 2021 Robin Townsend. +Copyright 2021 Robin Townsend Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/components/views/dialogs/ForwardDialog.tsx b/src/components/views/dialogs/ForwardDialog.tsx index 355c92240e..e97958973b 100644 --- a/src/components/views/dialogs/ForwardDialog.tsx +++ b/src/components/views/dialogs/ForwardDialog.tsx @@ -1,5 +1,5 @@ /* -Copyright 2021 Robin Townsend. +Copyright 2021 Robin Townsend Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/test/components/views/dialogs/ForwardDialog-test.js b/test/components/views/dialogs/ForwardDialog-test.js index 004954c713..c50fb073bf 100644 --- a/test/components/views/dialogs/ForwardDialog-test.js +++ b/test/components/views/dialogs/ForwardDialog-test.js @@ -1,5 +1,5 @@ /* -Copyright 2021 Robin Townsend. +Copyright 2021 Robin Townsend Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From 43921500d3459896a8a220c870a2b46d49d34303 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Sat, 5 Jun 2021 22:21:10 -0400 Subject: [PATCH 385/999] Revert "Match requested avatar size to displayed size" This reverts commit 44b143c8c3063be7ca2bf24e6cfdb81be9351c75. --- src/components/views/elements/EventTilePreview.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/elements/EventTilePreview.tsx b/src/components/views/elements/EventTilePreview.tsx index 6d2ea687de..77db94b5dd 100644 --- a/src/components/views/elements/EventTilePreview.tsx +++ b/src/components/views/elements/EventTilePreview.tsx @@ -61,7 +61,7 @@ interface IState { message: string; } -const AVATAR_SIZE = 30; +const AVATAR_SIZE = 32; @replaceableComponent("views.elements.EventTilePreview") export default class EventTilePreview extends React.Component { From ea2120bdfdb6b75b25973fb89e63df5dc2a9b72b Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Sun, 6 Jun 2021 01:55:01 -0400 Subject: [PATCH 386/999] Fix timestamps 7f835908468dc10848e3e9dcb071f08c7f8132b9 changed timestamps to be hidden at the DOM level, not the CSS level. We can keep that approach, we just need to ensure they still get shown at the right times. Signed-off-by: Robin Townsend --- res/css/views/rooms/_EventTile.scss | 21 +------------------ src/components/structures/MessagePanel.js | 10 +-------- .../views/dialogs/MessageEditHistoryDialog.js | 2 +- src/components/views/elements/ReplyThread.js | 6 +++++- src/components/views/rooms/EventTile.tsx | 6 +++++- src/components/views/rooms/ReplyPreview.js | 2 +- .../views/rooms/SearchResultTile.js | 2 ++ 7 files changed, 16 insertions(+), 33 deletions(-) diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 51d9e1cc9d..c8b4138f27 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -85,12 +85,11 @@ $left-gutter: 64px; } .mx_EventTile_isEditing .mx_MessageTimestamp { - visibility: hidden !important; + visibility: hidden; } .mx_EventTile .mx_MessageTimestamp { display: block; - visibility: hidden; white-space: nowrap; left: 0px; text-align: center; @@ -142,29 +141,11 @@ $left-gutter: 64px; line-height: 57px !important; } -.mx_MessagePanel_alwaysShowTimestamps .mx_MessageTimestamp { - visibility: visible; -} - .mx_EventTile_selected > div > a > .mx_MessageTimestamp { left: 3px; width: auto; } -// Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies) -// The first set is to handle the 'group layout' (default) and the second for the IRC layout -.mx_EventTile_last > div > a > .mx_MessageTimestamp, -.mx_EventTile:hover > div > a > .mx_MessageTimestamp, -.mx_EventTile.mx_EventTile_actionBarFocused > div > a > .mx_MessageTimestamp, -.mx_EventTile.focus-visible:focus-within > div > a > .mx_MessageTimestamp, -.mx_IRCLayout .mx_EventTile_last > a > .mx_MessageTimestamp, -.mx_IRCLayout .mx_EventTile:hover > a > .mx_MessageTimestamp, -.mx_IRCLayout .mx_ReplyThread .mx_EventTile > a > .mx_MessageTimestamp, -.mx_IRCLayout .mx_EventTile.mx_EventTile_actionBarFocused > a > .mx_MessageTimestamp, -.mx_IRCLayout .mx_EventTile.focus-visible:focus-within > a > .mx_MessageTimestamp { - visibility: visible; -} - .mx_EventTile:hover .mx_MessageActionBar, .mx_EventTile.mx_EventTile_actionBarFocused .mx_MessageActionBar, [data-whatinput='keyboard'] .mx_EventTile:focus-within .mx_MessageActionBar, diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 6709fef814..e64d3770d4 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -19,7 +19,6 @@ limitations under the License. import React, {createRef} from 'react'; import ReactDOM from 'react-dom'; import PropTypes from 'prop-types'; -import classNames from 'classnames'; import shouldHideEvent from '../../shouldHideEvent'; import {wantsDateSeparator} from '../../DateUtils'; import * as sdk from '../../index'; @@ -854,13 +853,6 @@ export default class MessagePanel extends React.Component { const style = this.props.hidden ? { display: 'none' } : {}; - const className = classNames( - this.props.className, - { - "mx_MessagePanel_alwaysShowTimestamps": this.props.alwaysShowTimestamps, - }, - ); - let whoIsTyping; if (this.props.room && !this.props.tileShape && this.state.showTypingNotifications) { whoIsTyping = ( -
        {this._renderEdits()}
      +
        {this._renderEdits()}
      ); } const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); diff --git a/src/components/views/elements/ReplyThread.js b/src/components/views/elements/ReplyThread.js index bbced5328f..81ed360b17 100644 --- a/src/components/views/elements/ReplyThread.js +++ b/src/components/views/elements/ReplyThread.js @@ -46,6 +46,8 @@ export default class ReplyThread extends React.Component { permalinkCreator: PropTypes.instanceOf(RoomPermalinkCreator).isRequired, // Specifies which layout to use. layout: LayoutPropType, + // Whether to always show a timestamp + alwaysShowTimestamps: PropTypes.bool, }; static contextType = MatrixClientContext; @@ -212,7 +214,7 @@ export default class ReplyThread extends React.Component { }; } - static makeThread(parentEv, onHeightChanged, permalinkCreator, ref, layout) { + static makeThread(parentEv, onHeightChanged, permalinkCreator, ref, layout, alwaysShowTimestamps) { if (!ReplyThread.getParentEventId(parentEv)) { return null; } @@ -222,6 +224,7 @@ export default class ReplyThread extends React.Component { ref={ref} permalinkCreator={permalinkCreator} layout={layout} + alwaysShowTimestamps={alwaysShowTimestamps} />; } @@ -386,6 +389,7 @@ export default class ReplyThread extends React.Component { isRedacted={ev.isRedacted()} isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")} layout={this.props.layout} + alwaysShowTimestamps={this.props.alwaysShowTimestamps} enableFlair={SettingsStore.getValue(UIFeature.Flair)} replacingEventId={ev.replacingEventId()} /> diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 8cec067c39..e1e3b04f36 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -975,7 +975,8 @@ export default class EventTile extends React.Component { onFocusChange={this.onActionBarFocusChange} /> : undefined; - const showTimestamp = this.props.mxEvent.getTs() && (this.props.alwaysShowTimestamps || this.state.hover); + const showTimestamp = this.props.mxEvent.getTs() && + (this.props.alwaysShowTimestamps || this.props.last || this.state.hover || this.state.actionBarFocused); const timestamp = showTimestamp ? : null; @@ -1108,6 +1109,8 @@ export default class EventTile extends React.Component { this.props.onHeightChanged, this.props.permalinkCreator, this.replyThread, + null, + this.props.alwaysShowTimestamps || this.state.hover, ); } return ( @@ -1139,6 +1142,7 @@ export default class EventTile extends React.Component { this.props.permalinkCreator, this.replyThread, this.props.layout, + this.props.alwaysShowTimestamps || this.state.hover, ); // tab-index=-1 to allow it to be focusable but do not add tab stop for it, primarily for screen readers diff --git a/src/components/views/rooms/ReplyPreview.js b/src/components/views/rooms/ReplyPreview.js index d7f8c74d73..5835eb9f36 100644 --- a/src/components/views/rooms/ReplyPreview.js +++ b/src/components/views/rooms/ReplyPreview.js @@ -89,7 +89,7 @@ export default class ReplyPreview extends React.Component {
      ]; + const alwaysShowTimestamps = SettingsStore.getValue("alwaysShowTimestamps"); const timeline = result.context.getTimeline(); for (let j = 0; j < timeline.length; j++) { @@ -67,6 +68,7 @@ export default class SearchResultTile extends React.Component { highlightLink={this.props.resultLink} onHeightChanged={this.props.onHeightChanged} isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")} + alwaysShowTimestamps={alwaysShowTimestamps} enableFlair={SettingsStore.getValue(UIFeature.Flair)} /> )); From 1c8e34114318e472d7deee6ecc8dd99e2e6baa7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 6 Jun 2021 08:09:41 +0200 Subject: [PATCH 387/999] Reset all translation vars MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Not doing this would result in jumps because everything would get out of sync Signed-off-by: Šimon Brandner --- src/components/views/elements/ImageView.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index df73e1a8cb..1b1decd6d5 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -283,6 +283,10 @@ export default class ImageView extends React.Component { translationX: 0, translationY: 0, }); + this.lastX = 0; + this.lastY = 0; + this.initX = 0; + this.initY = 0; } this.setState({moving: false}); }; From c0964b69b73af9de4759b2f45d97caf0d6c98b59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 6 Jun 2021 08:14:43 +0200 Subject: [PATCH 388/999] Remove redundant vars 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 | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index 1b1decd6d5..d6256f7dd2 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -95,8 +95,6 @@ export default class ImageView extends React.Component { private initX = 0; private initY = 0; - private lastX = 0; - private lastY = 0; private previousX = 0; private previousY = 0; @@ -253,8 +251,8 @@ export default class ImageView extends React.Component { this.setState({moving: true}); this.previousX = this.state.translationX; this.previousY = this.state.translationY; - this.initX = ev.pageX - this.lastX; - this.initY = ev.pageY - this.lastY; + this.initX = ev.pageX - this.state.translationX; + this.initY = ev.pageY - this.state.translationY; }; private onMoving = (ev: React.MouseEvent) => { @@ -263,11 +261,9 @@ export default class ImageView extends React.Component { if (!this.state.moving) return; - this.lastX = ev.pageX - this.initX; - this.lastY = ev.pageY - this.initY; this.setState({ - translationX: this.lastX, - translationY: this.lastY, + translationX: ev.pageX - this.initX, + translationY: ev.pageY - this.initY, }); }; @@ -283,8 +279,6 @@ export default class ImageView extends React.Component { translationX: 0, translationY: 0, }); - this.lastX = 0; - this.lastY = 0; this.initX = 0; this.initY = 0; } From 28be581af18615ec24c4a664744ccae3a110696f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 6 Jun 2021 08:51:07 +0200 Subject: [PATCH 389/999] Take image rotation into account when zooming 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 | 31 +++++++++++++-------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index d6256f7dd2..d4870a569b 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -97,29 +97,36 @@ export default class ImageView extends React.Component { private initY = 0; private previousX = 0; private previousY = 0; + private rotation = 0; componentDidMount() { // We have to use addEventListener() because the listener // needs to be passive in order to work with Chromium this.focusLock.current.addEventListener('wheel', this.onWheel, { passive: false }); // We want to recalculate zoom whenever the window's size changes - window.addEventListener("resize", this.calculateZoom); + window.addEventListener("resize", this.calculateZoomAndRotate); // After the image loads for the first time we want to calculate the zoom - this.image.current.addEventListener("load", this.calculateZoom); + this.image.current.addEventListener("load", this.calculateZoomAndRotate); } componentWillUnmount() { this.focusLock.current.removeEventListener('wheel', this.onWheel); - window.removeEventListener("resize", this.calculateZoom); - this.image.current.removeEventListener("load", this.calculateZoom); + window.removeEventListener("resize", this.calculateZoomAndRotate); + this.image.current.removeEventListener("load", this.calculateZoomAndRotate); } - private calculateZoom = () => { + private calculateZoomAndRotate = () => { const image = this.image.current; const imageWrapper = this.imageWrapper.current; - const zoomX = imageWrapper.clientWidth / image.naturalWidth; - const zoomY = imageWrapper.clientHeight / image.naturalHeight; + const landscape = this.rotation % 180 === 0; + + // If the image is rotated take it into account + const width = landscape ? image.naturalWidth : image.naturalHeight; + const height = landscape ? image.naturalHeight : image.naturalWidth; + + const zoomX = imageWrapper.clientWidth / width; + const zoomY = imageWrapper.clientHeight / height; // If the image is smaller in both dimensions set its the zoom to 1 to // display it in its original size @@ -128,6 +135,7 @@ export default class ImageView extends React.Component { zoom: 1, minZoom: 1, maxZoom: 1, + rotation: this.rotation, }); return; } @@ -140,6 +148,7 @@ export default class ImageView extends React.Component { this.setState({ minZoom: minZoom, maxZoom: 1, + rotation: this.rotation, }); } @@ -190,14 +199,14 @@ export default class ImageView extends React.Component { private onRotateCounterClockwiseClick = () => { const cur = this.state.rotation; - const rotationDegrees = cur - 90; - this.setState({ rotation: rotationDegrees }); + this.rotation = cur - 90; + this.calculateZoomAndRotate(); }; private onRotateClockwiseClick = () => { const cur = this.state.rotation; - const rotationDegrees = cur + 90; - this.setState({ rotation: rotationDegrees }); + this.rotation = cur + 90; + this.calculateZoomAndRotate(); }; private onDownloadClick = () => { From c3fdd733570550fcd48473834009bf6bafafeaac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 6 Jun 2021 08:56:12 +0200 Subject: [PATCH 390/999] Avoid multiple setState() calls 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 | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index d4870a569b..c4ec3497e4 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -144,11 +144,14 @@ export default class ImageView extends React.Component { // image by default const minZoom = Math.min(zoomX, zoomY) * MAX_SCALE; - if (this.state.zoom <= this.state.minZoom) this.setState({zoom: minZoom}); + // If zoom is smaller than minZoom don't go beneath that value + const zoom = (this.state.zoom <= this.state.minZoom) ? minZoom : this.state.zoom; + this.setState({ minZoom: minZoom, maxZoom: 1, rotation: this.rotation, + zoom: zoom, }); } From ba0f6766caefb700324a0a4441e0427689b8faf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 6 Jun 2021 09:02:30 +0200 Subject: [PATCH 391/999] Update curly braces styling 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 | 131 ++++++++++---------- 1 file changed, 66 insertions(+), 65 deletions(-) diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index c4ec3497e4..948e005978 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -19,20 +19,20 @@ limitations under the License. import React, { createRef } from 'react'; import { _t } from '../../../languageHandler'; import AccessibleTooltipButton from "./AccessibleTooltipButton"; -import {Key} from "../../../Keyboard"; +import { Key } from "../../../Keyboard"; import FocusLock from "react-focus-lock"; import MemberAvatar from "../avatars/MemberAvatar"; -import {ContextMenuTooltipButton} from "../../../accessibility/context_menu/ContextMenuTooltipButton"; +import { ContextMenuTooltipButton } from "../../../accessibility/context_menu/ContextMenuTooltipButton"; import MessageContextMenu from "../context_menus/MessageContextMenu"; -import {aboveLeftOf, ContextMenu} from '../../structures/ContextMenu'; +import { aboveLeftOf, ContextMenu } from '../../structures/ContextMenu'; import MessageTimestamp from "../messages/MessageTimestamp"; import SettingsStore from "../../../settings/SettingsStore"; -import {formatFullDate} from "../../../DateUtils"; +import { formatFullDate } from "../../../DateUtils"; import dis from '../../../dispatcher/dispatcher'; -import {replaceableComponent} from "../../../utils/replaceableComponent"; -import {RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks" -import {MatrixEvent} from "matrix-js-sdk/src/models/event"; -import {normalizeWheelEvent} from "../../../utils/Mouse"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks" +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { normalizeWheelEvent } from "../../../utils/Mouse"; // Max scale to keep gaps around the image const MAX_SCALE = 0.95; @@ -167,7 +167,7 @@ export default class ImageView extends React.Component { return; } if (newZoom >= this.state.maxZoom) { - this.setState({zoom: this.state.maxZoom}); + this.setState({ zoom: this.state.maxZoom }); return; } @@ -256,11 +256,11 @@ export default class ImageView extends React.Component { // Zoom in if we are completely zoomed out if (this.state.zoom === this.state.minZoom) { - this.setState({zoom: this.state.maxZoom}); + this.setState({ zoom: this.state.maxZoom }); return; } - this.setState({moving: true}); + this.setState({ moving: true }); this.previousX = this.state.translationX; this.previousY = this.state.translationY; this.initX = ev.pageX - this.state.translationX; @@ -294,7 +294,7 @@ export default class ImageView extends React.Component { this.initX = 0; this.initY = 0; } - this.setState({moving: false}); + this.setState({ moving: false }); }; private renderContextMenu() { @@ -302,14 +302,14 @@ export default class ImageView extends React.Component { if (this.state.contextMenuDisplayed) { contextMenu = ( ); @@ -347,10 +347,10 @@ export default class ImageView extends React.Component { const style = { cursor: cursor, transition: this.state.moving ? null : "transform 200ms ease 0s", - transform: `translateX(${translatePixelsX}) - translateY(${translatePixelsY}) - scale(${zoom}) - rotate(${rotationDegrees})`, + transform: `translateX(${ translatePixelsX }) + translateY(${ translatePixelsY }) + scale(${ zoom }) + rotate(${ rotationDegrees })`, }; let info; @@ -365,37 +365,38 @@ export default class ImageView extends React.Component { const senderName = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender(); const sender = (
      - {senderName} + { senderName }
      ); const messageTimestamp = ( ); const avatar = ( ); info = (
      - {avatar} + { avatar }
      - {sender} - {messageTimestamp} + { sender } + { messageTimestamp }
      ); @@ -413,10 +414,10 @@ export default class ImageView extends React.Component { contextMenuButton = ( ); } @@ -427,14 +428,14 @@ export default class ImageView extends React.Component { zoomOutButton = ( + title={ _t("Zoom out") } + onClick={ this.onZoomOutClick }> ); zoomInButton = ( ); @@ -442,57 +443,57 @@ export default class ImageView extends React.Component { return (
      - {info} + { info }
      + title={ _t("Rotate Right") } + onClick={ this.onRotateClockwiseClick }> - {zoomOutButton} - {zoomInButton} + { zoomOutButton } + { zoomInButton } - {contextMenuButton} + { contextMenuButton } - {this.renderContextMenu()} + { this.renderContextMenu() }
      + ref={ this.imageWrapper }>
      From 903d4d252a0c2ac6043ec24251d4c446f33d7990 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Sun, 6 Jun 2021 23:06:56 -0400 Subject: [PATCH 392/999] Add optimized function to determine whether event has text to display Signed-off-by: Robin Townsend --- src/TextForEvent.js | 274 ++++++++++++---------- src/components/structures/MessagePanel.js | 9 +- src/components/views/rooms/EventTile.tsx | 4 +- 3 files changed, 155 insertions(+), 132 deletions(-) diff --git a/src/TextForEvent.js b/src/TextForEvent.js index 86f9ff20f4..22ce0dd9cf 100644 --- a/src/TextForEvent.js +++ b/src/TextForEvent.js @@ -21,6 +21,10 @@ import SettingsStore from "./settings/SettingsStore"; import {ALL_RULE_TYPES, ROOM_RULE_TYPES, SERVER_RULE_TYPES, USER_RULE_TYPES} from "./mjolnir/BanList"; import {WIDGET_LAYOUT_EVENT_TYPE} from "./stores/widgets/WidgetLayoutStore"; +// These functions are frequently used just to check whether an event has +// any text to display at all. For this reason they return deferred values +// to avoid the expense of looking up translations when they're not needed. + function textForMemberEvent(ev) { // XXX: SYJS-16 "sender is sometimes null for join messages" const senderName = ev.sender ? ev.sender.name : ev.getSender(); @@ -28,84 +32,84 @@ function textForMemberEvent(ev) { const prevContent = ev.getPrevContent(); const content = ev.getContent(); - const reason = content.reason ? (_t('Reason') + ': ' + content.reason) : ''; + const getReason = () => content.reason ? (_t('Reason') + ': ' + content.reason) : ''; switch (content.membership) { case 'invite': { const threePidContent = content.third_party_invite; if (threePidContent) { if (threePidContent.display_name) { - return _t('%(targetName)s accepted the invitation for %(displayName)s.', { + return () => _t('%(targetName)s accepted the invitation for %(displayName)s.', { targetName, displayName: threePidContent.display_name, }); } else { - return _t('%(targetName)s accepted an invitation.', {targetName}); + return () => _t('%(targetName)s accepted an invitation.', {targetName}); } } else { - return _t('%(senderName)s invited %(targetName)s.', {senderName, targetName}); + return () => _t('%(senderName)s invited %(targetName)s.', {senderName, targetName}); } } case 'ban': - return _t('%(senderName)s banned %(targetName)s.', {senderName, targetName}) + ' ' + reason; + return () => _t('%(senderName)s banned %(targetName)s.', {senderName, targetName}) + ' ' + getReason(); case 'join': if (prevContent && prevContent.membership === 'join') { if (prevContent.displayname && content.displayname && prevContent.displayname !== content.displayname) { - return _t('%(oldDisplayName)s changed their display name to %(displayName)s.', { + return () => _t('%(oldDisplayName)s changed their display name to %(displayName)s.', { oldDisplayName: prevContent.displayname, displayName: content.displayname, }); } else if (!prevContent.displayname && content.displayname) { - return _t('%(senderName)s set their display name to %(displayName)s.', { + return () => _t('%(senderName)s set their display name to %(displayName)s.', { senderName: ev.getSender(), displayName: content.displayname, }); } else if (prevContent.displayname && !content.displayname) { - return _t('%(senderName)s removed their display name (%(oldDisplayName)s).', { + return () => _t('%(senderName)s removed their display name (%(oldDisplayName)s).', { senderName, oldDisplayName: prevContent.displayname, }); } else if (prevContent.avatar_url && !content.avatar_url) { - return _t('%(senderName)s removed their profile picture.', {senderName}); + return () => _t('%(senderName)s removed their profile picture.', {senderName}); } else if (prevContent.avatar_url && content.avatar_url && prevContent.avatar_url !== content.avatar_url) { - return _t('%(senderName)s changed their profile picture.', {senderName}); + return () => _t('%(senderName)s changed their profile picture.', {senderName}); } else if (!prevContent.avatar_url && content.avatar_url) { - return _t('%(senderName)s set a profile picture.', {senderName}); + return () => _t('%(senderName)s set a profile picture.', {senderName}); } else if (SettingsStore.getValue("showHiddenEventsInTimeline")) { // This is a null rejoin, it will only be visible if the Labs option is enabled - return _t("%(senderName)s made no change.", {senderName}); + return () => _t("%(senderName)s made no change.", {senderName}); } else { - return ""; + return null; } } else { if (!ev.target) console.warn("Join message has no target! -- " + ev.getContent().state_key); - return _t('%(targetName)s joined the room.', {targetName}); + return () => _t('%(targetName)s joined the room.', {targetName}); } case 'leave': if (ev.getSender() === ev.getStateKey()) { if (prevContent.membership === "invite") { - return _t('%(targetName)s rejected the invitation.', {targetName}); + return () => _t('%(targetName)s rejected the invitation.', {targetName}); } else { - return _t('%(targetName)s left the room.', {targetName}); + return () => _t('%(targetName)s left the room.', {targetName}); } } else if (prevContent.membership === "ban") { - return _t('%(senderName)s unbanned %(targetName)s.', {senderName, targetName}); + return () => _t('%(senderName)s unbanned %(targetName)s.', {senderName, targetName}); } else if (prevContent.membership === "invite") { - return _t('%(senderName)s withdrew %(targetName)s\'s invitation.', { + return () => _t('%(senderName)s withdrew %(targetName)s\'s invitation.', { senderName, targetName, - }) + ' ' + reason; + }) + ' ' + getReason(); } else if (prevContent.membership === "join") { - return _t('%(senderName)s kicked %(targetName)s.', {senderName, targetName}) + ' ' + reason; + return () => _t('%(senderName)s kicked %(targetName)s.', {senderName, targetName}) + ' ' + getReason(); } else { - return ""; + return null; } } } function textForTopicEvent(ev) { const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); - return _t('%(senderDisplayName)s changed the topic to "%(topic)s".', { + return () => _t('%(senderDisplayName)s changed the topic to "%(topic)s".', { senderDisplayName, topic: ev.getContent().topic, }); @@ -115,16 +119,16 @@ function textForRoomNameEvent(ev) { const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); if (!ev.getContent().name || ev.getContent().name.trim().length === 0) { - return _t('%(senderDisplayName)s removed the room name.', {senderDisplayName}); + return () => _t('%(senderDisplayName)s removed the room name.', {senderDisplayName}); } if (ev.getPrevContent().name) { - return _t('%(senderDisplayName)s changed the room name from %(oldRoomName)s to %(newRoomName)s.', { + return () => _t('%(senderDisplayName)s changed the room name from %(oldRoomName)s to %(newRoomName)s.', { senderDisplayName, oldRoomName: ev.getPrevContent().name, newRoomName: ev.getContent().name, }); } - return _t('%(senderDisplayName)s changed the room name to %(roomName)s.', { + return () => _t('%(senderDisplayName)s changed the room name to %(roomName)s.', { senderDisplayName, roomName: ev.getContent().name, }); @@ -132,19 +136,23 @@ function textForRoomNameEvent(ev) { function textForTombstoneEvent(ev) { const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); - return _t('%(senderDisplayName)s upgraded this room.', {senderDisplayName}); + return () => _t('%(senderDisplayName)s upgraded this room.', {senderDisplayName}); } function textForJoinRulesEvent(ev) { const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); switch (ev.getContent().join_rule) { case "public": - return _t('%(senderDisplayName)s made the room public to whoever knows the link.', {senderDisplayName}); + return () => _t('%(senderDisplayName)s made the room public to whoever knows the link.', { + senderDisplayName, + }); case "invite": - return _t('%(senderDisplayName)s made the room invite only.', {senderDisplayName}); + return () => _t('%(senderDisplayName)s made the room invite only.', { + senderDisplayName, + }); default: // The spec supports "knock" and "private", however nothing implements these. - return _t('%(senderDisplayName)s changed the join rule to %(rule)s', { + return () => _t('%(senderDisplayName)s changed the join rule to %(rule)s', { senderDisplayName, rule: ev.getContent().join_rule, }); @@ -155,12 +163,12 @@ function textForGuestAccessEvent(ev) { const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); switch (ev.getContent().guest_access) { case "can_join": - return _t('%(senderDisplayName)s has allowed guests to join the room.', {senderDisplayName}); + return () => _t('%(senderDisplayName)s has allowed guests to join the room.', {senderDisplayName}); case "forbidden": - return _t('%(senderDisplayName)s has prevented guests from joining the room.', {senderDisplayName}); + return () => _t('%(senderDisplayName)s has prevented guests from joining the room.', {senderDisplayName}); default: // There's no other options we can expect, however just for safety's sake we'll do this. - return _t('%(senderDisplayName)s changed guest access to %(rule)s', { + return () => _t('%(senderDisplayName)s changed guest access to %(rule)s', { senderDisplayName, rule: ev.getContent().guest_access, }); @@ -175,17 +183,17 @@ function textForRelatedGroupsEvent(ev) { const removed = prevGroups.filter((g) => !groups.includes(g)); if (added.length && !removed.length) { - return _t('%(senderDisplayName)s enabled flair for %(groups)s in this room.', { + return () => _t('%(senderDisplayName)s enabled flair for %(groups)s in this room.', { senderDisplayName, groups: added.join(', '), }); } else if (!added.length && removed.length) { - return _t('%(senderDisplayName)s disabled flair for %(groups)s in this room.', { + return () => _t('%(senderDisplayName)s disabled flair for %(groups)s in this room.', { senderDisplayName, groups: removed.join(', '), }); } else if (added.length && removed.length) { - return _t('%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for ' + + return () => _t('%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for ' + '%(oldGroups)s in this room.', { senderDisplayName, newGroups: added.join(', '), @@ -193,7 +201,7 @@ function textForRelatedGroupsEvent(ev) { }); } else { // Don't bother rendering this change (because there were no changes) - return ''; + return null; } } @@ -207,11 +215,11 @@ function textForServerACLEvent(ev) { allow_ip_literals: !(prevContent.allow_ip_literals === false), }; - let text = ""; + let getText = null; if (prev.deny.length === 0 && prev.allow.length === 0) { - text = _t("%(senderDisplayName)s set the server ACLs for this room.", {senderDisplayName}); + getText = () => _t("%(senderDisplayName)s set the server ACLs for this room.", {senderDisplayName}); } else { - text = _t("%(senderDisplayName)s changed the server ACLs for this room.", {senderDisplayName}); + getText = () => _t("%(senderDisplayName)s changed the server ACLs for this room.", {senderDisplayName}); } if (!Array.isArray(current.allow)) { @@ -220,21 +228,24 @@ function textForServerACLEvent(ev) { // If we know for sure everyone is banned, mark the room as obliterated if (current.allow.length === 0) { - return text + " " + _t("🎉 All servers are banned from participating! This room can no longer be used."); + return () => getText() + " " + + _t("🎉 All servers are banned from participating! This room can no longer be used."); } - return text; + return getText; } function textForMessageEvent(ev) { - const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); - let message = senderDisplayName + ': ' + ev.getContent().body; - if (ev.getContent().msgtype === "m.emote") { - message = "* " + senderDisplayName + " " + message; - } else if (ev.getContent().msgtype === "m.image") { - message = _t('%(senderDisplayName)s sent an image.', {senderDisplayName}); - } - return message; + return () => { + const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); + let message = senderDisplayName + ': ' + ev.getContent().body; + if (ev.getContent().msgtype === "m.emote") { + message = "* " + senderDisplayName + " " + message; + } else if (ev.getContent().msgtype === "m.image") { + message = _t('%(senderDisplayName)s sent an image.', {senderDisplayName}); + } + return message; + }; } function textForCanonicalAliasEvent(ev) { @@ -248,96 +259,100 @@ function textForCanonicalAliasEvent(ev) { if (!removedAltAliases.length && !addedAltAliases.length) { if (newAlias) { - return _t('%(senderName)s set the main address for this room to %(address)s.', { + return () => _t('%(senderName)s set the main address for this room to %(address)s.', { senderName: senderName, address: ev.getContent().alias, }); } else if (oldAlias) { - return _t('%(senderName)s removed the main address for this room.', { + return () => _t('%(senderName)s removed the main address for this room.', { senderName: senderName, }); } } else if (newAlias === oldAlias) { if (addedAltAliases.length && !removedAltAliases.length) { - return _t('%(senderName)s added the alternative addresses %(addresses)s for this room.', { + return () => _t('%(senderName)s added the alternative addresses %(addresses)s for this room.', { senderName: senderName, addresses: addedAltAliases.join(", "), count: addedAltAliases.length, }); } if (removedAltAliases.length && !addedAltAliases.length) { - return _t('%(senderName)s removed the alternative addresses %(addresses)s for this room.', { + return () => _t('%(senderName)s removed the alternative addresses %(addresses)s for this room.', { senderName: senderName, addresses: removedAltAliases.join(", "), count: removedAltAliases.length, }); } if (removedAltAliases.length && addedAltAliases.length) { - return _t('%(senderName)s changed the alternative addresses for this room.', { + return () => _t('%(senderName)s changed the alternative addresses for this room.', { senderName: senderName, }); } } else { // both alias and alt_aliases where modified - return _t('%(senderName)s changed the main and alternative addresses for this room.', { + return () => _t('%(senderName)s changed the main and alternative addresses for this room.', { senderName: senderName, }); } // in case there is no difference between the two events, // say something as we can't simply hide the tile from here - return _t('%(senderName)s changed the addresses for this room.', { + return () => _t('%(senderName)s changed the addresses for this room.', { senderName: senderName, }); } function textForCallAnswerEvent(event) { - const senderName = event.sender ? event.sender.name : _t('Someone'); - const supported = MatrixClientPeg.get().supportsVoip() ? '' : _t('(not supported by this browser)'); - return _t('%(senderName)s answered the call.', {senderName}) + ' ' + supported; + return () => { + const senderName = event.sender ? event.sender.name : _t('Someone'); + const supported = MatrixClientPeg.get().supportsVoip() ? '' : _t('(not supported by this browser)'); + return _t('%(senderName)s answered the call.', {senderName}) + ' ' + supported; + }; } function textForCallHangupEvent(event) { - const senderName = event.sender ? event.sender.name : _t('Someone'); + const getSenderName = () => event.sender ? event.sender.name : _t('Someone'); const eventContent = event.getContent(); - let reason = ""; + let getReason = () => ""; if (!MatrixClientPeg.get().supportsVoip()) { - reason = _t('(not supported by this browser)'); + getReason = () => _t('(not supported by this browser)'); } else if (eventContent.reason) { if (eventContent.reason === "ice_failed") { // We couldn't establish a connection at all - reason = _t('(could not connect media)'); + getReason = () => _t('(could not connect media)'); } else if (eventContent.reason === "ice_timeout") { // We established a connection but it died - reason = _t('(connection failed)'); + getReason = () => _t('(connection failed)'); } else if (eventContent.reason === "user_media_failed") { // The other side couldn't open capture devices - reason = _t("(their device couldn't start the camera / microphone)"); + getReason = () => _t("(their device couldn't start the camera / microphone)"); } else if (eventContent.reason === "unknown_error") { // An error code the other side doesn't have a way to express // (as opposed to an error code they gave but we don't know about, // in which case we show the error code) - reason = _t("(an error occurred)"); + getReason = () => _t("(an error occurred)"); } else if (eventContent.reason === "invite_timeout") { - reason = _t('(no answer)'); + getReason = () => _t('(no answer)'); } else if (eventContent.reason === "user hangup" || eventContent.reason === "user_hangup") { // workaround for https://github.com/vector-im/element-web/issues/5178 // it seems Android randomly sets a reason of "user hangup" which is // interpreted as an error code :( // https://github.com/vector-im/riot-android/issues/2623 // Also the correct hangup code as of VoIP v1 (with underscore) - reason = ''; + getReason = () => ''; } else { - reason = _t('(unknown failure: %(reason)s)', {reason: eventContent.reason}); + getReason = () => _t('(unknown failure: %(reason)s)', {reason: eventContent.reason}); } } - return _t('%(senderName)s ended the call.', {senderName}) + ' ' + reason; + return () => _t('%(senderName)s ended the call.', {senderName: getSenderName()}) + ' ' + getReason(); } function textForCallRejectEvent(event) { - const senderName = event.sender ? event.sender.name : _t('Someone'); - return _t('%(senderName)s declined the call.', {senderName}); + return () => { + const senderName = event.sender ? event.sender.name : _t('Someone'); + return _t('%(senderName)s declined the call.', {senderName}); + }; } function textForCallInviteEvent(event) { - const senderName = event.sender ? event.sender.name : _t('Someone'); + const getSenderName = () => event.sender ? event.sender.name : _t('Someone'); // FIXME: Find a better way to determine this from the event? let isVoice = true; if (event.getContent().offer && event.getContent().offer.sdp && @@ -350,13 +365,21 @@ function textForCallInviteEvent(event) { // can have a hard time translating those strings. In an effort to make translations easier // and more accurate, we break out the string-based variables to a couple booleans. if (isVoice && isSupported) { - return _t("%(senderName)s placed a voice call.", {senderName}); + return () => _t("%(senderName)s placed a voice call.", { + senderName: getSenderName(), + }); } else if (isVoice && !isSupported) { - return _t("%(senderName)s placed a voice call. (not supported by this browser)", {senderName}); + return () => _t("%(senderName)s placed a voice call. (not supported by this browser)", { + senderName: getSenderName(), + }); } else if (!isVoice && isSupported) { - return _t("%(senderName)s placed a video call.", {senderName}); + return () => _t("%(senderName)s placed a video call.", { + senderName: getSenderName(), + }); } else if (!isVoice && !isSupported) { - return _t("%(senderName)s placed a video call. (not supported by this browser)", {senderName}); + return () => _t("%(senderName)s placed a video call. (not supported by this browser)", { + senderName: getSenderName(), + }); } } @@ -364,14 +387,13 @@ function textForThreePidInviteEvent(event) { const senderName = event.sender ? event.sender.name : event.getSender(); if (!isValid3pidInvite(event)) { - const targetDisplayName = event.getPrevContent().display_name || _t("Someone"); - return _t('%(senderName)s revoked the invitation for %(targetDisplayName)s to join the room.', { + return () => _t('%(senderName)s revoked the invitation for %(targetDisplayName)s to join the room.', { senderName, - targetDisplayName, + targetDisplayName: event.getPrevContent().display_name || _t("Someone"), }); } - return _t('%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.', { + return () => _t('%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.', { senderName, targetDisplayName: event.getContent().display_name, }); @@ -381,17 +403,17 @@ function textForHistoryVisibilityEvent(event) { const senderName = event.sender ? event.sender.name : event.getSender(); switch (event.getContent().history_visibility) { case 'invited': - return _t('%(senderName)s made future room history visible to all room members, ' + return () => _t('%(senderName)s made future room history visible to all room members, ' + 'from the point they are invited.', {senderName}); case 'joined': - return _t('%(senderName)s made future room history visible to all room members, ' + return () => _t('%(senderName)s made future room history visible to all room members, ' + 'from the point they joined.', {senderName}); case 'shared': - return _t('%(senderName)s made future room history visible to all room members.', {senderName}); + return () => _t('%(senderName)s made future room history visible to all room members.', {senderName}); case 'world_readable': - return _t('%(senderName)s made future room history visible to anyone.', {senderName}); + return () => _t('%(senderName)s made future room history visible to anyone.', {senderName}); default: - return _t('%(senderName)s made future room history visible to unknown (%(visibility)s).', { + return () => _t('%(senderName)s made future room history visible to unknown (%(visibility)s).', { senderName, visibility: event.getContent().history_visibility, }); @@ -403,7 +425,7 @@ function textForPowerEvent(event) { const senderName = event.sender ? event.sender.name : event.getSender(); if (!event.getPrevContent() || !event.getPrevContent().users || !event.getContent() || !event.getContent().users) { - return ''; + return null; } const userDefault = event.getContent().users_default || 0; // Construct set of userIds @@ -418,35 +440,35 @@ function textForPowerEvent(event) { if (users.indexOf(userId) === -1) users.push(userId); }, ); - const diff = []; - // XXX: This is also surely broken for i18n + const diffs = []; users.forEach((userId) => { // Previous power level const from = event.getPrevContent().users[userId]; // Current power level const to = event.getContent().users[userId]; if (to !== from) { - diff.push( - _t('%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s', { - userId, - fromPowerLevel: Roles.textualPowerLevel(from, userDefault), - toPowerLevel: Roles.textualPowerLevel(to, userDefault), - }), - ); + diffs.push({ userId, from, to }); } }); - if (!diff.length) { - return ''; + if (!diffs.length) { + return null; } - return _t('%(senderName)s changed the power level of %(powerLevelDiffText)s.', { + // XXX: This is also surely broken for i18n + return () => _t('%(senderName)s changed the power level of %(powerLevelDiffText)s.', { senderName, - powerLevelDiffText: diff.join(", "), + powerLevelDiffText: diffs.map(diff => + _t('%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s', { + userId: diff.userId, + fromPowerLevel: Roles.textualPowerLevel(diff.from, userDefault), + toPowerLevel: Roles.textualPowerLevel(diff.to, userDefault), + }), + ).join(", "), }); } function textForPinnedEvent(event) { const senderName = event.sender ? event.sender.name : event.getSender(); - return _t("%(senderName)s changed the pinned messages for the room.", {senderName}); + return () => _t("%(senderName)s changed the pinned messages for the room.", {senderName}); } function textForWidgetEvent(event) { @@ -464,16 +486,16 @@ function textForWidgetEvent(event) { // equivalent to that condition. if (url) { if (prevUrl) { - return _t('%(widgetName)s widget modified by %(senderName)s', { + return () => _t('%(widgetName)s widget modified by %(senderName)s', { widgetName, senderName, }); } else { - return _t('%(widgetName)s widget added by %(senderName)s', { + return () => _t('%(widgetName)s widget added by %(senderName)s', { widgetName, senderName, }); } } else { - return _t('%(widgetName)s widget removed by %(senderName)s', { + return () => _t('%(widgetName)s widget removed by %(senderName)s', { widgetName, senderName, }); } @@ -481,7 +503,7 @@ function textForWidgetEvent(event) { function textForWidgetLayoutEvent(event) { const senderName = event.sender?.name || event.getSender(); - return _t("%(senderName)s has updated the widget layout", {senderName}); + return () => _t("%(senderName)s has updated the widget layout", {senderName}); } function textForMjolnirEvent(event) { @@ -492,74 +514,74 @@ function textForMjolnirEvent(event) { // Rule removed if (!entity) { if (USER_RULE_TYPES.includes(event.getType())) { - return _t("%(senderName)s removed the rule banning users matching %(glob)s", + return () => _t("%(senderName)s removed the rule banning users matching %(glob)s", {senderName, glob: prevEntity}); } else if (ROOM_RULE_TYPES.includes(event.getType())) { - return _t("%(senderName)s removed the rule banning rooms matching %(glob)s", + return () => _t("%(senderName)s removed the rule banning rooms matching %(glob)s", {senderName, glob: prevEntity}); } else if (SERVER_RULE_TYPES.includes(event.getType())) { - return _t("%(senderName)s removed the rule banning servers matching %(glob)s", + return () => _t("%(senderName)s removed the rule banning servers matching %(glob)s", {senderName, glob: prevEntity}); } // Unknown type. We'll say something, but we shouldn't end up here. - return _t("%(senderName)s removed a ban rule matching %(glob)s", {senderName, glob: prevEntity}); + return () => _t("%(senderName)s removed a ban rule matching %(glob)s", {senderName, glob: prevEntity}); } // Invalid rule - if (!recommendation || !reason) return _t(`%(senderName)s updated an invalid ban rule`, {senderName}); + if (!recommendation || !reason) return () => _t(`%(senderName)s updated an invalid ban rule`, {senderName}); // Rule updated if (entity === prevEntity) { if (USER_RULE_TYPES.includes(event.getType())) { - return _t("%(senderName)s updated the rule banning users matching %(glob)s for %(reason)s", + return () => _t("%(senderName)s updated the rule banning users matching %(glob)s for %(reason)s", {senderName, glob: entity, reason}); } else if (ROOM_RULE_TYPES.includes(event.getType())) { - return _t("%(senderName)s updated the rule banning rooms matching %(glob)s for %(reason)s", + return () => _t("%(senderName)s updated the rule banning rooms matching %(glob)s for %(reason)s", {senderName, glob: entity, reason}); } else if (SERVER_RULE_TYPES.includes(event.getType())) { - return _t("%(senderName)s updated the rule banning servers matching %(glob)s for %(reason)s", + return () => _t("%(senderName)s updated the rule banning servers matching %(glob)s for %(reason)s", {senderName, glob: entity, reason}); } // Unknown type. We'll say something but we shouldn't end up here. - return _t("%(senderName)s updated a ban rule matching %(glob)s for %(reason)s", + return () => _t("%(senderName)s updated a ban rule matching %(glob)s for %(reason)s", {senderName, glob: entity, reason}); } // New rule if (!prevEntity) { if (USER_RULE_TYPES.includes(event.getType())) { - return _t("%(senderName)s created a rule banning users matching %(glob)s for %(reason)s", + return () => _t("%(senderName)s created a rule banning users matching %(glob)s for %(reason)s", {senderName, glob: entity, reason}); } else if (ROOM_RULE_TYPES.includes(event.getType())) { - return _t("%(senderName)s created a rule banning rooms matching %(glob)s for %(reason)s", + return () => _t("%(senderName)s created a rule banning rooms matching %(glob)s for %(reason)s", {senderName, glob: entity, reason}); } else if (SERVER_RULE_TYPES.includes(event.getType())) { - return _t("%(senderName)s created a rule banning servers matching %(glob)s for %(reason)s", + return () => _t("%(senderName)s created a rule banning servers matching %(glob)s for %(reason)s", {senderName, glob: entity, reason}); } // Unknown type. We'll say something but we shouldn't end up here. - return _t("%(senderName)s created a ban rule matching %(glob)s for %(reason)s", + return () => _t("%(senderName)s created a ban rule matching %(glob)s for %(reason)s", {senderName, glob: entity, reason}); } // else the entity !== prevEntity - count as a removal & add if (USER_RULE_TYPES.includes(event.getType())) { - return _t( + return () => _t( "%(senderName)s changed a rule that was banning users matching %(oldGlob)s to matching " + "%(newGlob)s for %(reason)s", {senderName, oldGlob: prevEntity, newGlob: entity, reason}, ); } else if (ROOM_RULE_TYPES.includes(event.getType())) { - return _t( + return () => _t( "%(senderName)s changed a rule that was banning rooms matching %(oldGlob)s to matching " + "%(newGlob)s for %(reason)s", {senderName, oldGlob: prevEntity, newGlob: entity, reason}, ); } else if (SERVER_RULE_TYPES.includes(event.getType())) { - return _t( + return () => _t( "%(senderName)s changed a rule that was banning servers matching %(oldGlob)s to matching " + "%(newGlob)s for %(reason)s", {senderName, oldGlob: prevEntity, newGlob: entity, reason}, @@ -567,7 +589,7 @@ function textForMjolnirEvent(event) { } // Unknown type. We'll say something but we shouldn't end up here. - return _t("%(senderName)s updated a ban rule that was matching %(oldGlob)s to matching %(newGlob)s " + + return () => _t("%(senderName)s updated a ban rule that was matching %(oldGlob)s to matching %(newGlob)s " + "for %(reason)s", {senderName, oldGlob: prevEntity, newGlob: entity, reason}); } @@ -604,8 +626,12 @@ for (const evType of ALL_RULE_TYPES) { stateHandlers[evType] = textForMjolnirEvent; } +export function hasText(ev) { + const handler = (ev.isState() ? stateHandlers : handlers)[ev.getType()]; + return Boolean(handler?.(ev)); +} + export function textForEvent(ev) { const handler = (ev.isState() ? stateHandlers : handlers)[ev.getType()]; - if (handler) return handler(ev); - return ''; + return handler?.(ev)?.() || ''; } diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index bca62e7b2f..d040944833 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -30,7 +30,7 @@ import RoomContext from "../../contexts/RoomContext"; import {Layout, LayoutPropType} from "../../settings/Layout"; import {_t} from "../../languageHandler"; import {haveTileForEvent} from "../views/rooms/EventTile"; -import {textForEvent} from "../../TextForEvent"; +import {hasText} from "../../TextForEvent"; import IRCTimelineProfileResizer from "../views/elements/IRCTimelineProfileResizer"; import DMRoomMap from "../../utils/DMRoomMap"; import NewRoomIntro from "../views/rooms/NewRoomIntro"; @@ -1180,11 +1180,8 @@ class MemberGrouper { add(ev) { if (ev.getType() === 'm.room.member') { - // We'll just double check that it's worth our time to do so, through an - // ugly hack. If textForEvent returns something, we should group it for - // rendering but if it doesn't then we'll exclude it. - const renderText = textForEvent(ev); - if (!renderText || renderText.trim().length === 0) return; // quietly ignore + // We can ignore any events that don't actually have a message to display + if (!hasText(ev)) return; } this.readMarker = this.readMarker || this.panel._readMarkerForEvent( ev.getId(), diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 8cec067c39..fda2906f07 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -25,7 +25,7 @@ import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import ReplyThread from "../elements/ReplyThread"; import { _t } from '../../../languageHandler'; -import * as TextForEvent from "../../../TextForEvent"; +import { hasText } from "../../../TextForEvent"; import * as sdk from "../../../index"; import dis from '../../../dispatcher/dispatcher'; import SettingsStore from "../../../settings/SettingsStore"; @@ -1200,7 +1200,7 @@ export function haveTileForEvent(e) { const handler = getHandlerTile(e); if (handler === undefined) return false; if (handler === 'messages.TextualEvent') { - return TextForEvent.textForEvent(e) !== ''; + return hasText(e); } else if (handler === 'messages.RoomCreate') { return Boolean(e.getContent()['predecessor']); } else { From 1e574307d0a74c498429c6e2a22f07c61369550b Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Sun, 6 Jun 2021 23:08:47 -0400 Subject: [PATCH 393/999] Cache lowBandwidth setting to speed up BaseAvatar Signed-off-by: Robin Townsend --- src/components/structures/RoomView.tsx | 5 +++++ src/components/views/avatars/BaseAvatar.tsx | 15 +++++++++++---- src/contexts/RoomContext.ts | 1 + 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index b80d909a94..e4dc90f141 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -182,6 +182,7 @@ export interface IState { canReact: boolean; canReply: boolean; layout: Layout; + lowBandwidth: boolean; showReadReceipts: boolean; showRedactions: boolean; showJoinLeaves: boolean; @@ -244,6 +245,7 @@ export default class RoomView extends React.Component { canReact: false, canReply: false, layout: SettingsStore.getValue("layout"), + lowBandwidth: SettingsStore.getValue("lowBandwidth"), showReadReceipts: true, showRedactions: true, showJoinLeaves: true, @@ -279,6 +281,9 @@ export default class RoomView extends React.Component { SettingsStore.watchSetting("layout", null, () => this.setState({ layout: SettingsStore.getValue("layout") }), ), + SettingsStore.watchSetting("lowBandwidth", null, () => + this.setState({ lowBandwidth: SettingsStore.getValue("lowBandwidth") }), + ), ]; } diff --git a/src/components/views/avatars/BaseAvatar.tsx b/src/components/views/avatars/BaseAvatar.tsx index 8ce05e0a55..6949c14636 100644 --- a/src/components/views/avatars/BaseAvatar.tsx +++ b/src/components/views/avatars/BaseAvatar.tsx @@ -22,6 +22,7 @@ import classNames from 'classnames'; import * as AvatarLogic from '../../../Avatar'; import SettingsStore from "../../../settings/SettingsStore"; import AccessibleButton from '../elements/AccessibleButton'; +import RoomContext from "../../../contexts/RoomContext"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import {useEventEmitter} from "../../../hooks/useEventEmitter"; import {toPx} from "../../../utils/units"; @@ -44,12 +45,12 @@ interface IProps { className?: string; } -const calculateUrls = (url, urls) => { +const calculateUrls = (url, urls, lowBandwidth) => { // work out the full set of urls to try to load. This is formed like so: // imageUrls: [ props.url, ...props.urls ] let _urls = []; - if (!SettingsStore.getValue("lowBandwidth")) { + if (!lowBandwidth) { _urls = urls || []; if (url) { @@ -63,7 +64,13 @@ const calculateUrls = (url, urls) => { }; const useImageUrl = ({url, urls}): [string, () => void] => { - const [imageUrls, setUrls] = useState(calculateUrls(url, urls)); + // Since this is a hot code path and the settings store can be slow, we + // use the cached lowBandwidth value from the room context if it exists + const roomContext = useContext(RoomContext); + const lowBandwidth = roomContext ? + roomContext.lowBandwidth : SettingsStore.getValue("lowBandwidth"); + + const [imageUrls, setUrls] = useState(calculateUrls(url, urls, lowBandwidth)); const [urlsIndex, setIndex] = useState(0); const onError = useCallback(() => { @@ -71,7 +78,7 @@ const useImageUrl = ({url, urls}): [string, () => void] => { }, []); useEffect(() => { - setUrls(calculateUrls(url, urls)); + setUrls(calculateUrls(url, urls, lowBandwidth)); setIndex(0); }, [url, JSON.stringify(urls)]); // eslint-disable-line react-hooks/exhaustive-deps diff --git a/src/contexts/RoomContext.ts b/src/contexts/RoomContext.ts index 1efa1c03e7..3464f952a6 100644 --- a/src/contexts/RoomContext.ts +++ b/src/contexts/RoomContext.ts @@ -40,6 +40,7 @@ const RoomContext = createContext({ canReact: false, canReply: false, layout: Layout.Group, + lowBandwidth: false, showReadReceipts: true, showRedactions: true, showJoinLeaves: true, From 21ff1f521fa191308852429214d2697ee35adda8 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Sun, 6 Jun 2021 23:14:26 -0400 Subject: [PATCH 394/999] Fix i18n strings Signed-off-by: Robin Townsend --- src/i18n/strings/en_EN.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 9e85ea28c8..5108ea7fe2 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -556,8 +556,8 @@ "%(senderName)s made future room history visible to all room members.": "%(senderName)s made future room history visible to all room members.", "%(senderName)s made future room history visible to anyone.": "%(senderName)s made future room history visible to anyone.", "%(senderName)s made future room history visible to unknown (%(visibility)s).": "%(senderName)s made future room history visible to unknown (%(visibility)s).", - "%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s": "%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s", "%(senderName)s changed the power level of %(powerLevelDiffText)s.": "%(senderName)s changed the power level of %(powerLevelDiffText)s.", + "%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s": "%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s", "%(senderName)s changed the pinned messages for the room.": "%(senderName)s changed the pinned messages for the room.", "%(widgetName)s widget modified by %(senderName)s": "%(widgetName)s widget modified by %(senderName)s", "%(widgetName)s widget added by %(senderName)s": "%(widgetName)s widget added by %(senderName)s", From b2b95257a8571edde2cf1a7ed110eb1a65652252 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 7 Jun 2021 08:54:41 +0100 Subject: [PATCH 395/999] Convert RoomAliasField to Typescript --- src/components/views/elements/Field.tsx | 7 +- .../{RoomAliasField.js => RoomAliasField.tsx} | 75 ++++++++++--------- src/i18n/strings/en_EN.json | 2 +- 3 files changed, 48 insertions(+), 36 deletions(-) rename src/components/views/elements/{RoomAliasField.js => RoomAliasField.tsx} (67%) diff --git a/src/components/views/elements/Field.tsx b/src/components/views/elements/Field.tsx index 59d9a11596..1373c2df0e 100644 --- a/src/components/views/elements/Field.tsx +++ b/src/components/views/elements/Field.tsx @@ -29,6 +29,11 @@ function getId() { return `${BASE_ID}_${count++}`; } +export interface IValidateOpts { + focused?: boolean; + allowEmpty?: boolean; +} + interface IProps { // The field's ID, which binds the input and label together. Immutable. id?: string; @@ -180,7 +185,7 @@ export default class Field extends React.PureComponent { } }; - public async validate({ focused, allowEmpty = true }: {focused?: boolean, allowEmpty?: boolean}) { + public async validate({ focused, allowEmpty = true }: IValidateOpts) { if (!this.props.onValidate) { return; } diff --git a/src/components/views/elements/RoomAliasField.js b/src/components/views/elements/RoomAliasField.tsx similarity index 67% rename from src/components/views/elements/RoomAliasField.js rename to src/components/views/elements/RoomAliasField.tsx index 813dd8b5cc..7eff529c46 100644 --- a/src/components/views/elements/RoomAliasField.js +++ b/src/components/views/elements/RoomAliasField.tsx @@ -1,5 +1,5 @@ /* -Copyright 2019 New Vector Ltd +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. @@ -13,67 +13,74 @@ 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, { createRef } from "react"; + import { _t } from '../../../languageHandler'; -import React from 'react'; -import PropTypes from 'prop-types'; -import * as sdk from '../../../index'; import withValidation from './Validation'; -import {MatrixClientPeg} from '../../../MatrixClientPeg'; -import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { MatrixClientPeg } from '../../../MatrixClientPeg'; +import { replaceableComponent } from "../../../utils/replaceableComponent"; +import Field, { IValidateOpts } from "./Field"; + +interface IProps { + domain: string; + value: string; + label?: string; + placeholder?: string; + onChange?(value: string): void; +} + +interface IState { + isValid: boolean; +} // Controlled form component wrapping Field for inputting a room alias scoped to a given domain @replaceableComponent("views.elements.RoomAliasField") -export default class RoomAliasField extends React.PureComponent { - static propTypes = { - domain: PropTypes.string.isRequired, - onChange: PropTypes.func, - value: PropTypes.string.isRequired, +export default class RoomAliasField extends React.PureComponent { + private fieldRef = createRef(); + + public state = { + isValid: true, }; - constructor(props) { - super(props); - this.state = {isValid: true}; - } - - _asFullAlias(localpart) { + private asFullAlias(localpart: string): string { return `#${localpart}:${this.props.domain}`; } render() { - const Field = sdk.getComponent('views.elements.Field'); const poundSign = (#); const aliasPostfix = ":" + this.props.domain; const domain = ({aliasPostfix}); const maxlength = 255 - this.props.domain.length - 2; // 2 for # and : return ( this._fieldRef = ref} - onValidate={this._onValidate} - placeholder={_t("e.g. my-room")} - onChange={this._onChange} + ref={this.fieldRef} + onValidate={this.onValidate} + placeholder={this.props.placeholder || _t("e.g. my-room")} + onChange={this.onChange} value={this.props.value.substring(1, this.props.value.length - this.props.domain.length - 1)} maxLength={maxlength} /> ); } - _onChange = (ev) => { + private onChange = (ev) => { if (this.props.onChange) { - this.props.onChange(this._asFullAlias(ev.target.value)); + this.props.onChange(this.asFullAlias(ev.target.value)); } }; - _onValidate = async (fieldState) => { - const result = await this._validationRules(fieldState); + private onValidate = async (fieldState) => { + const result = await this.validationRules(fieldState); this.setState({isValid: result.valid}); return result; }; - _validationRules = withValidation({ + private validationRules = withValidation({ rules: [ { key: "safeLocalpart", @@ -81,7 +88,7 @@ export default class RoomAliasField extends React.PureComponent { if (!value) { return true; } - const fullAlias = this._asFullAlias(value); + const fullAlias = this.asFullAlias(value); // XXX: FIXME https://github.com/matrix-org/matrix-doc/issues/668 return !value.includes("#") && !value.includes(":") && !value.includes(",") && encodeURI(fullAlias) === fullAlias; @@ -90,7 +97,7 @@ export default class RoomAliasField extends React.PureComponent { }, { key: "required", test: async ({ value, allowEmpty }) => allowEmpty || !!value, - invalid: () => _t("Please provide a room address"), + invalid: () => _t("Please provide an address"), }, { key: "taken", final: true, @@ -100,7 +107,7 @@ export default class RoomAliasField extends React.PureComponent { } const client = MatrixClientPeg.get(); try { - await client.getRoomIdForAlias(this._asFullAlias(value)); + await client.getRoomIdForAlias(this.asFullAlias(value)); // we got a room id, so the alias is taken return false; } catch (err) { @@ -120,11 +127,11 @@ export default class RoomAliasField extends React.PureComponent { return this.state.isValid; } - validate(options) { - return this._fieldRef.validate(options); + validate(options: IValidateOpts) { + return this.fieldRef.current?.validate(options); } focus() { - this._fieldRef.focus(); + this.fieldRef.current?.focus(); } } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 9e85ea28c8..02662aa508 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2014,7 +2014,7 @@ "Room address": "Room address", "e.g. my-room": "e.g. my-room", "Some characters not allowed": "Some characters not allowed", - "Please provide a room address": "Please provide a room address", + "Please provide an address": "Please provide an address", "This address is available to use": "This address is available to use", "This address is already in use": "This address is already in use", "Server Options": "Server Options", From 8c34a8461ee64949b670715cafac5948bedbca57 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 7 Jun 2021 08:57:39 +0100 Subject: [PATCH 396/999] Add way to specify address during public space creation --- res/css/views/spaces/_SpaceBasicSettings.scss | 2 +- .../views/spaces/SpaceCreateMenu.tsx | 45 +++++++++++++++---- src/i18n/strings/en_EN.json | 2 + 3 files changed, 40 insertions(+), 9 deletions(-) diff --git a/res/css/views/spaces/_SpaceBasicSettings.scss b/res/css/views/spaces/_SpaceBasicSettings.scss index 204ccab2b7..e6e06e7181 100644 --- a/res/css/views/spaces/_SpaceBasicSettings.scss +++ b/res/css/views/spaces/_SpaceBasicSettings.scss @@ -16,7 +16,7 @@ limitations under the License. .mx_SpaceBasicSettings { .mx_Field { - margin: 32px 0; + margin: 24px 0; } .mx_SpaceBasicSettings_avatarContainer { diff --git a/src/components/views/spaces/SpaceCreateMenu.tsx b/src/components/views/spaces/SpaceCreateMenu.tsx index 0ebf511018..a65b53a045 100644 --- a/src/components/views/spaces/SpaceCreateMenu.tsx +++ b/src/components/views/spaces/SpaceCreateMenu.tsx @@ -33,6 +33,7 @@ import {USER_LABS_TAB} from "../dialogs/UserSettingsDialog"; import Field from "../elements/Field"; import withValidation from "../elements/Validation"; import {SpaceFeedbackPrompt} from "../../structures/SpaceRoomView"; +import RoomAliasField from "../elements/RoomAliasField"; const SpaceCreateMenuType = ({ title, description, className, onClick }) => { return ( @@ -58,6 +59,11 @@ const spaceNameValidator = withValidation({ ], }); +const nameToAlias = (name: string, domain: string): string => { + const localpart = name.trim().toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9_-]+/gi, ""); + return `#${localpart}:${domain}`; +}; + const SpaceCreateMenu = ({ onFinished }) => { const cli = useContext(MatrixClientContext); const [visibility, setVisibility] = useState(null); @@ -65,6 +71,8 @@ const SpaceCreateMenu = ({ onFinished }) => { const [name, setName] = useState(""); const spaceNameField = useRef(); + const [alias, setAlias] = useState(""); + const spaceAliasField = useRef(); const [avatar, setAvatar] = useState(null); const [topic, setTopic] = useState(""); @@ -80,6 +88,13 @@ const SpaceCreateMenu = ({ onFinished }) => { setBusy(false); return; } + // validate the space name alias field but do not require it + if (visibility === Visibility.Public && !await spaceAliasField.current.validate({ allowEmpty: true })) { + spaceAliasField.current.focus(); + spaceAliasField.current.validate({ allowEmpty: true, focused: true }); + setBusy(false); + return; + } const initialState: IStateEvent[] = [ { @@ -97,12 +112,6 @@ const SpaceCreateMenu = ({ onFinished }) => { content: { url }, }); } - if (topic) { - initialState.push({ - type: EventType.RoomTopic, - content: { topic }, - }); - } try { await createRoom({ @@ -110,7 +119,6 @@ const SpaceCreateMenu = ({ onFinished }) => { preset: visibility === Visibility.Public ? Preset.PublicChat : Preset.PrivateChat, name, creation_content: { - // Based on MSC1840 [RoomCreateTypeField]: RoomType.Space, }, initial_state: initialState, @@ -119,6 +127,8 @@ const SpaceCreateMenu = ({ onFinished }) => { events_default: 100, ...Visibility.Public ? { invite: 0 } : {}, }, + room_alias_name: alias ? alias.substr(1, alias.indexOf(":") - 1) : undefined, + topic, }, spinner: false, encryption: false, @@ -157,6 +167,7 @@ const SpaceCreateMenu = ({ onFinished }) => { ; } else { + const domain = cli.getDomain(); body = { label={_t("Name")} autoFocus={true} value={name} - onChange={ev => setName(ev.target.value)} + onChange={ev => { + const newName = ev.target.value; + if (!alias || alias === nameToAlias(name, domain)) { + setAlias(nameToAlias(newName, domain)); + } + setName(newName); + }} ref={spaceNameField} onValidate={spaceNameValidator} disabled={busy} /> + { visibility === Visibility.Public + ? + : null + } + Date: Mon, 7 Jun 2021 08:59:57 +0100 Subject: [PATCH 397/999] Stash --- src/stores/SpaceStore.tsx | 62 +++++++++++++++++++++++++-------------- 1 file changed, 40 insertions(+), 22 deletions(-) diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index 9ef961ce2d..5e09b617a7 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -61,8 +61,6 @@ const partitionSpacesAndRooms = (arr: Room[]): [Room[], Room[]] => { // [spaces, }, [[], []]); }; -const SpaceTagOrderingField = "org.matrix.mscXXXX.space"; - // For sorting space children using a validated `order`, `m.room.create`'s `origin_server_ts`, `room_id` export const getChildOrder = (order: string, creationTs: number, roomId: string): Array>> => { let validatedOrder: string = null; @@ -98,7 +96,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { private _activeSpace?: Room = null; private _suggestedRooms: ISuggestedRoom[] = []; private _invitedSpaces = new Set(); - private spaceOrderLocalEchoMap = new Map(); + private spaceOrderLocalEchoMap = new Map(); public get invitedSpaces(): Room[] { return Array.from(this._invitedSpaces); @@ -477,11 +475,11 @@ export class SpaceStoreClass extends AsyncStoreWithClient { }; private onRoomAccountData = (ev: MatrixEvent, room: Room, lastEv?: MatrixEvent) => { - if (!room.isSpaceRoom() || ev.getType() !== EventType.Tag) return; + if (!room.isSpaceRoom() || ev.getType() !== EventType.SpaceOrder) return; this.spaceOrderLocalEchoMap.delete(room.roomId); // clear any local echo - const order = ev.getContent()?.tags?.[SpaceTagOrderingField]?.order; - const lastOrder = lastEv?.getContent()?.tags?.[SpaceTagOrderingField]?.order; + const order = ev.getContent()?.order; + const lastOrder = lastEv?.getContent()?.order; if (order !== lastOrder) { this.notifyIfOrderChanged(); } @@ -625,15 +623,21 @@ export class SpaceStoreClass extends AsyncStoreWithClient { childSpaces.forEach(s => this.traverseSpace(s.roomId, fn, includeRooms, newPath)); } - private getSpaceTagOrdering = (space: Room): number | undefined => { + private getSpaceTagOrdering = (space: Room): string | undefined => { if (this.spaceOrderLocalEchoMap.has(space.roomId)) return this.spaceOrderLocalEchoMap.get(space.roomId); - return space.tags?.[SpaceTagOrderingField]?.order; + const order = space.getAccountData(EventType.SpaceOrder)?.getContent()?.order; + return typeof order === "string" ? order : undefined; }; private sortRootSpaces(spaces: Room[]): Room[] { return sortBy(spaces, [this.getSpaceTagOrdering, "roomId"]); } + private setRootSpaceOrder(space: Room, order: string): void { + this.spaceOrderLocalEchoMap.set(space.roomId, order); + this.matrixClient.setRoomAccountData(space.roomId, EventType.SpaceOrder, { order }); + } + public moveRootSpace(fromIndex: number, toIndex: number): void { if ( fromIndex < 0 || toIndex < 0 || @@ -645,29 +649,43 @@ export class SpaceStoreClass extends AsyncStoreWithClient { const space = this.rootSpaces[fromIndex]; const orders = this.rootSpaces.map(this.getSpaceTagOrdering); - let prevOrder = orders[toIndex - 1]; - let nextOrder = orders[toIndex]; // accounts for downwards displacement of existing inhabitant of this index + let prevOrder: string; + let nextOrder: string; - if (prevOrder === undefined && nextOrder === undefined) { - // TODO WHAT A PAIN + if (toIndex > fromIndex) { + prevOrder = toIndex >= 0 ? orders[toIndex] : "aaaaa"; + nextOrder = toIndex <= orders.length ? orders[toIndex + 1] : "zzzzz"; + } else { + // accounts for downwards displacement of existing inhabitant of this index + prevOrder = toIndex > 0 ? orders[toIndex - 1] : "aaaaa"; + nextOrder = toIndex < orders.length ? orders[toIndex] : "zzzzz"; } + console.log("@@ start", {fromIndex, toIndex, orders, prevOrder, nextOrder}); - prevOrder = prevOrder || 0.0; - nextOrder = nextOrder || 1.0; + if (prevOrder === undefined) { + const firstUndefinedIndex = orders.indexOf(undefined); + const numUndefined = orders.length - firstUndefinedIndex; + const lastOrder = orders[firstUndefinedIndex - 1]; + console.log("@@ precalc", {firstUndefinedIndex, numUndefined, lastOrder}); + nextOrder = lastOrder + step; + for (let i = firstUndefinedIndex; i < toIndex; i++, nextOrder += step) { + console.log("@@ preset", {i, nextOrder}); + this.setRootSpaceOrder(this.rootSpaces[i], nextOrder); + } + + prevOrder = nextOrder; + nextOrder += step; + } if (prevOrder !== nextOrder) { const order = prevOrder + ((nextOrder - prevOrder) / 2); - this.spaceOrderLocalEchoMap.set(space.roomId, order); - this.matrixClient.setRoomAccountData(space.roomId, EventType.Tag, { - tags: { - ...space.tags, - [SpaceTagOrderingField]: { order }, - }, - }); - this.notifyIfOrderChanged(); + console.log("@@ set", {prevOrder, nextOrder, order}); + this.setRootSpaceOrder(space, order); } else { // TODO REBUILD } + + this.notifyIfOrderChanged(); } } From 31d308a1fbdd94176a8a3ea43e1e20f5b41590db Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 7 Jun 2021 09:22:47 +0100 Subject: [PATCH 398/999] Fix Stickerpicker context menu --- .../views/context_menus/WidgetContextMenu.tsx | 46 ++++++++++++------- src/components/views/elements/AppTile.js | 2 + src/components/views/rooms/Stickerpicker.js | 6 +-- src/stores/widgets/WidgetLayoutStore.ts | 2 +- 4 files changed, 35 insertions(+), 21 deletions(-) diff --git a/src/components/views/context_menus/WidgetContextMenu.tsx b/src/components/views/context_menus/WidgetContextMenu.tsx index 623fe04f2f..068bfd6497 100644 --- a/src/components/views/context_menus/WidgetContextMenu.tsx +++ b/src/components/views/context_menus/WidgetContextMenu.tsx @@ -40,6 +40,8 @@ interface IProps extends React.ComponentProps { showUnpin?: boolean; // override delete handler onDeleteClick?(): void; + // override edit handler + onEditClick?(): void; } const WidgetContextMenu: React.FC = ({ @@ -47,6 +49,7 @@ const WidgetContextMenu: React.FC = ({ app, userWidget, onDeleteClick, + onEditClick, showUnpin, ...props }) => { @@ -89,12 +92,16 @@ const WidgetContextMenu: React.FC = ({ let editButton; if (canModify && WidgetUtils.isManagedByManager(app)) { - const onEditClick = () => { - WidgetUtils.editWidget(room, app); + const _onEditClick = () => { + if (onEditClick) { + onEditClick(); + } else { + WidgetUtils.editWidget(room, app); + } onFinished(); }; - editButton = ; + editButton = ; } let snapshotButton; @@ -116,24 +123,29 @@ const WidgetContextMenu: React.FC = ({ let deleteButton; if (onDeleteClick || canModify) { - const onDeleteClickDefault = () => { - // Show delete confirmation dialog - Modal.createTrackedDialog('Delete Widget', '', QuestionDialog, { - title: _t("Delete Widget"), - description: _t( - "Deleting a widget removes it for all users in this room." + - " Are you sure you want to delete this widget?"), - button: _t("Delete widget"), - onFinished: (confirmed) => { - if (!confirmed) return; - WidgetUtils.setRoomWidget(roomId, app.id); - }, - }); + const _onDeleteClick = () => { + if (onDeleteClick) { + onDeleteClick(); + } else { + // Show delete confirmation dialog + Modal.createTrackedDialog('Delete Widget', '', QuestionDialog, { + title: _t("Delete Widget"), + description: _t( + "Deleting a widget removes it for all users in this room." + + " Are you sure you want to delete this widget?"), + button: _t("Delete widget"), + onFinished: (confirmed) => { + if (!confirmed) return; + WidgetUtils.setRoomWidget(roomId, app.id); + }, + }); + } + onFinished(); }; deleteButton = ; } diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index b898ad2ebc..ad5cbfa6b3 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -417,6 +417,8 @@ export default class AppTile extends React.Component { onFinished={this._closeContextMenu} showUnpin={!this.props.userWidget} userWidget={this.props.userWidget} + onEditClick={this.props.onEditClick} + onDeleteClick={this.props.onDeleteClick} /> ); } diff --git a/src/components/views/rooms/Stickerpicker.js b/src/components/views/rooms/Stickerpicker.js index 3d2300b83c..b9aaee7a57 100644 --- a/src/components/views/rooms/Stickerpicker.js +++ b/src/components/views/rooms/Stickerpicker.js @@ -367,7 +367,7 @@ export default class Stickerpicker extends React.PureComponent { /** * Launch the integration manager on the stickers integration page */ - _launchManageIntegrations() { + _launchManageIntegrations = () => { // TODO: Open the right integration manager for the widget if (SettingsStore.getValue("feature_many_integration_managers")) { IntegrationManagers.sharedInstance().openAll( @@ -382,7 +382,7 @@ export default class Stickerpicker extends React.PureComponent { this.state.widgetId, ); } - } + }; render() { let stickerPicker; @@ -401,7 +401,7 @@ export default class Stickerpicker extends React.PureComponent { key="controls_hide_stickers" className={className} onClick={this._onHideStickersClick} - active={this.state.showStickers} + active={this.state.showStickers.toString()} title={_t("Hide Stickers")} > ; diff --git a/src/stores/widgets/WidgetLayoutStore.ts b/src/stores/widgets/WidgetLayoutStore.ts index f5734d74c5..b74da98c9c 100644 --- a/src/stores/widgets/WidgetLayoutStore.ts +++ b/src/stores/widgets/WidgetLayoutStore.ts @@ -332,7 +332,7 @@ export class WidgetLayoutStore extends ReadyWatchingStore { } public getContainerWidgets(room: Room, container: Container): IApp[] { - return this.byRoom[room.roomId]?.[container]?.ordered || []; + return this.byRoom[room?.roomId]?.[container]?.ordered || []; } public isInContainer(room: Room, widget: IApp, container: Container): boolean { From a6ddffe74c8f081e4a99ea087cfa51622a5c9769 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 7 Jun 2021 09:26:42 +0100 Subject: [PATCH 399/999] Add scroll token to file and notif event tiles --- src/components/structures/MessagePanel.js | 5 ----- src/components/views/rooms/EventTile.tsx | 22 ++++++++++++++-------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 6709fef814..19bf27b1d2 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -616,10 +616,6 @@ export default class MessagePanel extends React.Component { const eventId = mxEv.getId(); const highlight = (eventId === this.props.highlightedEventId); - // we can't use local echoes as scroll tokens, because their event IDs change. - // Local echos have a send "status". - const scrollToken = mxEv.status ? undefined : eventId; - const readReceipts = this._readReceiptsByEvent[eventId]; let isLastSuccessful = false; @@ -651,7 +647,6 @@ export default class MessagePanel extends React.Component { { permalink = this.props.permalinkCreator.forEvent(this.props.mxEvent.getId()); } + const scrollToken = this.props.mxEvent.status + ? undefined + : this.props.mxEvent.getId(); + let avatar; let sender; let avatarSize; @@ -1046,7 +1050,7 @@ export default class EventTile extends React.Component { case 'notif': { const room = this.context.getRoom(this.props.mxEvent.getRoomId()); return ( - + ); } case 'file_grid': { return ( - + ); } @@ -1111,7 +1115,7 @@ export default class EventTile extends React.Component { ); } return ( -
      +
    13. { ircTimestamp } { avatar } { sender } @@ -1129,7 +1133,7 @@ export default class EventTile extends React.Component { showUrlPreview={false} />
    14. -
      + ); } default: { @@ -1143,13 +1147,15 @@ export default class EventTile extends React.Component { // tab-index=-1 to allow it to be focusable but do not add tab stop for it, primarily for screen readers return ( - React.createElement(this.props.as || "div", { + React.createElement(this.props.as || "li", { "ref": this.ref, "className": classes, "tabIndex": -1, "aria-live": ariaLive, "aria-atomic": "true", - "data-scroll-tokens": this.props["data-scroll-tokens"], + // we can't use local echoes as scroll tokens, because their event IDs change. + // Local echos have a send "status". + "data-scroll-tokens": scrollToken, "onMouseEnter": () => this.setState({ hover: true }), "onMouseLeave": () => this.setState({ hover: false }), }, [ From d2ce670d3ec2c4c01653b3150d9b1d610bdec9e7 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 7 Jun 2021 10:56:10 +0100 Subject: [PATCH 400/999] Remove unused code --- src/utils/Accessibility.js | 28 ---------------------------- 1 file changed, 28 deletions(-) delete mode 100644 src/utils/Accessibility.js diff --git a/src/utils/Accessibility.js b/src/utils/Accessibility.js deleted file mode 100644 index f4909f971b..0000000000 --- a/src/utils/Accessibility.js +++ /dev/null @@ -1,28 +0,0 @@ -/* -Copyright 2019 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. -*/ - -/** - * Automatically focuses the captured reference when receiving a non-null - * object. Useful in scenarios where componentDidMount does not have a - * useful reference to an element, but one needs to focus the element on - * first render. Example usage: ref={focusCapturedRef} - * @param {function} ref The React reference to focus on, if not null - */ -export function focusCapturedRef(ref) { - if (ref) { - ref.focus(); - } -} From 9315a87ebfa9adacb0cf638fca65a687378c9b43 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 7 Jun 2021 10:57:11 +0100 Subject: [PATCH 401/999] Convert EventIndex to Typescript --- src/indexing/BaseEventIndexManager.ts | 10 +- src/indexing/{EventIndex.js => EventIndex.ts} | 177 ++++++++++-------- 2 files changed, 106 insertions(+), 81 deletions(-) rename src/indexing/{EventIndex.js => EventIndex.ts} (87%) diff --git a/src/indexing/BaseEventIndexManager.ts b/src/indexing/BaseEventIndexManager.ts index 6349f31524..debcb213ca 100644 --- a/src/indexing/BaseEventIndexManager.ts +++ b/src/indexing/BaseEventIndexManager.ts @@ -35,7 +35,7 @@ export interface MatrixProfile { export interface CrawlerCheckpoint { roomId: string; token: string; - fullCrawl: boolean; + fullCrawl?: boolean; direction: string; } @@ -73,14 +73,14 @@ export interface EventAndProfile { export interface LoadArgs { roomId: string; limit: number; - fromEvent: string; - direction: string; + fromEvent?: string; + direction?: string; } export interface IndexStats { size: number; - event_count: number; - room_count: number; + eventCount: number; + roomCount: number; } /** diff --git a/src/indexing/EventIndex.js b/src/indexing/EventIndex.ts similarity index 87% rename from src/indexing/EventIndex.js rename to src/indexing/EventIndex.ts index 33f2d594ae..b6289969bd 100644 --- a/src/indexing/EventIndex.js +++ b/src/indexing/EventIndex.ts @@ -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,33 +14,42 @@ See the License for the specific language governing permissions and 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 { 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 { RoomState } from 'matrix-js-sdk/src/models/room-state'; +import { TimelineWindow } from 'matrix-js-sdk/src/timeline-window'; + import PlatformPeg from "../PlatformPeg"; -import {MatrixClientPeg} from "../MatrixClientPeg"; -import {RoomMember} from 'matrix-js-sdk/src/models/room-member'; -import {EventTimeline} from 'matrix-js-sdk/src/models/event-timeline'; -import {sleep} from "../utils/promise"; +import { MatrixClientPeg } from "../MatrixClientPeg"; +import { sleep } from "../utils/promise"; import SettingsStore from "../settings/SettingsStore"; -import {EventEmitter} from "events"; -import {SettingLevel} from "../settings/SettingLevel"; +import { SettingLevel } from "../settings/SettingLevel"; +import {CrawlerCheckpoint, LoadArgs, SearchArgs} from "./BaseEventIndexManager"; + +// The time in ms that the crawler will wait loop iterations if there +// have not been any checkpoints to consume in the last iteration. +const CRAWLER_IDLE_TIME = 5000; + +// The maximum number of events our crawler should fetch in a single crawl. +const EVENTS_PER_CRAWL = 100; + +interface ICrawler { + cancel(): void; +} /* * Event indexing class that wraps the platform specific event indexing. */ export default class EventIndex extends EventEmitter { - constructor() { - super(); - this.crawlerCheckpoints = []; - // The time in ms that the crawler will wait loop iterations if there - // have not been any checkpoints to consume in the last iteration. - this._crawlerIdleTime = 5000; - // The maximum number of events our crawler should fetch in a single - // crawl. - this._eventsPerCrawl = 100; - this._crawler = null; - this._currentCheckpoint = null; - } + private crawlerCheckpoints: CrawlerCheckpoint[] = []; + private crawler: ICrawler = null; + private currentCheckpoint: CrawlerCheckpoint = null; - async init() { + public async init() { const indexManager = PlatformPeg.get().getEventIndexingManager(); this.crawlerCheckpoints = await indexManager.loadCheckpoints(); @@ -52,7 +61,7 @@ export default class EventIndex extends EventEmitter { /** * Register event listeners that are necessary for the event index to work. */ - registerListeners() { + public registerListeners() { const client = MatrixClientPeg.get(); client.on('sync', this.onSync); @@ -66,7 +75,7 @@ export default class EventIndex extends EventEmitter { /** * Remove the event index specific event listeners. */ - removeListeners() { + public removeListeners() { const client = MatrixClientPeg.get(); if (client === null) return; @@ -81,7 +90,7 @@ export default class EventIndex extends EventEmitter { /** * Get crawler checkpoints for the encrypted rooms and store them in the index. */ - async addInitialCheckpoints() { + public async addInitialCheckpoints() { const indexManager = PlatformPeg.get().getEventIndexingManager(); const client = MatrixClientPeg.get(); const rooms = client.getRooms(); @@ -102,14 +111,14 @@ export default class EventIndex extends EventEmitter { const timeline = room.getLiveTimeline(); const token = timeline.getPaginationToken("b"); - const backCheckpoint = { + const backCheckpoint: CrawlerCheckpoint = { roomId: room.roomId, token: token, direction: "b", fullCrawl: true, }; - const forwardCheckpoint = { + const forwardCheckpoint: CrawlerCheckpoint = { roomId: room.roomId, token: token, direction: "f", @@ -146,7 +155,7 @@ export default class EventIndex extends EventEmitter { * - Every other sync, tell the event index to commit all the queued up * live events */ - onSync = async (state, prevState, data) => { + private onSync = async (state: string, prevState: string, data: object) => { const indexManager = PlatformPeg.get().getEventIndexingManager(); if (prevState === "PREPARED" && state === "SYNCING") { @@ -176,7 +185,15 @@ export default class EventIndex extends EventEmitter { * otherwise we save their event id and wait for them in the Event.decrypted * listener. */ - onRoomTimeline = async (ev, room, toStartOfTimeline, removed, data) => { + private onRoomTimeline = async ( + ev: MatrixEvent, + room: Room, + toStartOfTimeline: boolean, + removed: boolean, + data: { + liveEvent: boolean; + }, + ) => { const client = MatrixClientPeg.get(); // We only index encrypted rooms locally. @@ -194,7 +211,7 @@ export default class EventIndex extends EventEmitter { await this.addLiveEventToIndex(ev); } - onRoomStateEvent = async (ev, state) => { + private onRoomStateEvent = async (ev: MatrixEvent, state: RoomState) => { if (!MatrixClientPeg.get().isRoomEncrypted(state.roomId)) return; if (ev.getType() === "m.room.encryption" && !await this.isRoomIndexed(state.roomId)) { @@ -209,7 +226,7 @@ export default class EventIndex extends EventEmitter { * Checks if the event was marked for addition in the Room.timeline * listener, if so queues it up to be added to the index. */ - onEventDecrypted = async (ev, err) => { + private onEventDecrypted = async (ev: MatrixEvent, err: Error) => { // If the event isn't in our live event set, ignore it. if (err) return; await this.addLiveEventToIndex(ev); @@ -220,7 +237,7 @@ export default class EventIndex extends EventEmitter { * * Removes a redacted event from our event index. */ - onRedaction = async (ev, room) => { + private onRedaction = async (ev: MatrixEvent, room: Room) => { // We only index encrypted rooms locally. if (!MatrixClientPeg.get().isRoomEncrypted(room.roomId)) return; const indexManager = PlatformPeg.get().getEventIndexingManager(); @@ -238,7 +255,7 @@ export default class EventIndex extends EventEmitter { * Listens for timeline resets that are caused by a limited timeline to * re-add checkpoints for rooms that need to be crawled again. */ - onTimelineReset = async (room, timelineSet, resetAllTimelines) => { + private onTimelineReset = async (room: Room, timelineSet: EventTimelineSet, resetAllTimelines: boolean) => { if (room === null) return; if (!MatrixClientPeg.get().isRoomEncrypted(room.roomId)) return; @@ -258,7 +275,7 @@ export default class EventIndex extends EventEmitter { * @returns {bool} Returns true if the event can be indexed, false * otherwise. */ - isValidEvent(ev) { + private isValidEvent(ev: MatrixEvent) { const isUsefulType = ["m.room.message", "m.room.name", "m.room.topic"].includes(ev.getType()); const validEventType = isUsefulType && !ev.isRedacted() && !ev.isDecryptionFailure(); @@ -282,7 +299,7 @@ export default class EventIndex extends EventEmitter { return validEventType && validMsgType && hasContentValue; } - eventToJson(ev) { + private eventToJson(ev: MatrixEvent) { const jsonEvent = ev.toJSON(); const e = ev.isEncrypted() ? jsonEvent.decrypted : jsonEvent; @@ -314,7 +331,7 @@ export default class EventIndex extends EventEmitter { * * @param {MatrixEvent} ev The event that should be added to the index. */ - async addLiveEventToIndex(ev) { + private async addLiveEventToIndex(ev: MatrixEvent) { const indexManager = PlatformPeg.get().getEventIndexingManager(); if (!this.isValidEvent(ev)) return; @@ -333,11 +350,11 @@ export default class EventIndex extends EventEmitter { * Emmit that the crawler has changed the checkpoint that it's currently * handling. */ - emitNewCheckpoint() { + private emitNewCheckpoint() { this.emit("changedCheckpoint", this.currentRoom()); } - async addEventsFromLiveTimeline(timeline) { + private async addEventsFromLiveTimeline(timeline: EventTimeline) { const events = timeline.getEvents(); for (let i = 0; i < events.length; i++) { @@ -346,7 +363,7 @@ export default class EventIndex extends EventEmitter { } } - async addRoomCheckpoint(roomId, fullCrawl = false) { + private async addRoomCheckpoint(roomId: string, fullCrawl = false) { const indexManager = PlatformPeg.get().getEventIndexingManager(); const client = MatrixClientPeg.get(); const room = client.getRoom(roomId); @@ -396,16 +413,16 @@ export default class EventIndex extends EventEmitter { * crawl, otherwise create a new checkpoint and push it to the * crawlerCheckpoints queue so we go through them in a round-robin way. */ - async crawlerFunc() { + private async crawlerFunc() { let cancelled = false; const client = MatrixClientPeg.get(); const indexManager = PlatformPeg.get().getEventIndexingManager(); - this._crawler = {}; - - this._crawler.cancel = () => { - cancelled = true; + this.crawler = { + cancel: () => { + cancelled = true; + }, }; let idle = false; @@ -417,11 +434,11 @@ export default class EventIndex extends EventEmitter { sleepTime = Math.max(sleepTime, 100); if (idle) { - sleepTime = this._crawlerIdleTime; + sleepTime = CRAWLER_IDLE_TIME; } - if (this._currentCheckpoint !== null) { - this._currentCheckpoint = null; + if (this.currentCheckpoint !== null) { + this.currentCheckpoint = null; this.emitNewCheckpoint(); } @@ -440,7 +457,7 @@ export default class EventIndex extends EventEmitter { continue; } - this._currentCheckpoint = checkpoint; + this.currentCheckpoint = checkpoint; this.emitNewCheckpoint(); idle = false; @@ -454,8 +471,11 @@ export default class EventIndex extends EventEmitter { try { res = await client.createMessagesRequest( - checkpoint.roomId, checkpoint.token, this._eventsPerCrawl, - checkpoint.direction); + checkpoint.roomId, + checkpoint.token, + EVENTS_PER_CRAWL, + checkpoint.direction, + ); } catch (e) { if (e.httpStatus === 403) { console.log("EventIndex: Removing checkpoint as we don't have ", @@ -612,23 +632,23 @@ export default class EventIndex extends EventEmitter { } } - this._crawler = null; + this.crawler = null; } /** * Start the crawler background task. */ - startCrawler() { - if (this._crawler !== null) return; + public startCrawler() { + if (this.crawler !== null) return; this.crawlerFunc(); } /** * Stop the crawler background task. */ - stopCrawler() { - if (this._crawler === null) return; - this._crawler.cancel(); + public stopCrawler() { + if (this.crawler === null) return; + this.crawler.cancel(); } /** @@ -637,7 +657,7 @@ export default class EventIndex extends EventEmitter { * This removes all the MatrixClient event listeners, stops the crawler * task, and closes the index. */ - async close() { + public async close() { const indexManager = PlatformPeg.get().getEventIndexingManager(); this.removeListeners(); this.stopCrawler(); @@ -654,7 +674,7 @@ export default class EventIndex extends EventEmitter { * @return {Promise<[SearchResult]>} A promise that will resolve to an array * of search results once the search is done. */ - async search(searchArgs) { + public async search(searchArgs: SearchArgs) { const indexManager = PlatformPeg.get().getEventIndexingManager(); return indexManager.searchEventIndex(searchArgs); } @@ -680,11 +700,16 @@ export default class EventIndex extends EventEmitter { * @returns {Promise} Resolves to an array of events that * contain URLs. */ - async loadFileEvents(room, limit = 10, fromEvent = null, direction = EventTimeline.BACKWARDS) { + public async loadFileEvents( + room: Room, + limit = 10, + fromEvent: string = null, + direction: string = EventTimeline.BACKWARDS, + ) { const client = MatrixClientPeg.get(); const indexManager = PlatformPeg.get().getEventIndexingManager(); - const loadArgs = { + const loadArgs: LoadArgs = { roomId: room.roomId, limit: limit, }; @@ -772,13 +797,13 @@ export default class EventIndex extends EventEmitter { * @returns {Promise} Resolves to true if events were added to the * timeline, false otherwise. */ - async populateFileTimeline( - timelineSet, - timeline, - room, + public async populateFileTimeline( + timelineSet: EventTimelineSet, + timeline: EventTimeline, + room: Room, limit = 10, - fromEvent = null, - direction = EventTimeline.BACKWARDS, + fromEvent: string = null, + direction: string = EventTimeline.BACKWARDS, ) { const matrixEvents = await this.loadFileEvents(room, limit, fromEvent, direction); @@ -837,7 +862,7 @@ export default class EventIndex extends EventEmitter { * @returns {Promise} Resolves to a boolean which is true if more * events were successfully retrieved. */ - paginateTimelineWindow(room, timelineWindow, direction, limit) { + public paginateTimelineWindow(room: Room, timelineWindow: TimelineWindow, direction: string, limit: number) { const tl = timelineWindow.getTimelineIndex(direction); if (!tl) return Promise.resolve(false); @@ -871,7 +896,7 @@ export default class EventIndex extends EventEmitter { * @return {Promise} A promise that will resolve to the index * statistics. */ - async getStats() { + public async getStats() { const indexManager = PlatformPeg.get().getEventIndexingManager(); return indexManager.getStats(); } @@ -885,7 +910,7 @@ export default class EventIndex extends EventEmitter { * @return {Promise} Returns true if the index contains events for * the given room, false otherwise. */ - async isRoomIndexed(roomId) { + public async isRoomIndexed(roomId) { const indexManager = PlatformPeg.get().getEventIndexingManager(); return indexManager.isRoomIndexed(roomId); } @@ -896,21 +921,21 @@ export default class EventIndex extends EventEmitter { * @returns {Room} A MatrixRoom that is being currently crawled, null * if no room is currently being crawled. */ - currentRoom() { - if (this._currentCheckpoint === null && this.crawlerCheckpoints.length === 0) { + public currentRoom() { + if (this.currentCheckpoint === null && this.crawlerCheckpoints.length === 0) { return null; } const client = MatrixClientPeg.get(); - if (this._currentCheckpoint !== null) { - return client.getRoom(this._currentCheckpoint.roomId); + if (this.currentCheckpoint !== null) { + return client.getRoom(this.currentCheckpoint.roomId); } else { return client.getRoom(this.crawlerCheckpoints[0].roomId); } } - crawlingRooms() { + public crawlingRooms() { const totalRooms = new Set(); const crawlingRooms = new Set(); @@ -918,14 +943,14 @@ export default class EventIndex extends EventEmitter { crawlingRooms.add(checkpoint.roomId); }); - if (this._currentCheckpoint !== null) { - crawlingRooms.add(this._currentCheckpoint.roomId); + if (this.currentCheckpoint !== null) { + crawlingRooms.add(this.currentCheckpoint.roomId); } const client = MatrixClientPeg.get(); const rooms = client.getRooms(); - const isRoomEncrypted = (room) => { + const isRoomEncrypted = (room: Room) => { return client.isRoomEncrypted(room.roomId); }; @@ -934,6 +959,6 @@ export default class EventIndex extends EventEmitter { totalRooms.add(room.roomId); }); - return {crawlingRooms, totalRooms}; + return { crawlingRooms, totalRooms }; } } From 3431ebb9957cb8424eb2ee32b4f87d445a6daf6b Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 7 Jun 2021 10:57:25 +0100 Subject: [PATCH 402/999] Convert AsyncWrapper to Typescript --- src/{AsyncWrapper.js => AsyncWrapper.tsx} | 63 ++++++++++++----------- 1 file changed, 34 insertions(+), 29 deletions(-) rename src/{AsyncWrapper.js => AsyncWrapper.tsx} (59%) diff --git a/src/AsyncWrapper.js b/src/AsyncWrapper.tsx similarity index 59% rename from src/AsyncWrapper.js rename to src/AsyncWrapper.tsx index 359828b312..1d3c42e60b 100644 --- a/src/AsyncWrapper.js +++ b/src/AsyncWrapper.tsx @@ -1,6 +1,5 @@ /* -Copyright 2015, 2016 OpenMarket Ltd -Copyright 2020 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. @@ -15,52 +14,63 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from "react"; -import * as sdk from './index'; -import PropTypes from 'prop-types'; +import React, { ComponentType } from "react"; + import { _t } from './languageHandler'; +import { IDialogProps } from "./components/views/dialogs/IDialogProps"; +import BaseDialog from "./components/views/dialogs/BaseDialog"; +import DialogButtons from "./components/views/elements/DialogButtons"; +import Spinner from "./components/views/elements/Spinner"; + +type AsyncImport = { default: T }; + +interface IProps extends IDialogProps { + // A promise which resolves with the real component + prom: Promise>; +} + +interface IState { + component?: ComponentType; + error?: Error; +} /** * Wrap an asynchronous loader function with a react component which shows a * spinner until the real component loads. */ -export default class AsyncWrapper extends React.Component { - static propTypes = { - /** A promise which resolves with the real component - */ - prom: PropTypes.object.isRequired, - }; +export default class AsyncWrapper extends React.Component { + private unmounted = false; - state = { + public state = { component: null, error: null, }; componentDidMount() { - this._unmounted = false; // XXX: temporary logging to try to diagnose // https://github.com/vector-im/element-web/issues/3148 console.log('Starting load of AsyncWrapper for modal'); this.props.prom.then((result) => { - if (this._unmounted) { - return; - } + if (this.unmounted) return; + // Take the 'default' member if it's there, then we support // passing in just an import()ed module, since ES6 async import // always returns a module *namespace*. - const component = result.default ? result.default : result; - this.setState({component}); + const component = (result as AsyncImport).default + ? (result as AsyncImport).default + : result as ComponentType; + this.setState({ component }); }).catch((e) => { console.warn('AsyncWrapper promise failed', e); - this.setState({error: e}); + this.setState({ error: e }); }); } componentWillUnmount() { - this._unmounted = true; + this.unmounted = true; } - _onWrapperCancelClick = () => { + private onWrapperCancelClick = () => { this.props.onFinished(false); }; @@ -69,20 +79,15 @@ 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.")} + 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 ; } } From b2d9dd7214c5f72add24d79c32b763b7c916187c Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 7 Jun 2021 10:57:39 +0100 Subject: [PATCH 403/999] Convert LifecycleStore to Typescript --- .../{LifecycleStore.js => LifecycleStore.ts} | 50 ++++++++++--------- 1 file changed, 27 insertions(+), 23 deletions(-) rename src/stores/{LifecycleStore.js => LifecycleStore.ts} (62%) diff --git a/src/stores/LifecycleStore.js b/src/stores/LifecycleStore.ts similarity index 62% rename from src/stores/LifecycleStore.js rename to src/stores/LifecycleStore.ts index a12bac7dd6..5381fc58ed 100644 --- a/src/stores/LifecycleStore.js +++ b/src/stores/LifecycleStore.ts @@ -1,7 +1,5 @@ /* -Copyright 2017 Vector Creations Ltd -Copyright 2018 New Vector Ltd -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2017-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,11 +13,18 @@ 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 { Store } from 'flux/utils'; + import dis from '../dispatcher/dispatcher'; -import {Store} from 'flux/utils'; +import { ActionPayload } from "../dispatcher/payloads"; + +interface IState { + deferredAction: any; +} const INITIAL_STATE = { - deferred_action: null, + deferredAction: null, }; /** @@ -27,39 +32,38 @@ const INITIAL_STATE = { * store that listens for actions and updates its state accordingly, informing any * listeners (views) of state changes. */ -class LifecycleStore extends Store { +class LifecycleStore extends Store { + private state: IState = INITIAL_STATE; + constructor() { super(dis); - - // Initialise state - this._state = INITIAL_STATE; } - _setState(newState) { - this._state = Object.assign(this._state, newState); + private setState(newState: Partial) { + this.state = Object.assign(this.state, newState); this.__emitChange(); } - __onDispatch(payload) { + protected __onDispatch(payload: ActionPayload) { switch (payload.action) { case 'do_after_sync_prepared': - this._setState({ - deferred_action: payload.deferred_action, + this.setState({ + deferredAction: payload.deferred_action, }); break; case 'cancel_after_sync_prepared': - this._setState({ - deferred_action: null, + this.setState({ + deferredAction: null, }); break; - case 'sync_state': { + case 'syncstate': { if (payload.state !== 'PREPARED') { break; } - if (!this._state.deferred_action) break; - const deferredAction = Object.assign({}, this._state.deferred_action); - this._setState({ - deferred_action: null, + if (!this.state.deferredAction) break; + const deferredAction = Object.assign({}, this.state.deferredAction); + this.setState({ + deferredAction: null, }); dis.dispatch(deferredAction); break; @@ -71,8 +75,8 @@ class LifecycleStore extends Store { } } - reset() { - this._state = Object.assign({}, INITIAL_STATE); + private reset() { + this.state = Object.assign({}, INITIAL_STATE); } } From c8b3acb7e4a23d5cd654a2d7234d9f3a5b62405d Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 7 Jun 2021 10:57:46 +0100 Subject: [PATCH 404/999] Convert utils to Typescript --- src/utils/{UrlUtils.js => UrlUtils.ts} | 6 +++--- src/utils/{humanize.js => humanize.ts} | 22 +++++++++++----------- 2 files changed, 14 insertions(+), 14 deletions(-) rename src/utils/{UrlUtils.js => UrlUtils.ts} (88%) rename src/utils/{humanize.js => humanize.ts} (83%) diff --git a/src/utils/UrlUtils.js b/src/utils/UrlUtils.ts similarity index 88% rename from src/utils/UrlUtils.js rename to src/utils/UrlUtils.ts index 7b207c128e..6f441ff98e 100644 --- a/src/utils/UrlUtils.js +++ b/src/utils/UrlUtils.ts @@ -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. @@ -22,7 +22,7 @@ import url from "url"; * @param {string} u The url to be abbreviated * @returns {string} The abbreviated url */ -export function abbreviateUrl(u) { +export function abbreviateUrl(u: string): string { if (!u) return ''; const parsedUrl = url.parse(u); @@ -37,7 +37,7 @@ export function abbreviateUrl(u) { return u; } -export function unabbreviateUrl(u) { +export function unabbreviateUrl(u: string): string { if (!u) return ''; let longUrl = u; diff --git a/src/utils/humanize.js b/src/utils/humanize.ts similarity index 83% rename from src/utils/humanize.js rename to src/utils/humanize.ts index 6e6f17f8f4..55bdbf760b 100644 --- a/src/utils/humanize.js +++ b/src/utils/humanize.ts @@ -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. @@ -14,22 +14,22 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {_t} from "../languageHandler"; +import { _t } from "../languageHandler"; + +// These are the constants we use for when to break the text +const MILLISECONDS_RECENT = 15000; +const MILLISECONDS_1_MIN = 75000; +const MINUTES_UNDER_1_HOUR = 45; +const MINUTES_1_HOUR = 75; +const HOURS_UNDER_1_DAY = 23; +const HOURS_1_DAY = 26; /** * Converts a timestamp into human-readable, translated, text. * @param {number} timeMillis The time in millis to compare against. * @returns {string} The humanized time. */ -export function humanizeTime(timeMillis) { - // These are the constants we use for when to break the text - const MILLISECONDS_RECENT = 15000; - const MILLISECONDS_1_MIN = 75000; - const MINUTES_UNDER_1_HOUR = 45; - const MINUTES_1_HOUR = 75; - const HOURS_UNDER_1_DAY = 23; - const HOURS_1_DAY = 26; - +export function humanizeTime(timeMillis: number): string { const now = (new Date()).getTime(); let msAgo = now - timeMillis; const minutes = Math.abs(Math.ceil(msAgo / 60000)); From 50d3e04bdabc2cfd6d23381f18de11d4c7ef1287 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 7 Jun 2021 10:58:22 +0100 Subject: [PATCH 405/999] Convert MatrixActionCreators to Typescript --- ...ionCreators.js => MatrixActionCreators.ts} | 125 +++++++++++------- 1 file changed, 77 insertions(+), 48 deletions(-) rename src/actions/{MatrixActionCreators.js => MatrixActionCreators.ts} (68%) diff --git a/src/actions/MatrixActionCreators.js b/src/actions/MatrixActionCreators.ts similarity index 68% rename from src/actions/MatrixActionCreators.js rename to src/actions/MatrixActionCreators.ts index 93a4fcf07c..33e72becd2 100644 --- a/src/actions/MatrixActionCreators.js +++ b/src/actions/MatrixActionCreators.ts @@ -1,5 +1,5 @@ /* -Copyright 2017 New Vector Ltd +Copyright 2017, 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,7 +14,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -import dis from '../dispatcher/dispatcher'; +import { MatrixClient } from "matrix-js-sdk/src/client"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { Room } from "matrix-js-sdk/src/models/room"; +import { EventTimeline } from "matrix-js-sdk/src/models/event-timeline"; + +import dis from "../dispatcher/dispatcher"; +import {ActionPayload} from "../dispatcher/payloads"; // TODO: migrate from sync_state to MatrixActions.sync so that more js-sdk events // become dispatches in the same place. @@ -27,7 +33,7 @@ import dis from '../dispatcher/dispatcher'; * @param {string} prevState the previous sync state. * @returns {Object} an action of type MatrixActions.sync. */ -function createSyncAction(matrixClient, state, prevState) { +function createSyncAction(matrixClient: MatrixClient, state: string, prevState: string): ActionPayload { return { action: 'MatrixActions.sync', state, @@ -53,7 +59,7 @@ function createSyncAction(matrixClient, state, prevState) { * @param {MatrixEvent} accountDataEvent the account data event. * @returns {AccountDataAction} an action of type MatrixActions.accountData. */ -function createAccountDataAction(matrixClient, accountDataEvent) { +function createAccountDataAction(matrixClient: MatrixClient, accountDataEvent: MatrixEvent): ActionPayload { return { action: 'MatrixActions.accountData', event: accountDataEvent, @@ -81,7 +87,11 @@ function createAccountDataAction(matrixClient, accountDataEvent) { * @param {Room} room the room where account data was changed * @returns {RoomAccountDataAction} an action of type MatrixActions.Room.accountData. */ -function createRoomAccountDataAction(matrixClient, accountDataEvent, room) { +function createRoomAccountDataAction( + matrixClient: MatrixClient, + accountDataEvent: MatrixEvent, + room: Room, +): ActionPayload { return { action: 'MatrixActions.Room.accountData', event: accountDataEvent, @@ -106,7 +116,7 @@ function createRoomAccountDataAction(matrixClient, accountDataEvent, room) { * @param {Room} room the Room that was stored. * @returns {RoomAction} an action of type `MatrixActions.Room`. */ -function createRoomAction(matrixClient, room) { +function createRoomAction(matrixClient: MatrixClient, room: Room): ActionPayload { return { action: 'MatrixActions.Room', room }; } @@ -127,7 +137,7 @@ function createRoomAction(matrixClient, room) { * @param {Room} room the Room whose tags were changed. * @returns {RoomTagsAction} an action of type `MatrixActions.Room.tags`. */ -function createRoomTagsAction(matrixClient, roomTagsEvent, room) { +function createRoomTagsAction(matrixClient: MatrixClient, roomTagsEvent: MatrixEvent, room: Room): ActionPayload { return { action: 'MatrixActions.Room.tags', room }; } @@ -140,7 +150,7 @@ function createRoomTagsAction(matrixClient, roomTagsEvent, room) { * @param {Room} room the room the receipt happened in. * @returns {Object} an action of type MatrixActions.Room.receipt. */ -function createRoomReceiptAction(matrixClient, event, room) { +function createRoomReceiptAction(matrixClient: MatrixClient, event: MatrixEvent, room: Room): ActionPayload { return { action: 'MatrixActions.Room.receipt', event, @@ -178,7 +188,17 @@ function createRoomReceiptAction(matrixClient, event, room) { * @param {EventTimeline} data.timeline the timeline being altered. * @returns {RoomTimelineAction} an action of type `MatrixActions.Room.timeline`. */ -function createRoomTimelineAction(matrixClient, timelineEvent, room, toStartOfTimeline, removed, data) { +function createRoomTimelineAction( + matrixClient: MatrixClient, + timelineEvent: MatrixEvent, + room: Room, + toStartOfTimeline: boolean, + removed: boolean, + data: { + liveEvent: boolean; + timeline: EventTimeline; + }, +): ActionPayload { return { action: 'MatrixActions.Room.timeline', event: timelineEvent, @@ -208,8 +228,13 @@ function createRoomTimelineAction(matrixClient, timelineEvent, room, toStartOfTi * @param {string} oldMembership the previous membership, can be null. * @returns {RoomMembershipAction} an action of type `MatrixActions.Room.myMembership`. */ -function createSelfMembershipAction(matrixClient, room, membership, oldMembership) { - return { action: 'MatrixActions.Room.myMembership', room, membership, oldMembership}; +function createSelfMembershipAction( + matrixClient: MatrixClient, + room: Room, + membership: string, + oldMembership: string, +): ActionPayload { + return { action: 'MatrixActions.Room.myMembership', room, membership, oldMembership }; } /** @@ -228,61 +253,65 @@ function createSelfMembershipAction(matrixClient, room, membership, oldMembershi * @param {MatrixEvent} event the matrix event that was decrypted. * @returns {EventDecryptedAction} an action of type `MatrixActions.Event.decrypted`. */ -function createEventDecryptedAction(matrixClient, event) { +function createEventDecryptedAction(matrixClient: MatrixClient, event: MatrixEvent): ActionPayload { return { action: 'MatrixActions.Event.decrypted', event }; } +type Listener = () => void; +type ActionCreator = (matrixClient: MatrixClient, ...args: any) => ActionPayload; + +// A list of callbacks to call to unregister all listeners added +let matrixClientListenersStop: Listener[] = []; + +/** + * Start listening to events of type eventName on matrixClient and when they are emitted, + * dispatch an action created by the actionCreator function. + * @param {MatrixClient} matrixClient a MatrixClient to register a listener with. + * @param {string} eventName the event to listen to on MatrixClient. + * @param {function} actionCreator a function that should return an action to dispatch + * when given the MatrixClient as an argument as well as + * arguments emitted in the MatrixClient event. + */ +function addMatrixClientListener(matrixClient: MatrixClient, eventName: string, actionCreator: ActionCreator): void { + const listener: Listener = (...args) => { + const payload = actionCreator(matrixClient, ...args); + if (payload) { + dis.dispatch(payload, true); + } + }; + matrixClient.on(eventName, listener); + matrixClientListenersStop.push(() => { + matrixClient.removeListener(eventName, listener); + }); +} + /** * This object is responsible for dispatching actions when certain events are emitted by * the given MatrixClient. */ export default { - // A list of callbacks to call to unregister all listeners added - _matrixClientListenersStop: [], - /** * Start listening to certain events from the MatrixClient and dispatch actions when * they are emitted. * @param {MatrixClient} matrixClient the MatrixClient to listen to events from */ - start(matrixClient) { - this._addMatrixClientListener(matrixClient, 'sync', createSyncAction); - this._addMatrixClientListener(matrixClient, 'accountData', createAccountDataAction); - this._addMatrixClientListener(matrixClient, 'Room.accountData', createRoomAccountDataAction); - this._addMatrixClientListener(matrixClient, 'Room', createRoomAction); - this._addMatrixClientListener(matrixClient, 'Room.tags', createRoomTagsAction); - this._addMatrixClientListener(matrixClient, 'Room.receipt', createRoomReceiptAction); - this._addMatrixClientListener(matrixClient, 'Room.timeline', createRoomTimelineAction); - this._addMatrixClientListener(matrixClient, 'Room.myMembership', createSelfMembershipAction); - this._addMatrixClientListener(matrixClient, 'Event.decrypted', createEventDecryptedAction); - }, - - /** - * Start listening to events of type eventName on matrixClient and when they are emitted, - * dispatch an action created by the actionCreator function. - * @param {MatrixClient} matrixClient a MatrixClient to register a listener with. - * @param {string} eventName the event to listen to on MatrixClient. - * @param {function} actionCreator a function that should return an action to dispatch - * when given the MatrixClient as an argument as well as - * arguments emitted in the MatrixClient event. - */ - _addMatrixClientListener(matrixClient, eventName, actionCreator) { - const listener = (...args) => { - const payload = actionCreator(matrixClient, ...args); - if (payload) { - dis.dispatch(payload, true); - } - }; - matrixClient.on(eventName, listener); - this._matrixClientListenersStop.push(() => { - matrixClient.removeListener(eventName, listener); - }); + start(matrixClient: MatrixClient) { + addMatrixClientListener(matrixClient, 'sync', createSyncAction); + addMatrixClientListener(matrixClient, 'accountData', createAccountDataAction); + addMatrixClientListener(matrixClient, 'Room.accountData', createRoomAccountDataAction); + addMatrixClientListener(matrixClient, 'Room', createRoomAction); + addMatrixClientListener(matrixClient, 'Room.tags', createRoomTagsAction); + addMatrixClientListener(matrixClient, 'Room.receipt', createRoomReceiptAction); + addMatrixClientListener(matrixClient, 'Room.timeline', createRoomTimelineAction); + addMatrixClientListener(matrixClient, 'Room.myMembership', createSelfMembershipAction); + addMatrixClientListener(matrixClient, 'Event.decrypted', createEventDecryptedAction); }, /** * Stop listening to events. */ stop() { - this._matrixClientListenersStop.forEach((stopListener) => stopListener()); + matrixClientListenersStop.forEach((stopListener) => stopListener()); + matrixClientListenersStop = []; }, }; From f6e4943bef403230b0d99d4d0c7a07b1b138978f Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 7 Jun 2021 11:01:38 +0100 Subject: [PATCH 406/999] Convert RoomAliasCache to Typescript --- src/{RoomAliasCache.js => RoomAliasCache.ts} | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) rename src/{RoomAliasCache.js => RoomAliasCache.ts} (81%) diff --git a/src/RoomAliasCache.js b/src/RoomAliasCache.ts similarity index 81% rename from src/RoomAliasCache.js rename to src/RoomAliasCache.ts index bb511ba4d7..c318db2d3f 100644 --- a/src/RoomAliasCache.js +++ b/src/RoomAliasCache.ts @@ -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. @@ -24,12 +24,12 @@ limitations under the License. * A similar thing could also be achieved via `pushState` with a state object, * but keeping it separate like this seems easier in case we do want to extend. */ -const aliasToIDMap = new Map(); +const aliasToIDMap = new Map(); -export function storeRoomAliasInCache(alias, id) { +export function storeRoomAliasInCache(alias: string, id: string): void { aliasToIDMap.set(alias, id); } -export function getCachedRoomIDForAlias(alias) { +export function getCachedRoomIDForAlias(alias: string): string { return aliasToIDMap.get(alias); } From 2525db6fb131ce760673463000895ed73f9d1ef8 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 7 Jun 2021 11:03:39 +0100 Subject: [PATCH 407/999] Convert Resend to Typescript --- src/{Resend.js => Resend.ts} | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) rename src/{Resend.js => Resend.ts} (70%) diff --git a/src/Resend.js b/src/Resend.ts similarity index 70% rename from src/Resend.js rename to src/Resend.ts index f1e5fb38f5..38b84a28e0 100644 --- a/src/Resend.js +++ b/src/Resend.ts @@ -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. @@ -15,35 +14,37 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {MatrixClientPeg} from './MatrixClientPeg'; +import { MatrixEvent, EventStatus } from 'matrix-js-sdk/src/models/event'; +import { Room } from 'matrix-js-sdk/src/models/room'; + +import { MatrixClientPeg } from './MatrixClientPeg'; import dis from './dispatcher/dispatcher'; -import { EventStatus } from 'matrix-js-sdk/src/models/event'; export default class Resend { - static resendUnsentEvents(room) { - return Promise.all(room.getPendingEvents().filter(function(ev) { + static resendUnsentEvents(room: Room): Promise { + return Promise.all(room.getPendingEvents().filter(function(ev: MatrixEvent) { return ev.status === EventStatus.NOT_SENT; - }).map(function(event) { + }).map(function(event: MatrixEvent) { return Resend.resend(event); })); } - static cancelUnsentEvents(room) { - room.getPendingEvents().filter(function(ev) { + static cancelUnsentEvents(room: Room): void { + room.getPendingEvents().filter(function(ev: MatrixEvent) { return ev.status === EventStatus.NOT_SENT; - }).forEach(function(event) { + }).forEach(function(event: MatrixEvent) { Resend.removeFromQueue(event); }); } - static resend(event) { + static resend(event: MatrixEvent): Promise { const room = MatrixClientPeg.get().getRoom(event.getRoomId()); return MatrixClientPeg.get().resendEvent(event, room).then(function(res) { dis.dispatch({ action: 'message_sent', event: event, }); - }, function(err) { + }, function(err: Error) { // XXX: temporary logging to try to diagnose // https://github.com/vector-im/element-web/issues/3148 console.log('Resend got send failure: ' + err.name + '(' + err + ')'); @@ -55,7 +56,7 @@ export default class Resend { }); } - static removeFromQueue(event) { + static removeFromQueue(event: MatrixEvent): void { MatrixClientPeg.get().cancelPendingEvent(event); } } From ae04b2ce4ac0ea4ee425d848903aa5b5ec9db82f Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 7 Jun 2021 11:06:30 +0100 Subject: [PATCH 408/999] Fix Dialog/Modal types defn --- src/components/views/dialogs/IDialogProps.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/dialogs/IDialogProps.ts b/src/components/views/dialogs/IDialogProps.ts index 1027ca7607..b294fdafe1 100644 --- a/src/components/views/dialogs/IDialogProps.ts +++ b/src/components/views/dialogs/IDialogProps.ts @@ -15,5 +15,5 @@ limitations under the License. */ export interface IDialogProps { - onFinished: (bool) => void; + onFinished(...args: any): void; } From 4cc95dd1841111e93a18d1a7a8630abed681449b Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 7 Jun 2021 11:20:35 +0100 Subject: [PATCH 409/999] Make skinning less upset --- src/AsyncWrapper.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/AsyncWrapper.tsx b/src/AsyncWrapper.tsx index 1d3c42e60b..3bbef71093 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 DialogButtons from "./components/views/elements/DialogButtons"; -import Spinner from "./components/views/elements/Spinner"; type AsyncImport = { default: T }; @@ -79,6 +77,8 @@ 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 ; } } From 6693c39709fb5fae664ce27e20343dfca347d278 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 7 Jun 2021 12:17:29 +0100 Subject: [PATCH 410/999] make event list summary a list item --- src/components/views/elements/EventListSummary.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/elements/EventListSummary.tsx b/src/components/views/elements/EventListSummary.tsx index 1d3b6e8764..86d3e082ad 100644 --- a/src/components/views/elements/EventListSummary.tsx +++ b/src/components/views/elements/EventListSummary.tsx @@ -63,9 +63,9 @@ const EventListSummary: React.FC = ({ // If we are only given few events then just pass them through if (events.length < threshold) { return ( -
      +
    15. { children } -
    16. + ); } From ea6904ce2a10e9aa368db43a900ea95fbb134911 Mon Sep 17 00:00:00 2001 From: Germain Date: Mon, 7 Jun 2021 12:37:11 +0100 Subject: [PATCH 411/999] Fix local echo comment for scroll tokens Co-authored-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/rooms/EventTile.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 0796475bf8..ff310e1c43 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -906,6 +906,8 @@ export default class EventTile extends React.Component { permalink = this.props.permalinkCreator.forEvent(this.props.mxEvent.getId()); } + // we can't use local echoes as scroll tokens, because their event IDs change. + // Local echos have a send "status". const scrollToken = this.props.mxEvent.status ? undefined : this.props.mxEvent.getId(); @@ -1153,8 +1155,6 @@ export default class EventTile extends React.Component { "tabIndex": -1, "aria-live": ariaLive, "aria-atomic": "true", - // we can't use local echoes as scroll tokens, because their event IDs change. - // Local echos have a send "status". "data-scroll-tokens": scrollToken, "onMouseEnter": () => this.setState({ hover: true }), "onMouseLeave": () => this.setState({ hover: false }), From f3aa5056730c1bc94ca4988f0abacba30d4348ab Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 7 Jun 2021 14:14:31 +0100 Subject: [PATCH 412/999] Add warning to private space creation flow --- res/css/structures/_SpaceRoomView.scss | 39 +++++++++++++++++++++ src/components/structures/SpaceRoomView.tsx | 4 +++ src/i18n/strings/en_EN.json | 2 ++ 3 files changed, 45 insertions(+) diff --git a/res/css/structures/_SpaceRoomView.scss b/res/css/structures/_SpaceRoomView.scss index 4bc4af467c..12762cbc9d 100644 --- a/res/css/structures/_SpaceRoomView.scss +++ b/res/css/structures/_SpaceRoomView.scss @@ -365,6 +365,45 @@ $SpaceRoomViewInnerWidth: 428px; } } + .mx_SpaceRoomView_betaWarning { + padding: 12px 12px 12px 54px; + position: relative; + font-size: $font-15px; + line-height: $font-24px; + width: 432px; + border-radius: 8px; + background-color: $info-plinth-bg-color; + color: $secondary-fg-color; + box-sizing: border-box; + + > h3 { + font-weight: $font-semi-bold; + font-size: inherit; + line-height: inherit; + margin: 0; + } + + > p { + font-size: inherit; + line-height: inherit; + margin: 0; + } + + &::before { + mask-image: url('$(res)/img/element-icons/room/room-summary.svg'); + mask-position: center; + mask-repeat: no-repeat; + mask-size: contain; + content: ''; + width: 20px; + height: 20px; + position: absolute; + top: 14px; + left: 14px; + background-color: $secondary-fg-color; + } + } + .mx_SpaceRoomView_inviteTeammates { // XXX remove this when spaces leaves Beta .mx_SpaceRoomView_inviteTeammates_betaDisclaimer { diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx index 06d2bda16e..276f4ae6ca 100644 --- a/src/components/structures/SpaceRoomView.tsx +++ b/src/components/structures/SpaceRoomView.tsx @@ -587,6 +587,10 @@ const SpaceSetupPrivateScope = ({ space, justCreatedOpts, onFinished }) => {

      { _t("Me and my teammates") }

      { _t("A private space for you and your teammates") }
      +
      +

      { _t("Teammates might not be able to view or join any private rooms you make.") }

      +

      { _t("We're working on this as part of the beta, but just want to let you know.") }

      +
      ; }; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 9e85ea28c8..16cfcccd11 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2723,6 +2723,8 @@ "A private space to organise your rooms": "A private space to organise your rooms", "Me and my teammates": "Me and my teammates", "A private space for you and your teammates": "A private space for you and your teammates", + "Teammates might not be able to view or join any private rooms you make.": "Teammates might not be able to view or join any private rooms you make.", + "We're working on this as part of the beta, but just want to let you know.": "We're working on this as part of the beta, but just want to let you know.", "Failed to invite the following users to your space: %(csvUsers)s": "Failed to invite the following users to your space: %(csvUsers)s", "Inviting...": "Inviting...", "Invite your teammates": "Invite your teammates", From 67c9fe1e070d41192d9ff8553523c5834ff9f38c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 7 Jun 2021 15:25:57 +0200 Subject: [PATCH 413/999] Revert "Update curly braces styling" This reverts commit ba0f6766caefb700324a0a4441e0427689b8faf7. --- src/components/views/elements/ImageView.tsx | 131 ++++++++++---------- 1 file changed, 65 insertions(+), 66 deletions(-) diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index 948e005978..c4ec3497e4 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -19,20 +19,20 @@ limitations under the License. import React, { createRef } from 'react'; import { _t } from '../../../languageHandler'; import AccessibleTooltipButton from "./AccessibleTooltipButton"; -import { Key } from "../../../Keyboard"; +import {Key} from "../../../Keyboard"; import FocusLock from "react-focus-lock"; import MemberAvatar from "../avatars/MemberAvatar"; -import { ContextMenuTooltipButton } from "../../../accessibility/context_menu/ContextMenuTooltipButton"; +import {ContextMenuTooltipButton} from "../../../accessibility/context_menu/ContextMenuTooltipButton"; import MessageContextMenu from "../context_menus/MessageContextMenu"; -import { aboveLeftOf, ContextMenu } from '../../structures/ContextMenu'; +import {aboveLeftOf, ContextMenu} from '../../structures/ContextMenu'; import MessageTimestamp from "../messages/MessageTimestamp"; import SettingsStore from "../../../settings/SettingsStore"; -import { formatFullDate } from "../../../DateUtils"; +import {formatFullDate} from "../../../DateUtils"; import dis from '../../../dispatcher/dispatcher'; -import { replaceableComponent } from "../../../utils/replaceableComponent"; -import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks" -import { MatrixEvent } from "matrix-js-sdk/src/models/event"; -import { normalizeWheelEvent } from "../../../utils/Mouse"; +import {replaceableComponent} from "../../../utils/replaceableComponent"; +import {RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks" +import {MatrixEvent} from "matrix-js-sdk/src/models/event"; +import {normalizeWheelEvent} from "../../../utils/Mouse"; // Max scale to keep gaps around the image const MAX_SCALE = 0.95; @@ -167,7 +167,7 @@ export default class ImageView extends React.Component { return; } if (newZoom >= this.state.maxZoom) { - this.setState({ zoom: this.state.maxZoom }); + this.setState({zoom: this.state.maxZoom}); return; } @@ -256,11 +256,11 @@ export default class ImageView extends React.Component { // Zoom in if we are completely zoomed out if (this.state.zoom === this.state.minZoom) { - this.setState({ zoom: this.state.maxZoom }); + this.setState({zoom: this.state.maxZoom}); return; } - this.setState({ moving: true }); + this.setState({moving: true}); this.previousX = this.state.translationX; this.previousY = this.state.translationY; this.initX = ev.pageX - this.state.translationX; @@ -294,7 +294,7 @@ export default class ImageView extends React.Component { this.initX = 0; this.initY = 0; } - this.setState({ moving: false }); + this.setState({moving: false}); }; private renderContextMenu() { @@ -302,14 +302,14 @@ export default class ImageView extends React.Component { if (this.state.contextMenuDisplayed) { contextMenu = ( ); @@ -347,10 +347,10 @@ export default class ImageView extends React.Component { const style = { cursor: cursor, transition: this.state.moving ? null : "transform 200ms ease 0s", - transform: `translateX(${ translatePixelsX }) - translateY(${ translatePixelsY }) - scale(${ zoom }) - rotate(${ rotationDegrees })`, + transform: `translateX(${translatePixelsX}) + translateY(${translatePixelsY}) + scale(${zoom}) + rotate(${rotationDegrees})`, }; let info; @@ -365,38 +365,37 @@ export default class ImageView extends React.Component { const senderName = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender(); const sender = (
      - { senderName } + {senderName}
      ); const messageTimestamp = ( ); const avatar = ( ); info = (
      - { avatar } + {avatar}
      - { sender } - { messageTimestamp } + {sender} + {messageTimestamp}
      ); @@ -414,10 +413,10 @@ export default class ImageView extends React.Component { contextMenuButton = ( ); } @@ -428,14 +427,14 @@ export default class ImageView extends React.Component { zoomOutButton = ( + title={_t("Zoom out")} + onClick={this.onZoomOutClick}> ); zoomInButton = ( ); @@ -443,57 +442,57 @@ export default class ImageView extends React.Component { return (
      - { info } + {info}
      + title={_t("Rotate Right")} + onClick={this.onRotateClockwiseClick}> - { zoomOutButton } - { zoomInButton } + {zoomOutButton} + {zoomInButton} - { contextMenuButton } + {contextMenuButton} - { this.renderContextMenu() } + {this.renderContextMenu()}
      + ref={this.imageWrapper}>
      From 5880cb3b5f032f2a274fc1bcdd4a32b9ba31b8e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 7 Jun 2021 15:26:54 +0200 Subject: [PATCH 414/999] Better wording 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 | 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 c4ec3497e4..ebff886bb3 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -144,7 +144,7 @@ export default class ImageView extends React.Component { // image by default const minZoom = Math.min(zoomX, zoomY) * MAX_SCALE; - // If zoom is smaller than minZoom don't go beneath that value + // If zoom is smaller than minZoom don't go below that value const zoom = (this.state.zoom <= this.state.minZoom) ? minZoom : this.state.zoom; this.setState({ From 0918ebad0adf697a52354e8fc837a5b35ac56f46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 7 Jun 2021 15:28:37 +0200 Subject: [PATCH 415/999] calculateZoomAndRotate -> setZoomAndRotation 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 | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index ebff886bb3..b5162c6084 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -104,18 +104,18 @@ export default class ImageView extends React.Component { // needs to be passive in order to work with Chromium this.focusLock.current.addEventListener('wheel', this.onWheel, { passive: false }); // We want to recalculate zoom whenever the window's size changes - window.addEventListener("resize", this.calculateZoomAndRotate); + window.addEventListener("resize", this.setZoomAndRotation); // After the image loads for the first time we want to calculate the zoom - this.image.current.addEventListener("load", this.calculateZoomAndRotate); + this.image.current.addEventListener("load", this.setZoomAndRotation); } componentWillUnmount() { this.focusLock.current.removeEventListener('wheel', this.onWheel); - window.removeEventListener("resize", this.calculateZoomAndRotate); - this.image.current.removeEventListener("load", this.calculateZoomAndRotate); + window.removeEventListener("resize", this.setZoomAndRotation); + this.image.current.removeEventListener("load", this.setZoomAndRotation); } - private calculateZoomAndRotate = () => { + private setZoomAndRotation = () => { const image = this.image.current; const imageWrapper = this.imageWrapper.current; @@ -203,13 +203,13 @@ export default class ImageView extends React.Component { private onRotateCounterClockwiseClick = () => { const cur = this.state.rotation; this.rotation = cur - 90; - this.calculateZoomAndRotate(); + this.setZoomAndRotation(); }; private onRotateClockwiseClick = () => { const cur = this.state.rotation; this.rotation = cur + 90; - this.calculateZoomAndRotate(); + this.setZoomAndRotation(); }; private onDownloadClick = () => { From 2e47a1176f6dd883d2b68b6d35cc13d590c52fa6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 7 Jun 2021 15:39:11 +0200 Subject: [PATCH 416/999] Get rid of weird rotation prop 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 | 29 ++++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index b5162c6084..e0991a1023 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -97,29 +97,34 @@ export default class ImageView extends React.Component { private initY = 0; private previousX = 0; private previousY = 0; - private rotation = 0; componentDidMount() { // We have to use addEventListener() because the listener // needs to be passive in order to work with Chromium this.focusLock.current.addEventListener('wheel', this.onWheel, { passive: false }); // We want to recalculate zoom whenever the window's size changes - window.addEventListener("resize", this.setZoomAndRotation); + window.addEventListener("resize", this.recalculateZoom); // After the image loads for the first time we want to calculate the zoom - this.image.current.addEventListener("load", this.setZoomAndRotation); + this.image.current.addEventListener("load", this.recalculateZoom); } componentWillUnmount() { this.focusLock.current.removeEventListener('wheel', this.onWheel); - window.removeEventListener("resize", this.setZoomAndRotation); - this.image.current.removeEventListener("load", this.setZoomAndRotation); + window.removeEventListener("resize", this.recalculateZoom); + this.image.current.removeEventListener("load", this.recalculateZoom); } - private setZoomAndRotation = () => { + private recalculateZoom = () => { + this.setZoomAndRotation(); + } + + private setZoomAndRotation = (inputRotation?: number) => { const image = this.image.current; const imageWrapper = this.imageWrapper.current; - const landscape = this.rotation % 180 === 0; + const rotation = inputRotation || this.state.rotation; + + const landscape = rotation % 180 === 0; // If the image is rotated take it into account const width = landscape ? image.naturalWidth : image.naturalHeight; @@ -135,7 +140,7 @@ export default class ImageView extends React.Component { zoom: 1, minZoom: 1, maxZoom: 1, - rotation: this.rotation, + rotation: rotation, }); return; } @@ -150,7 +155,7 @@ export default class ImageView extends React.Component { this.setState({ minZoom: minZoom, maxZoom: 1, - rotation: this.rotation, + rotation: rotation, zoom: zoom, }); } @@ -202,14 +207,12 @@ export default class ImageView extends React.Component { private onRotateCounterClockwiseClick = () => { const cur = this.state.rotation; - this.rotation = cur - 90; - this.setZoomAndRotation(); + this.setZoomAndRotation(cur - 90); }; private onRotateClockwiseClick = () => { const cur = this.state.rotation; - this.rotation = cur + 90; - this.setZoomAndRotation(); + this.setZoomAndRotation(cur + 90); }; private onDownloadClick = () => { From b5d271759f227a03f1905759dcab4c92149cc15e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 7 Jun 2021 15:43:43 +0200 Subject: [PATCH 417/999] Why would I format this myself if eslint can do it for me? 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 | 26 ++++++++++----------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index e0991a1023..d0dcb1fa00 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -19,20 +19,20 @@ limitations under the License. import React, { createRef } from 'react'; import { _t } from '../../../languageHandler'; import AccessibleTooltipButton from "./AccessibleTooltipButton"; -import {Key} from "../../../Keyboard"; +import { Key } from "../../../Keyboard"; import FocusLock from "react-focus-lock"; import MemberAvatar from "../avatars/MemberAvatar"; -import {ContextMenuTooltipButton} from "../../../accessibility/context_menu/ContextMenuTooltipButton"; +import { ContextMenuTooltipButton } from "../../../accessibility/context_menu/ContextMenuTooltipButton"; import MessageContextMenu from "../context_menus/MessageContextMenu"; -import {aboveLeftOf, ContextMenu} from '../../structures/ContextMenu'; +import { aboveLeftOf, ContextMenu } from '../../structures/ContextMenu'; import MessageTimestamp from "../messages/MessageTimestamp"; import SettingsStore from "../../../settings/SettingsStore"; -import {formatFullDate} from "../../../DateUtils"; +import { formatFullDate } from "../../../DateUtils"; import dis from '../../../dispatcher/dispatcher'; -import {replaceableComponent} from "../../../utils/replaceableComponent"; -import {RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks" -import {MatrixEvent} from "matrix-js-sdk/src/models/event"; -import {normalizeWheelEvent} from "../../../utils/Mouse"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks" +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { normalizeWheelEvent } from "../../../utils/Mouse"; // Max scale to keep gaps around the image const MAX_SCALE = 0.95; @@ -172,7 +172,7 @@ export default class ImageView extends React.Component { return; } if (newZoom >= this.state.maxZoom) { - this.setState({zoom: this.state.maxZoom}); + this.setState({ zoom: this.state.maxZoom }); return; } @@ -185,7 +185,7 @@ export default class ImageView extends React.Component { ev.stopPropagation(); ev.preventDefault(); - const {deltaY} = normalizeWheelEvent(ev); + const { deltaY } = normalizeWheelEvent(ev); this.zoom(-(deltaY * ZOOM_COEFFICIENT)); }; @@ -259,11 +259,11 @@ export default class ImageView extends React.Component { // Zoom in if we are completely zoomed out if (this.state.zoom === this.state.minZoom) { - this.setState({zoom: this.state.maxZoom}); + this.setState({ zoom: this.state.maxZoom }); return; } - this.setState({moving: true}); + this.setState({ moving: true }); this.previousX = this.state.translationX; this.previousY = this.state.translationY; this.initX = ev.pageX - this.state.translationX; @@ -297,7 +297,7 @@ export default class ImageView extends React.Component { this.initX = 0; this.initY = 0; } - this.setState({moving: false}); + this.setState({ moving: false }); }; private renderContextMenu() { From 255f9967109ae6024dbfbab0dd5581968691dbac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 7 Jun 2021 15:46:03 +0200 Subject: [PATCH 418/999] landscape -> imageIsNotFlipped 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 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index d0dcb1fa00..298951428f 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -124,11 +124,11 @@ export default class ImageView extends React.Component { const rotation = inputRotation || this.state.rotation; - const landscape = rotation % 180 === 0; + const imageIsNotFlipped = rotation % 180 === 0; // If the image is rotated take it into account - const width = landscape ? image.naturalWidth : image.naturalHeight; - const height = landscape ? image.naturalHeight : image.naturalWidth; + const width = imageIsNotFlipped ? image.naturalWidth : image.naturalHeight; + const height = imageIsNotFlipped ? image.naturalHeight : image.naturalWidth; const zoomX = imageWrapper.clientWidth / width; const zoomY = imageWrapper.clientHeight / height; From 20a3dd67d6b3029c75adac9da58f162aed5aa1bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 7 Jun 2021 16:12:06 +0200 Subject: [PATCH 419/999] Some more styling 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 | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index 298951428f..630280120c 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -368,7 +368,7 @@ export default class ImageView extends React.Component { const senderName = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender(); const sender = (
      - {senderName} + { senderName }
      ); const messageTimestamp = ( @@ -395,10 +395,10 @@ export default class ImageView extends React.Component { info = (
      - {avatar} + { avatar }
      - {sender} - {messageTimestamp} + { sender } + { messageTimestamp }
      ); @@ -438,7 +438,7 @@ export default class ImageView extends React.Component { + onClick={this.onZoomInClick}> ); } @@ -454,7 +454,7 @@ export default class ImageView extends React.Component { ref={this.focusLock} >
      - {info} + { info }
      { title={_t("Rotate Right")} onClick={this.onRotateClockwiseClick}> - {zoomOutButton} - {zoomInButton} + { zoomOutButton } + { zoomInButton } - {contextMenuButton} + { contextMenuButton } - {this.renderContextMenu()} + { this.renderContextMenu() }
      Date: Mon, 7 Jun 2021 15:48:55 +0100 Subject: [PATCH 420/999] Convert EditableItemList & AliasSettings to Typescript --- ...itableItemList.js => EditableItemList.tsx} | 133 +++++++++-------- .../{AliasSettings.js => AliasSettings.tsx} | 134 ++++++++++-------- 2 files changed, 146 insertions(+), 121 deletions(-) rename src/components/views/elements/{EditableItemList.js => EditableItemList.tsx} (54%) rename src/components/views/room_settings/{AliasSettings.js => AliasSettings.tsx} (78%) diff --git a/src/components/views/elements/EditableItemList.js b/src/components/views/elements/EditableItemList.tsx similarity index 54% rename from src/components/views/elements/EditableItemList.js rename to src/components/views/elements/EditableItemList.tsx index d8ec5af278..89e2e1b8a0 100644 --- a/src/components/views/elements/EditableItemList.js +++ b/src/components/views/elements/EditableItemList.tsx @@ -1,5 +1,5 @@ /* -Copyright 2017, 2019 New Vector Ltd. +Copyright 2017-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,48 +14,48 @@ 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 from "react"; + +import { _t } from '../../../languageHandler'; import Field from "./Field"; import AccessibleButton from "./AccessibleButton"; -import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; -export class EditableItem extends React.Component { - static propTypes = { - index: PropTypes.number, - value: PropTypes.string, - onRemove: PropTypes.func, +interface IItemProps { + index?: number; + value?: string; + onRemove?(index: number): void; +} + +interface IItemState { + verifyRemove: boolean; +} + +export class EditableItem extends React.Component { + public state = { + verifyRemove: false, }; - constructor() { - super(); - - this.state = { - verifyRemove: false, - }; - } - - _onRemove = (e) => { + private onRemove = (e) => { e.stopPropagation(); e.preventDefault(); - this.setState({verifyRemove: true}); + this.setState({ verifyRemove: true }); }; - _onDontRemove = (e) => { + private onDontRemove = (e) => { e.stopPropagation(); e.preventDefault(); - this.setState({verifyRemove: false}); + this.setState({ verifyRemove: false }); }; - _onActuallyRemove = (e) => { + private onActuallyRemove = (e) => { e.stopPropagation(); e.preventDefault(); if (this.props.onRemove) this.props.onRemove(this.props.index); - this.setState({verifyRemove: false}); + this.setState({ verifyRemove: false }); }; render() { @@ -66,14 +66,14 @@ export class EditableItem extends React.Component { {_t("Are you sure?")} {_t("Yes")} @@ -85,59 +85,68 @@ export class EditableItem extends React.Component { return (
      -
      +
      {this.props.value}
      ); } } +interface IProps { + id: string; + items: string[]; + itemsLabel?: string; + noItemsLabel?: string; + placeholder?: string; + newItem?: string; + canEdit?: boolean; + canRemove?: boolean; + suggestionsListId?: string; + onItemAdded?(item: string): void; + onItemRemoved?(index: number): void; + onNewItemChanged?(item: string): void; +} + @replaceableComponent("views.elements.EditableItemList") -export default class EditableItemList extends React.Component { - static propTypes = { - id: PropTypes.string.isRequired, - items: PropTypes.arrayOf(PropTypes.string).isRequired, - itemsLabel: PropTypes.string, - noItemsLabel: PropTypes.string, - placeholder: PropTypes.string, - newItem: PropTypes.string, - - onItemAdded: PropTypes.func, - onItemRemoved: PropTypes.func, - onNewItemChanged: PropTypes.func, - - canEdit: PropTypes.bool, - canRemove: PropTypes.bool, - }; - - _onItemAdded = (e) => { +export default class EditableItemList

      extends React.PureComponent { + protected onItemAdded = (e) => { e.stopPropagation(); e.preventDefault(); if (this.props.onItemAdded) this.props.onItemAdded(this.props.newItem); }; - _onItemRemoved = (index) => { + protected onItemRemoved = (index) => { if (this.props.onItemRemoved) this.props.onItemRemoved(index); }; - _onNewItemChanged = (e) => { + protected onNewItemChanged = (e) => { if (this.props.onNewItemChanged) this.props.onNewItemChanged(e.target.value); }; - _renderNewItemField() { + protected renderNewItemField() { return ( - - - {_t("Add")} + + + { _t("Add") } ); @@ -153,19 +162,21 @@ export default class EditableItemList extends React.Component { key={item} index={index} value={item} - onRemove={this._onItemRemoved} + onRemove={this.onItemRemoved} />; }); const editableItemsSection = this.props.canRemove ? editableItems :

        {editableItems}
      ; const label = this.props.items.length > 0 ? this.props.itemsLabel : this.props.noItemsLabel; - return (
      -
      - { label } + return ( +
      +
      + { label } +
      + { editableItemsSection } + { this.props.canEdit ? this.renderNewItemField() :
      }
      - { editableItemsSection } - { this.props.canEdit ? this._renderNewItemField() :
      } -
      ); + ); } } diff --git a/src/components/views/room_settings/AliasSettings.js b/src/components/views/room_settings/AliasSettings.tsx similarity index 78% rename from src/components/views/room_settings/AliasSettings.js rename to src/components/views/room_settings/AliasSettings.tsx index 80e0099ab3..d6e79c4ee9 100644 --- a/src/components/views/room_settings/AliasSettings.js +++ b/src/components/views/room_settings/AliasSettings.tsx @@ -1,6 +1,5 @@ /* -Copyright 2016 OpenMarket Ltd -Copyright 2018, 2019 New Vector Ltd +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. @@ -15,59 +14,60 @@ See the License for the specific language governing permissions and limitations under the License. */ +import React, { ChangeEvent, createRef } from "react"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; + import EditableItemList from "../elements/EditableItemList"; -import React, {createRef} from 'react'; -import PropTypes from 'prop-types'; -import {MatrixClientPeg} from "../../../MatrixClientPeg"; -import * as sdk from "../../../index"; +import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { _t } from '../../../languageHandler'; import Field from "../elements/Field"; +import Spinner from "../elements/Spinner"; import ErrorDialog from "../dialogs/ErrorDialog"; import AccessibleButton from "../elements/AccessibleButton"; import Modal from "../../../Modal"; import RoomPublishSetting from "./RoomPublishSetting"; -import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; +import RoomAliasField from "../elements/RoomAliasField"; -class EditableAliasesList extends EditableItemList { - constructor(props) { - super(props); +interface IEditableAliasesListProps { + domain?: string; +} - this._aliasField = createRef(); - } +class EditableAliasesList extends EditableItemList { + private aliasField = createRef(); - _onAliasAdded = async () => { - await this._aliasField.current.validate({ allowEmpty: false }); + private onAliasAdded = async () => { + await this.aliasField.current.validate({ allowEmpty: false }); - if (this._aliasField.current.isValid) { + if (this.aliasField.current.isValid) { if (this.props.onItemAdded) this.props.onItemAdded(this.props.newItem); return; } - this._aliasField.current.focus(); - this._aliasField.current.validate({ allowEmpty: false, focused: true }); + this.aliasField.current.focus(); + this.aliasField.current.validate({ allowEmpty: false, focused: true }); }; - _renderNewItemField() { + protected renderNewItemField() { // if we don't need the RoomAliasField, - // we don't need to overriden version of _renderNewItemField + // we don't need to overriden version of renderNewItemField if (!this.props.domain) { - return super._renderNewItemField(); + return super.renderNewItemField(); } - const RoomAliasField = sdk.getComponent('views.elements.RoomAliasField'); - const onChange = (alias) => this._onNewItemChanged({target: {value: alias}}); + const onChange = (alias) => this.onNewItemChanged({target: {value: alias}}); return (
      - + { _t("Add") } @@ -75,15 +75,27 @@ class EditableAliasesList extends EditableItemList { } } -@replaceableComponent("views.room_settings.AliasSettings") -export default class AliasSettings extends React.Component { - static propTypes = { - roomId: PropTypes.string.isRequired, - canSetCanonicalAlias: PropTypes.bool.isRequired, - canSetAliases: PropTypes.bool.isRequired, - canonicalAliasEvent: PropTypes.object, // MatrixEvent - }; +interface IProps { + roomId: string; + canSetCanonicalAlias: boolean; + canSetAliases: boolean; + canonicalAliasEvent?: MatrixEvent; + aliasEvents?: MatrixEvent[]; +} +interface IState { + altAliases: string[]; + localAliases: string[]; + canonicalAlias?: string; + updatingCanonicalAlias: boolean; + localAliasesLoading: boolean; + detailsOpen: boolean; + newAlias?: string; + newAltAlias?: string; +} + +@replaceableComponent("views.room_settings.AliasSettings") +export default class AliasSettings extends React.Component { static defaultProps = { canSetAliases: false, canSetCanonicalAlias: false, @@ -122,7 +134,7 @@ export default class AliasSettings extends React.Component { } } - async loadLocalAliases() { + private async loadLocalAliases() { this.setState({ localAliasesLoading: true }); try { const cli = MatrixClientPeg.get(); @@ -139,7 +151,7 @@ export default class AliasSettings extends React.Component { } } - changeCanonicalAlias(alias) { + private changeCanonicalAlias(alias: string) { if (!this.props.canSetCanonicalAlias) return; const oldAlias = this.state.canonicalAlias; @@ -170,7 +182,7 @@ export default class AliasSettings extends React.Component { }); } - changeAltAliases(altAliases) { + private changeAltAliases(altAliases: string[]) { if (!this.props.canSetCanonicalAlias) return; this.setState({ @@ -181,7 +193,7 @@ export default class AliasSettings extends React.Component { const eventContent = {}; if (this.state.canonicalAlias) { - eventContent.alias = this.state.canonicalAlias; + eventContent["alias"] = this.state.canonicalAlias; } if (altAliases) { eventContent["alt_aliases"] = altAliases; @@ -202,11 +214,11 @@ export default class AliasSettings extends React.Component { }); } - onNewAliasChanged = (value) => { - this.setState({newAlias: value}); + private onNewAliasChanged = (value: string) => { + this.setState({ newAlias: value }); }; - onLocalAliasAdded = (alias) => { + private onLocalAliasAdded = (alias: string) => { if (!alias || alias.length === 0) return; // ignore attempts to create blank aliases const localDomain = MatrixClientPeg.get().getDomain(); @@ -232,7 +244,7 @@ export default class AliasSettings extends React.Component { }); }; - onLocalAliasDeleted = (index) => { + private onLocalAliasDeleted = (index: number) => { const alias = this.state.localAliases[index]; // TODO: In future, we should probably be making sure that the alias actually belongs // to this room. See https://github.com/vector-im/element-web/issues/7353 @@ -261,7 +273,7 @@ export default class AliasSettings extends React.Component { }); }; - onLocalAliasesToggled = (event) => { + private onLocalAliasesToggled = (event: ChangeEvent) => { // expanded if (event.target.open) { // if local aliases haven't been preloaded yet at component mount @@ -269,37 +281,37 @@ export default class AliasSettings extends React.Component { this.loadLocalAliases(); } } - this.setState({detailsOpen: event.target.open}); + this.setState({ detailsOpen: event.currentTarget.open }); }; - onCanonicalAliasChange = (event) => { + private onCanonicalAliasChange = (event: ChangeEvent) => { this.changeCanonicalAlias(event.target.value); }; - onNewAltAliasChanged = (value) => { - this.setState({newAltAlias: value}); + private onNewAltAliasChanged = (value: string) => { + this.setState({ newAltAlias: value }); } - onAltAliasAdded = (alias) => { + private onAltAliasAdded = (alias: string) => { const altAliases = this.state.altAliases.slice(); if (!altAliases.some(a => a.trim() === alias.trim())) { altAliases.push(alias.trim()); this.changeAltAliases(altAliases); - this.setState({newAltAlias: ""}); + this.setState({ newAltAlias: "" }); } } - onAltAliasDeleted = (index) => { + private onAltAliasDeleted = (index: number) => { const altAliases = this.state.altAliases.slice(); altAliases.splice(index, 1); this.changeAltAliases(altAliases); } - _getAliases() { - return this.state.altAliases.concat(this._getLocalNonAltAliases()); + private getAliases() { + return this.state.altAliases.concat(this.getLocalNonAltAliases()); } - _getLocalNonAltAliases() { + private getLocalNonAltAliases() { const {altAliases} = this.state; return this.state.localAliases.filter(alias => !altAliases.includes(alias)); } @@ -320,7 +332,7 @@ export default class AliasSettings extends React.Component { > { - this._getAliases().map((alias, i) => { + this.getAliases().map((alias, i) => { if (alias === this.state.canonicalAlias) found = true; return (
      ); From a895b083a2947e4befe22a2b9c49f22ca0ac8698 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Mon, 7 Jun 2021 16:21:53 +0100 Subject: [PATCH 421/999] Fix notif panel timestamp padding This restores some space around the timestamp in the notif panel. We were previously relying somewhat randomly on the presence on empty flair elements to create space. Fixes https://github.com/vector-im/element-web/issues/17580 --- res/css/structures/_NotificationPanel.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/structures/_NotificationPanel.scss b/res/css/structures/_NotificationPanel.scss index 1258ace069..e54feca175 100644 --- a/res/css/structures/_NotificationPanel.scss +++ b/res/css/structures/_NotificationPanel.scss @@ -82,7 +82,6 @@ limitations under the License. color: $primary-fg-color; font-size: $font-12px; display: inline; - padding-left: 0px; } .mx_NotificationPanel .mx_EventTile_senderDetails { @@ -103,6 +102,7 @@ limitations under the License. visibility: visible; position: initial; display: inline; + padding-left: 5px; } .mx_NotificationPanel .mx_EventTile_line { From 4725a9e8fa954b3858f4dd8e8d4212f6a0d61488 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 7 Jun 2021 16:42:59 +0100 Subject: [PATCH 422/999] Extract useRoomState hook into hooks directory --- .../views/right_panel/PinnedMessagesCard.tsx | 19 +-------- src/hooks/useRoomState.ts | 40 +++++++++++++++++++ 2 files changed, 41 insertions(+), 18 deletions(-) create mode 100644 src/hooks/useRoomState.ts diff --git a/src/components/views/right_panel/PinnedMessagesCard.tsx b/src/components/views/right_panel/PinnedMessagesCard.tsx index a3f1f2d9df..ad62619593 100644 --- a/src/components/views/right_panel/PinnedMessagesCard.tsx +++ b/src/components/views/right_panel/PinnedMessagesCard.tsx @@ -28,6 +28,7 @@ import { useEventEmitter } from "../../../hooks/useEventEmitter"; import PinningUtils from "../../../utils/PinningUtils"; import { useAsyncMemo } from "../../../hooks/useAsyncMemo"; import PinnedEventTile from "../rooms/PinnedEventTile"; +import { useRoomState } from "../../../hooks/useRoomState"; interface IProps { room: Room; @@ -75,24 +76,6 @@ export const useReadPinnedEvents = (room: Room): Set => { return readPinnedEvents; }; -const useRoomState = (room: Room, mapper: (state: RoomState) => T): T => { - const [value, setValue] = useState(room ? mapper(room.currentState) : undefined); - - const update = useCallback(() => { - if (!room) return; - setValue(mapper(room.currentState)); - }, [room, mapper]); - - useEventEmitter(room?.currentState, "RoomState.events", update); - useEffect(() => { - update(); - return () => { - setValue(undefined); - }; - }, [update]); - return value; -}; - const PinnedMessagesCard = ({ room, onClose }: IProps) => { const cli = useContext(MatrixClientContext); const canUnpin = useRoomState(room, state => state.mayClientSendStateEvent(EventType.RoomPinnedEvents, cli)); diff --git a/src/hooks/useRoomState.ts b/src/hooks/useRoomState.ts new file mode 100644 index 0000000000..ded51c3900 --- /dev/null +++ b/src/hooks/useRoomState.ts @@ -0,0 +1,40 @@ +/* +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 { useCallback, useEffect, useState } from "react"; +import { Room } from "matrix-js-sdk/src/models/room"; +import { RoomState } from "matrix-js-sdk/src/models/room-state"; + +import { useEventEmitter } from "./useEventEmitter"; + +// Hook to simplify watching Matrix Room state +export const useRoomState = (room: Room, mapper: (state: RoomState) => T): T => { + const [value, setValue] = useState(room ? mapper(room.currentState) : undefined); + + const update = useCallback(() => { + if (!room) return; + setValue(mapper(room.currentState)); + }, [room, mapper]); + + useEventEmitter(room?.currentState, "RoomState.events", update); + useEffect(() => { + update(); + return () => { + setValue(undefined); + }; + }, [update]); + return value; +}; From 296186b844d286062e0fe2e401320d267bb2717b Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Mon, 7 Jun 2021 16:21:53 +0100 Subject: [PATCH 423/999] Fix notif panel timestamp padding This restores some space around the timestamp in the notif panel. We were previously relying somewhat randomly on the presence on empty flair elements to create space. Fixes https://github.com/vector-im/element-web/issues/17580 --- res/css/structures/_NotificationPanel.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/structures/_NotificationPanel.scss b/res/css/structures/_NotificationPanel.scss index 1258ace069..e54feca175 100644 --- a/res/css/structures/_NotificationPanel.scss +++ b/res/css/structures/_NotificationPanel.scss @@ -82,7 +82,6 @@ limitations under the License. color: $primary-fg-color; font-size: $font-12px; display: inline; - padding-left: 0px; } .mx_NotificationPanel .mx_EventTile_senderDetails { @@ -103,6 +102,7 @@ limitations under the License. visibility: visible; position: initial; display: inline; + padding-left: 5px; } .mx_NotificationPanel .mx_EventTile_line { From dd089bf9696a7c5ad0b765bc41e5bf1d9a351466 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Mon, 7 Jun 2021 17:03:05 +0100 Subject: [PATCH 424/999] Upgrade matrix-js-sdk to 11.2.0 --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 4f6a023383..5ee56ec8cf 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,7 @@ "katex": "^0.12.0", "linkifyjs": "^2.1.9", "lodash": "^4.17.20", - "matrix-js-sdk": "11.2.0-rc.1", + "matrix-js-sdk": "11.2.0", "matrix-widget-api": "^0.1.0-beta.14", "minimist": "^1.2.5", "opus-recorder": "^8.0.3", diff --git a/yarn.lock b/yarn.lock index 1b07d4502b..5e324b7ad2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5673,10 +5673,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@11.2.0-rc.1: - version "11.2.0-rc.1" - resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-11.2.0-rc.1.tgz#339259d892ce08195864c1130780dd0b3d996af3" - integrity sha512-ACFONqdRqiPIj7aWf7G4f/2d0S/hfouIH6tdkFGDbizbWJCmqXRJ8EN2gmu0F3A6GGDo0+k9cqaeMf3Y1KIM8w== +matrix-js-sdk@11.2.0: + version "11.2.0" + resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-11.2.0.tgz#b49f1ebbb671f5a8ec76eec3b21eeda97f10fb31" + integrity sha512-YsdAQNPsvoWL3fKI2fb6evO81UKfSt+hYDkBOIylzb3AA/RtUsjtSNXcT4f6BsQIVTh6xAhEjPVoMfKORchf1Q== dependencies: "@babel/runtime" "^7.12.5" another-json "^0.2.0" From 187a3671110f181c8129b1f1dd47e110621cd8a0 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 7 Jun 2021 17:11:14 +0100 Subject: [PATCH 425/999] Add prop to alwaysShowTimestamps on TimelinePanel --- src/components/structures/NotificationPanel.tsx | 1 + src/components/structures/TimelinePanel.js | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/structures/NotificationPanel.tsx b/src/components/structures/NotificationPanel.tsx index b4f13f6b37..6c22835447 100644 --- a/src/components/structures/NotificationPanel.tsx +++ b/src/components/structures/NotificationPanel.tsx @@ -50,6 +50,7 @@ export default class NotificationPanel extends React.PureComponent { showUrlPreview={false} tileShape="notif" empty={emptyState} + alwaysShowTimestamps={true} /> ); } else { diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 6300c7532e..a8b0dc88dd 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -120,6 +120,9 @@ class TimelinePanel extends React.Component { // which layout to use layout: LayoutPropType, + + // whether to always show timestamps for an event + alwaysShowTimestamps: PropTypes.bool, } // a map from room id to read marker event timestamp @@ -1440,7 +1443,7 @@ class TimelinePanel extends React.Component { onFillRequest={this.onMessageListFillRequest} onUnfillRequest={this.onMessageListUnfillRequest} isTwelveHour={this.state.isTwelveHour} - alwaysShowTimestamps={this.state.alwaysShowTimestamps} + alwaysShowTimestamps={this.props.alwaysShowTimestamps || this.state.alwaysShowTimestamps} className={this.props.className} tileShape={this.props.tileShape} resizeNotifier={this.props.resizeNotifier} From 2983b3ace21f55cf36efddecea9851918d336fd9 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Mon, 7 Jun 2021 17:41:18 +0100 Subject: [PATCH 426/999] Prepare changelog for v3.23.0 --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a3abd1f32b..94c9530941 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +Changes in [3.23.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.23.0) (2021-06-07) +===================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.23.0-rc.1...v3.23.0) + + * Upgrade to JS SDK 11.2.0 + * [Release] Fix notif panel timestamp padding + [\#6158](https://github.com/matrix-org/matrix-react-sdk/pull/6158) + Changes in [3.23.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.23.0-rc.1) (2021-06-01) =============================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.22.0...v3.23.0-rc.1) From a3dac02e4c1b4a5d632c34ad35e634cd23e59813 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Mon, 7 Jun 2021 17:41:18 +0100 Subject: [PATCH 427/999] v3.23.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5ee56ec8cf..17067df933 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "3.23.0-rc.1", + "version": "3.23.0", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { From bcd0ba8d456f93c45ab57f8b769ece0815227571 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Mon, 7 Jun 2021 17:42:46 +0100 Subject: [PATCH 428/999] Resetting package fields for development --- package.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 17067df933..dee4013006 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", @@ -200,6 +200,5 @@ "coverageReporters": [ "text" ] - }, - "typings": "./lib/index.d.ts" + } } From 8ccdf9a3e8a038def7c6be8e920839f26dae2f83 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Mon, 7 Jun 2021 17:43:27 +0100 Subject: [PATCH 429/999] Reset matrix-js-sdk back to develop branch --- package.json | 2 +- yarn.lock | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index dee4013006..2bff2a7b13 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,7 @@ "katex": "^0.12.0", "linkifyjs": "^2.1.9", "lodash": "^4.17.20", - "matrix-js-sdk": "11.2.0", + "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", "matrix-widget-api": "^0.1.0-beta.14", "minimist": "^1.2.5", "opus-recorder": "^8.0.3", diff --git a/yarn.lock b/yarn.lock index 5e324b7ad2..97179550c9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5673,10 +5673,9 @@ 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@11.2.0: +"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop": version "11.2.0" - resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-11.2.0.tgz#b49f1ebbb671f5a8ec76eec3b21eeda97f10fb31" - integrity sha512-YsdAQNPsvoWL3fKI2fb6evO81UKfSt+hYDkBOIylzb3AA/RtUsjtSNXcT4f6BsQIVTh6xAhEjPVoMfKORchf1Q== + resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/35ecbed29d16982deff27a8c37b05167738225a2" dependencies: "@babel/runtime" "^7.12.5" another-json "^0.2.0" From ed80db73d4209a518cb8828fa55aa0aec60dcd96 Mon Sep 17 00:00:00 2001 From: Tirifto Date: Sun, 6 Jun 2021 17:19:04 +0000 Subject: [PATCH 430/999] Translated using Weblate (Esperanto) Currently translated at 99.9% (2978 of 2979 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/eo/ --- src/i18n/strings/eo.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/eo.json b/src/i18n/strings/eo.json index fc8c00fd19..1aa5ba8a52 100644 --- a/src/i18n/strings/eo.json +++ b/src/i18n/strings/eo.json @@ -3320,5 +3320,11 @@ "Reset event store": "Restarigi deponejon de okazoj", "If you do, please note that none of your messages will be deleted, but the search experience might be degraded for a few moments whilst the index is recreated": "Se vi tamen tion faras, sciu ke neniu el viaj mesaĝoj foriĝos, sed via sperto pri serĉado povas malboniĝi momente, dum la indekso estas refarata", "You most likely do not want to reset your event index store": "Plej probable, vi ne volas restarigi vian deponejon de indeksoj de okazoj", - "Reset event store?": "Ĉu restarigi deponejon de okazoj?" + "Reset event store?": "Ĉu restarigi deponejon de okazoj?", + "Currently joining %(count)s rooms|one": "Nun aliĝante al %(count)s ĉambro", + "Currently joining %(count)s rooms|other": "Nun aliĝante al %(count)s ĉambroj", + "Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "Provu aliajn vortojn aŭ kontorolu, ĉu vi ne tajperaris. Iuj rezultoj eble ne videblos, ĉar ili estas privataj kaj vi bezonus inviton por aliĝi.", + "No results for \"%(query)s\"": "Neniuj rezultoj por «%(query)s»", + "The user you called is busy.": "La uzanto, kiun vi vokis, estas okupata.", + "User Busy": "Uzanto estas okupata" } From 4185185cdc9e97231ca8f25ca4c9a5041429225c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 7 Jun 2021 19:42:48 +0200 Subject: [PATCH 431/999] Reduce number of DOM elements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/messages/SenderProfile.tsx | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/components/views/messages/SenderProfile.tsx b/src/components/views/messages/SenderProfile.tsx index a51f50525d..df75de34ba 100644 --- a/src/components/views/messages/SenderProfile.tsx +++ b/src/components/views/messages/SenderProfile.tsx @@ -146,12 +146,10 @@ export default class SenderProfile extends React.Component { } return ( -
      -
      - { displayNameElement } - { mxidElement } - { flair } -
      +
      + { displayNameElement } + { mxidElement } + { flair }
      ); } From b39c615abc6f2787decfcd5ed4c38ae6ea01d072 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 7 Jun 2021 19:44:58 +0200 Subject: [PATCH 432/999] Reorder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- .../views/messages/SenderProfile.tsx | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/components/views/messages/SenderProfile.tsx b/src/components/views/messages/SenderProfile.tsx index df75de34ba..872dc132b9 100644 --- a/src/components/views/messages/SenderProfile.tsx +++ b/src/components/views/messages/SenderProfile.tsx @@ -118,18 +118,6 @@ export default class SenderProfile extends React.Component { return null; // emote message must include the name so don't duplicate it } - let flair; - if (this.props.enableFlair) { - const displayedGroups = this._getDisplayedGroups( - this.state.userGroups, this.state.relatedGroups, - ); - - flair = ; - } - const displayNameElement = ( { displayName } @@ -145,6 +133,18 @@ export default class SenderProfile extends React.Component { ); } + let flair; + if (this.props.enableFlair) { + const displayedGroups = this._getDisplayedGroups( + this.state.userGroups, this.state.relatedGroups, + ); + + flair = ; + } + return (
      { displayNameElement } From a5bf17ff659eeb8f3d915163217d9c8574d53f54 Mon Sep 17 00:00:00 2001 From: Aaron Raimist Date: Mon, 7 Jun 2021 16:54:36 -0500 Subject: [PATCH 433/999] Revert incorrect change to forShareableRoom Signed-off-by: Aaron Raimist --- src/utils/permalinks/Permalinks.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/permalinks/Permalinks.ts b/src/utils/permalinks/Permalinks.ts index 2f268aaa0c..c2f95defd6 100644 --- a/src/utils/permalinks/Permalinks.ts +++ b/src/utils/permalinks/Permalinks.ts @@ -293,12 +293,12 @@ export function makeRoomPermalink(roomId: string): string { // If the roomId isn't actually a room ID, don't try to list the servers. // Aliases are already routable, and don't need extra information. - if (roomId[0] !== '!') return getPermalinkConstructor().forShareableRoom(roomId, []); + if (roomId[0] !== '!') return getPermalinkConstructor().forRoom(roomId, []); const client = MatrixClientPeg.get(); const room = client.getRoom(roomId); if (!room) { - return getPermalinkConstructor().forShareableRoom(roomId, []); + return getPermalinkConstructor().forRoom(roomId, []); } const permalinkCreator = new RoomPermalinkCreator(room); permalinkCreator.load(); From 773af6c7be6244775b3c2da79e38921381b401a9 Mon Sep 17 00:00:00 2001 From: Aaron Raimist Date: Mon, 7 Jun 2021 16:54:50 -0500 Subject: [PATCH 434/999] Fix test Signed-off-by: Aaron Raimist --- test/utils/permalinks/Permalinks-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/utils/permalinks/Permalinks-test.js b/test/utils/permalinks/Permalinks-test.js index 0bd4466d97..3c4982b465 100644 --- a/test/utils/permalinks/Permalinks-test.js +++ b/test/utils/permalinks/Permalinks-test.js @@ -34,7 +34,7 @@ function mockRoom(roomId, members, serverACL) { return { roomId, - getCanonicalAlias: () => roomId, + getCanonicalAlias: () => null, getJoinedMembers: () => members, getMember: (userId) => members.find(m => m.userId === userId), currentState: { From 946416cf8756ac94d0229f038e561f9215ca695f Mon Sep 17 00:00:00 2001 From: Aaron Raimist Date: Mon, 7 Jun 2021 17:09:43 -0500 Subject: [PATCH 435/999] Make serverCandidates optional Signed-off-by: Aaron Raimist --- src/utils/permalinks/PermalinkConstructor.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/utils/permalinks/PermalinkConstructor.ts b/src/utils/permalinks/PermalinkConstructor.ts index 25855eb024..986d20d4d1 100644 --- a/src/utils/permalinks/PermalinkConstructor.ts +++ b/src/utils/permalinks/PermalinkConstructor.ts @@ -19,11 +19,11 @@ limitations under the License. * TODO: Convert this to a real TypeScript interface */ export default class PermalinkConstructor { - forEvent(roomId: string, eventId: string, serverCandidates: string[]): string { + forEvent(roomId: string, eventId: string, serverCandidates: string[] = []): string { throw new Error("Not implemented"); } - forRoom(roomIdOrAlias: string, serverCandidates: string[]): string { + forRoom(roomIdOrAlias: string, serverCandidates: string[] = []): string { throw new Error("Not implemented"); } @@ -73,12 +73,12 @@ export class PermalinkParts { return new PermalinkParts(null, null, null, groupId, null); } - static forRoom(roomIdOrAlias: string, viaServers: string[]): PermalinkParts { - return new PermalinkParts(roomIdOrAlias, null, null, null, viaServers || []); + static forRoom(roomIdOrAlias: string, viaServers: string[] = []): PermalinkParts { + return new PermalinkParts(roomIdOrAlias, null, null, null, viaServers); } - static forEvent(roomId: string, eventId: string, viaServers: string[]): PermalinkParts { - return new PermalinkParts(roomId, eventId, null, null, viaServers || []); + static forEvent(roomId: string, eventId: string, viaServers: string[] = []): PermalinkParts { + return new PermalinkParts(roomId, eventId, null, null, viaServers); } get primaryEntityId(): string { From 5e3ad621892786890692c987d95f74881bebe232 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Mon, 7 Jun 2021 19:03:04 -0400 Subject: [PATCH 436/999] Remove mysterious dot from EventTilePreviews It was a bullet point, since EventTiles now get created as li by default :P Signed-off-by: Robin Townsend --- src/components/views/elements/EventTilePreview.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/views/elements/EventTilePreview.tsx b/src/components/views/elements/EventTilePreview.tsx index 77db94b5dd..20d6cbaeb3 100644 --- a/src/components/views/elements/EventTilePreview.tsx +++ b/src/components/views/elements/EventTilePreview.tsx @@ -128,6 +128,7 @@ export default class EventTilePreview extends React.Component { mxEvent={event} layout={this.props.layout} enableFlair={SettingsStore.getValue(UIFeature.Flair)} + as="div" />
      ; } From a8dab04d5eed882f90244f0a5b1ec54cbfaaea58 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Mon, 7 Jun 2021 19:10:45 -0400 Subject: [PATCH 437/999] Remove mystery dot from forward dialog preview It was a bullet point, since EventTiles now get created as li by default :P Signed-off-by: Robin Townsend --- src/components/views/dialogs/ForwardDialog.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/views/dialogs/ForwardDialog.tsx b/src/components/views/dialogs/ForwardDialog.tsx index e97958973b..b89485943a 100644 --- a/src/components/views/dialogs/ForwardDialog.tsx +++ b/src/components/views/dialogs/ForwardDialog.tsx @@ -211,6 +211,7 @@ const ForwardDialog: React.FC = ({ matrixClient: cli, event, permalinkCr layout={previewLayout} enableFlair={flairEnabled} permalinkCreator={permalinkCreator} + as="div" />

      From b00ad63a35fa2aa18c0ca04b803b34e252fa270c Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Mon, 7 Jun 2021 19:16:00 -0400 Subject: [PATCH 438/999] Fix whitespace nitpick Signed-off-by: Robin Townsend --- src/components/views/dialogs/ForwardDialog.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/dialogs/ForwardDialog.tsx b/src/components/views/dialogs/ForwardDialog.tsx index b89485943a..1c90dca432 100644 --- a/src/components/views/dialogs/ForwardDialog.tsx +++ b/src/components/views/dialogs/ForwardDialog.tsx @@ -201,7 +201,7 @@ const ForwardDialog: React.FC = ({ matrixClient: cli, event, permalinkCr onFinished={onFinished} fixedWidth={false} > -

      {_t("Message preview")}

      +

      { _t("Message preview") }

      Date: Mon, 7 Jun 2021 19:19:14 -0400 Subject: [PATCH 439/999] Hide download links from forward dialog preview since trying to interact with them is pointless. Signed-off-by: Robin Townsend --- res/css/views/dialogs/_ForwardDialog.scss | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/res/css/views/dialogs/_ForwardDialog.scss b/res/css/views/dialogs/_ForwardDialog.scss index 7422d39626..1976a43ab9 100644 --- a/res/css/views/dialogs/_ForwardDialog.scss +++ b/res/css/views/dialogs/_ForwardDialog.scss @@ -49,6 +49,11 @@ limitations under the License. .mx_EventTile_e2eIcon_unencrypted { display: none; } + + // We also hide download links to not encourage users to try interacting + .mx_MFileBody_download { + display: none; + } } > hr { From 26a030abcd76daf27c9f360000e20d54fc65cb19 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 8 Jun 2021 12:40:38 +0100 Subject: [PATCH 440/999] Better handling for widgets that fail to load --- src/components/views/elements/AppTile.js | 40 +++++++++++++++++------- 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index b898ad2ebc..91ffbf2c27 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -47,9 +47,14 @@ export default class AppTile extends React.Component { // The key used for PersistedElement this._persistKey = getPersistKey(this.props.app.id); - this._sgWidget = new StopGapWidget(this.props); - this._sgWidget.on("preparing", this._onWidgetPrepared); - this._sgWidget.on("ready", this._onWidgetReady); + try { + this._sgWidget = new StopGapWidget(this.props); + this._sgWidget.on("preparing", this._onWidgetPrepared); + this._sgWidget.on("ready", this._onWidgetReady); + } catch (e) { + console.log("Failed to construct widget", e); + this._sgWidget = null; + } this.iframe = null; // ref to the iframe (callback style) this.state = this._getNewState(props); @@ -97,7 +102,7 @@ export default class AppTile extends React.Component { // Force the widget to be non-persistent (able to be deleted/forgotten) ActiveWidgetStore.destroyPersistentWidget(this.props.app.id); PersistedElement.destroyElement(this._persistKey); - this._sgWidget.stop(); + if (this._sgWidget) this._sgWidget.stop(); } this.setState({ hasPermissionToLoad }); @@ -117,7 +122,7 @@ export default class AppTile extends React.Component { componentDidMount() { // Only fetch IM token on mount if we're showing and have permission to load - if (this.state.hasPermissionToLoad) { + if (this._sgWidget && this.state.hasPermissionToLoad) { this._startWidget(); } @@ -146,10 +151,15 @@ export default class AppTile extends React.Component { if (this._sgWidget) { this._sgWidget.stop(); } - this._sgWidget = new StopGapWidget(newProps); - this._sgWidget.on("preparing", this._onWidgetPrepared); - this._sgWidget.on("ready", this._onWidgetReady); - this._startWidget(); + try { + this._sgWidget = new StopGapWidget(newProps); + this._sgWidget.on("preparing", this._onWidgetPrepared); + this._sgWidget.on("ready", this._onWidgetReady); + this._startWidget(); + } catch (e) { + console.log("Failed to construct widget", e); + this._sgWidget = null; + } } _startWidget() { @@ -161,7 +171,7 @@ export default class AppTile extends React.Component { _iframeRefChange = (ref) => { this.iframe = ref; if (ref) { - this._sgWidget.start(ref); + if (this._sgWidget) this._sgWidget.start(ref); } else { this._resetWidget(this.props); } @@ -209,7 +219,7 @@ export default class AppTile extends React.Component { // Delete the widget from the persisted store for good measure. PersistedElement.destroyElement(this._persistKey); - this._sgWidget.stop({forceDestroy: true}); + if (this._sgWidget) this._sgWidget.stop({forceDestroy: true}); } _onWidgetPrepared = () => { @@ -340,7 +350,13 @@ export default class AppTile extends React.Component {
      ); - if (!this.state.hasPermissionToLoad) { + if (this._sgWidget === null) { + appTileBody = ( +
      + +
      + ); + } else if (!this.state.hasPermissionToLoad) { // only possible for room widgets, can assert this.props.room here const isEncrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId); appTileBody = ( From 5c62728d13ac23721c7747c10929e38fa4f2458b Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Tue, 8 Jun 2021 12:52:32 +0100 Subject: [PATCH 441/999] Migrate end to end tests pipeline to GitHub Actions --- .github/workflows/end-to-end-tests.yml | 32 ++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 .github/workflows/end-to-end-tests.yml diff --git a/.github/workflows/end-to-end-tests.yml b/.github/workflows/end-to-end-tests.yml new file mode 100644 index 0000000000..271ea06529 --- /dev/null +++ b/.github/workflows/end-to-end-tests.yml @@ -0,0 +1,32 @@ +name: "end-to-end_tests" +on: [push] +jobs: + "end-to-end_tests": + runs-on: ubuntu-latest + defaults: + run: + shell: bash + working-directory: ${{ github.workspace }} + container: + image: vectorim/element-web-ci-e2etests-env:latest + env: + CI_PACKAGE: true + GITHUB_HEAD_REF: ${{ github.head_ref }} + GITHUB_BASE_REF: ${{ github.base_ref }} + steps: + - name: Checkout code + uses: actions/checkout@v2 + - name: End-to-End tests + run: ./scripts/ci/end-to-end-tests.sh + - name: Archive logs + uses: actions/upload-artifact@v2 + with: + path: | + test/end-to-end-tests/logs/**/* + test/end-to-end-tests/synapse/installations/consent/homeserver.log + retention-days: 14 + - name: Archive performance benchmark + uses: actions/upload-artifact@v2 + with: + name: performance-entries.json + path: test/end-to-end-tests/performance-entries.json From 90a58380fd3732f250f63de65a4d542b0a0a6837 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 8 Jun 2021 16:15:20 +0100 Subject: [PATCH 442/999] Make it translateable And also the other one that I copied from --- src/components/views/elements/AppTile.js | 4 ++-- src/i18n/strings/en_EN.json | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index 91ffbf2c27..4028909167 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -353,7 +353,7 @@ export default class AppTile extends React.Component { if (this._sgWidget === null) { appTileBody = (
      - +
      ); } else if (!this.state.hasPermissionToLoad) { @@ -380,7 +380,7 @@ export default class AppTile extends React.Component { if (this.isMixedContent()) { appTileBody = (
      - +
      ); } else { diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 9e85ea28c8..64613d0d88 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1926,6 +1926,8 @@ "Widgets do not use message encryption.": "Widgets do not use message encryption.", "Widget added by": "Widget added by", "This widget may use cookies.": "This widget may use cookies.", + "Error loading Widget": "Error loading Widget", + "Error - Mixed content": "Error - Mixed content", "Popout widget": "Popout widget", "Use the Desktop app to see all encrypted files": "Use the Desktop app to see all encrypted files", "Use the Desktop app to search encrypted messages": "Use the Desktop app to search encrypted messages", From 9454a4e6c79f0ecfa5ebd91bd7af689e478ca559 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 8 Jun 2021 16:28:02 +0100 Subject: [PATCH 443/999] Convert AdvancedRoomSettingsTab to Typescript --- ...ingsTab.js => AdvancedRoomSettingsTab.tsx} | 87 ++++++++++--------- 1 file changed, 47 insertions(+), 40 deletions(-) rename src/components/views/settings/tabs/room/{AdvancedRoomSettingsTab.js => AdvancedRoomSettingsTab.tsx} (73%) diff --git a/src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.js b/src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.tsx similarity index 73% rename from src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.js rename to src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.tsx index 28aad65129..f587210095 100644 --- a/src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.js +++ b/src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.tsx @@ -1,5 +1,5 @@ /* -Copyright 2019 New Vector Ltd +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. @@ -15,68 +15,75 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; -import {_t} from "../../../../../languageHandler"; -import {MatrixClientPeg} from "../../../../../MatrixClientPeg"; -import * as sdk from "../../../../.."; + +import { _t } from "../../../../../languageHandler"; +import { MatrixClientPeg } from "../../../../../MatrixClientPeg"; import AccessibleButton from "../../../elements/AccessibleButton"; +import RoomUpgradeDialog from "../../../dialogs/RoomUpgradeDialog"; +import DevtoolsDialog from "../../../dialogs/DevtoolsDialog"; import Modal from "../../../../../Modal"; import dis from "../../../../../dispatcher/dispatcher"; -import {replaceableComponent} from "../../../../../utils/replaceableComponent"; +import { replaceableComponent } from "../../../../../utils/replaceableComponent"; + +interface IProps { + roomId: string; + closeSettingsFn(): void; +} + +interface IRecommendedVersion { + version: string; + needsUpgrade: boolean; + urgent: boolean; +} + +interface IState { + upgradeRecommendation?: IRecommendedVersion; + oldRoomId?: string; + oldEventId?: string; + upgraded?: boolean; +} @replaceableComponent("views.settings.tabs.room.AdvancedRoomSettingsTab") -export default class AdvancedRoomSettingsTab extends React.Component { - static propTypes = { - roomId: PropTypes.string.isRequired, - closeSettingsFn: PropTypes.func.isRequired, - }; - - constructor(props) { - super(props); +export default class AdvancedRoomSettingsTab extends React.Component { + constructor(props, context) { + super(props, context); this.state = { // This is eventually set to the value of room.getRecommendedVersion() upgradeRecommendation: null, }; - } - // TODO: [REACT-WARNING] Move this to constructor - UNSAFE_componentWillMount() { // eslint-disable-line camelcase // we handle lack of this object gracefully later, so don't worry about it failing here. const room = MatrixClientPeg.get().getRoom(this.props.roomId); room.getRecommendedVersion().then((v) => { const tombstone = room.currentState.getStateEvents("m.room.tombstone", ""); - const additionalStateChanges = {}; + const additionalStateChanges: Partial = {}; const createEvent = room.currentState.getStateEvents("m.room.create", ""); const predecessor = createEvent ? createEvent.getContent().predecessor : null; if (predecessor && predecessor.room_id) { - additionalStateChanges['oldRoomId'] = predecessor.room_id; - additionalStateChanges['oldEventId'] = predecessor.event_id; - additionalStateChanges['hasPreviousRoom'] = true; + additionalStateChanges.oldRoomId = predecessor.room_id; + additionalStateChanges.oldEventId = predecessor.event_id; } - this.setState({ - upgraded: tombstone && tombstone.getContent().replacement_room, + upgraded: !!tombstone?.getContent().replacement_room, upgradeRecommendation: v, ...additionalStateChanges, }); }); } - _upgradeRoom = (e) => { - const RoomUpgradeDialog = sdk.getComponent('dialogs.RoomUpgradeDialog'); + private upgradeRoom = (e) => { const room = MatrixClientPeg.get().getRoom(this.props.roomId); Modal.createTrackedDialog('Upgrade Room Version', '', RoomUpgradeDialog, {room: room}); }; - _openDevtools = (e) => { - const DevtoolsDialog = sdk.getComponent('dialogs.DevtoolsDialog'); + private openDevtools = (e) => { Modal.createDialog(DevtoolsDialog, {roomId: this.props.roomId}); }; - _onOldRoomClicked = (e) => { + private onOldRoomClicked = (e) => { e.preventDefault(); e.stopPropagation(); @@ -113,7 +120,7 @@ export default class AdvancedRoomSettingsTab extends React.Component { }, )}

      - + {_t("Upgrade this room to the recommended room version")}
      @@ -121,13 +128,13 @@ export default class AdvancedRoomSettingsTab extends React.Component { } let oldRoomLink; - if (this.state.hasPreviousRoom) { + if (this.state.oldRoomId) { let name = _t("this room"); const room = MatrixClientPeg.get().getRoom(this.props.roomId); if (room && room.name) name = room.name; oldRoomLink = ( - - {_t("View older messages in %(roomName)s.", {roomName: name})} + + { _t("View older messages in %(roomName)s.", { roomName: name }) } ); } @@ -139,23 +146,23 @@ export default class AdvancedRoomSettingsTab extends React.Component { {_t("Room information")}
      {_t("Internal room ID:")}  - {this.props.roomId} + { this.props.roomId }
      - {unfederatableSection} + { unfederatableSection }
      {_t("Room version")}
      {_t("Room version:")}  - {room.getVersion()} + { room.getVersion() }
      - {oldRoomLink} - {roomUpgradeButton} + { oldRoomLink } + { roomUpgradeButton }
      - {_t("Developer options")} - - {_t("Open Devtools")} + { _t("Developer options") } + + { _t("Open Devtools") }
      From 8d4ac90265839a98aeb83277834ef8673fc2b97e Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 8 Jun 2021 16:28:37 +0100 Subject: [PATCH 444/999] Convert RoomPublishSetting and LabelledToggleSwitch to Typescript --- ...ggleSwitch.js => LabelledToggleSwitch.tsx} | 47 +++++++++---------- ...blishSetting.js => RoomPublishSetting.tsx} | 47 ++++++++++++------- 2 files changed, 51 insertions(+), 43 deletions(-) rename src/components/views/elements/{LabelledToggleSwitch.js => LabelledToggleSwitch.tsx} (63%) rename src/components/views/room_settings/{RoomPublishSetting.js => RoomPublishSetting.tsx} (62%) diff --git a/src/components/views/elements/LabelledToggleSwitch.js b/src/components/views/elements/LabelledToggleSwitch.tsx similarity index 63% rename from src/components/views/elements/LabelledToggleSwitch.js rename to src/components/views/elements/LabelledToggleSwitch.tsx index ef60eeed7b..957e3dbc97 100644 --- a/src/components/views/elements/LabelledToggleSwitch.js +++ b/src/components/views/elements/LabelledToggleSwitch.tsx @@ -1,5 +1,5 @@ /* -Copyright 2019 New Vector Ltd +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,38 +14,33 @@ 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 from "react"; + import ToggleSwitch from "./ToggleSwitch"; import {replaceableComponent} from "../../../utils/replaceableComponent"; +interface IProps { + // The value for the toggle switch + value: boolean; + // The translated label for the switch + label: string; + // Whether or not to disable the toggle switch + disabled?: boolean; + // True to put the toggle in front of the label + // Default false. + toggleInFront?: boolean; + // Additional class names to append to the switch. Optional. + className?: string; + // The function to call when the value changes + onChange(checked: boolean): void; +} + @replaceableComponent("views.elements.LabelledToggleSwitch") -export default class LabelledToggleSwitch extends React.Component { - static propTypes = { - // The value for the toggle switch - value: PropTypes.bool.isRequired, - - // The function to call when the value changes - onChange: PropTypes.func.isRequired, - - // The translated label for the switch - label: PropTypes.string.isRequired, - - // Whether or not to disable the toggle switch - disabled: PropTypes.bool, - - // True to put the toggle in front of the label - // Default false. - toggleInFront: PropTypes.bool, - - // Additional class names to append to the switch. Optional. - className: PropTypes.string, - }; - +export default class LabelledToggleSwitch extends React.PureComponent { render() { // This is a minimal version of a SettingsFlag - let firstPart = {this.props.label}; + let firstPart = { this.props.label }; let secondPart = { + public state = { + isRoomPublished: false, + }; - onRoomPublishChange = (e) => { + private onRoomPublishChange = (e) => { const valueBefore = this.state.isRoomPublished; const newValue = !valueBefore; this.setState({isRoomPublished: newValue}); @@ -52,11 +62,14 @@ export default class RoomPublishSetting extends React.PureComponent { render() { const client = MatrixClientPeg.get(); - return (); + return ( + + ); } } From 13a2f779b962e8498ae09d9675a91baeca3cd671 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 8 Jun 2021 16:29:06 +0100 Subject: [PATCH 445/999] improve defaults for useRoomState and useStateToggle hooks --- src/hooks/useRoomState.ts | 5 ++++- src/hooks/useStateToggle.ts | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/hooks/useRoomState.ts b/src/hooks/useRoomState.ts index ded51c3900..11ac7de49e 100644 --- a/src/hooks/useRoomState.ts +++ b/src/hooks/useRoomState.ts @@ -20,8 +20,11 @@ import { RoomState } from "matrix-js-sdk/src/models/room-state"; import { useEventEmitter } from "./useEventEmitter"; +type Mapper = (roomState: RoomState) => T; +const defaultMapper: Mapper = (roomState: RoomState) => roomState; + // Hook to simplify watching Matrix Room state -export const useRoomState = (room: Room, mapper: (state: RoomState) => T): T => { +export const useRoomState = (room: Room, mapper: Mapper = defaultMapper): T => { const [value, setValue] = useState(room ? mapper(room.currentState) : undefined); const update = useCallback(() => { diff --git a/src/hooks/useStateToggle.ts b/src/hooks/useStateToggle.ts index b50a923234..33701c4f16 100644 --- a/src/hooks/useStateToggle.ts +++ b/src/hooks/useStateToggle.ts @@ -18,7 +18,7 @@ import {Dispatch, SetStateAction, useState} from "react"; // Hook to simplify toggling of a boolean state value // Returns value, method to toggle boolean value and method to set the boolean value -export const useStateToggle = (initialValue: boolean): [boolean, () => void, Dispatch>] => { +export const useStateToggle = (initialValue = false): [boolean, () => void, Dispatch>] => { const [value, setValue] = useState(initialValue); const toggleValue = () => { setValue(!value); From 5c85ee1ea03097325680ed4574006c559fcccaa9 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 8 Jun 2021 16:30:46 +0100 Subject: [PATCH 446/999] Make AdvancedRoomSettingsTab space-aware --- .../views/settings/tabs/room/AdvancedRoomSettingsTab.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.tsx index f587210095..7e7d9cba90 100644 --- a/src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.tsx @@ -76,7 +76,7 @@ export default class AdvancedRoomSettingsTab extends React.Component { const room = MatrixClientPeg.get().getRoom(this.props.roomId); - Modal.createTrackedDialog('Upgrade Room Version', '', RoomUpgradeDialog, {room: room}); + Modal.createTrackedDialog('Upgrade Room Version', '', RoomUpgradeDialog, { room }); }; private openDevtools = (e) => { @@ -143,7 +143,9 @@ export default class AdvancedRoomSettingsTab extends React.Component
      {_t("Advanced")}
      - {_t("Room information")} + + { room?.isSpaceRoom() ? _t("Space information") : _t("Room information") } +
      {_t("Internal room ID:")}  { this.props.roomId } From 78debcc93b99eea96f270cbff8a5266a10a52db5 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 8 Jun 2021 16:31:39 +0100 Subject: [PATCH 447/999] Add method to disable StyledRadioGroup and wrap description in element with a className --- .../views/elements/StyledRadioGroup.tsx | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/components/views/elements/StyledRadioGroup.tsx b/src/components/views/elements/StyledRadioGroup.tsx index 6b9e992f92..744b6f2059 100644 --- a/src/components/views/elements/StyledRadioGroup.tsx +++ b/src/components/views/elements/StyledRadioGroup.tsx @@ -34,10 +34,19 @@ interface IProps { definitions: IDefinition[]; value?: T; // if not provided no options will be selected outlined?: boolean; + disabled?: boolean; onChange(newValue: T): void; } -function StyledRadioGroup({name, definitions, value, className, outlined, onChange}: IProps) { +function StyledRadioGroup({ + name, + definitions, + value, + className, + outlined, + disabled, + onChange, +}: IProps) { const _onChange = e => { onChange(e.target.value); }; @@ -50,12 +59,12 @@ function StyledRadioGroup({name, definitions, value, className checked={d.checked !== undefined ? d.checked : d.value === value} name={name} value={d.value} - disabled={d.disabled} + disabled={disabled || d.disabled} outlined={outlined} > - {d.label} + { d.label } - {d.description} + { d.description ? { d.description } : null } )} ; } From fdecba2fe54a5bfd4e5b9e6974e1fd9467858539 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 8 Jun 2021 16:32:43 +0100 Subject: [PATCH 448/999] Make AliasSettings space-aware, remove stale unused props --- .../views/room_settings/AliasSettings.tsx | 34 ++++++++++++++----- .../tabs/room/GeneralRoomSettingsTab.js | 3 +- 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/src/components/views/room_settings/AliasSettings.tsx b/src/components/views/room_settings/AliasSettings.tsx index d6e79c4ee9..6f83d64eaf 100644 --- a/src/components/views/room_settings/AliasSettings.tsx +++ b/src/components/views/room_settings/AliasSettings.tsx @@ -80,7 +80,7 @@ interface IProps { canSetCanonicalAlias: boolean; canSetAliases: boolean; canonicalAliasEvent?: MatrixEvent; - aliasEvents?: MatrixEvent[]; + hidePublishSetting?: boolean; } interface IState { @@ -99,7 +99,6 @@ export default class AliasSettings extends React.Component { static defaultProps = { canSetAliases: false, canSetCanonicalAlias: false, - aliasEvents: [], }; constructor(props) { @@ -317,7 +316,9 @@ export default class AliasSettings extends React.Component { } render() { - const localDomain = MatrixClientPeg.get().getDomain(); + const cli = MatrixClientPeg.get(); + const localDomain = cli.getDomain(); + const isSpaceRoom = cli.getRoom(this.props.roomId)?.isSpaceRoom(); let found = false; const canonicalValue = this.state.canonicalAlias || ""; @@ -363,7 +364,9 @@ export default class AliasSettings extends React.Component { canEdit={this.props.canSetAliases} onItemAdded={this.onLocalAliasAdded} onItemRemoved={this.onLocalAliasDeleted} - noItemsLabel={_t('This room has no local addresses')} + noItemsLabel={isSpaceRoom + ? _t("This space has no local addresses") + : _t("This room has no local addresses")} placeholder={_t('Local address')} domain={localDomain} />); @@ -372,10 +375,20 @@ export default class AliasSettings extends React.Component { return (
      {_t("Published Addresses")} -

      {_t("Published addresses can be used by anyone on any server to join your room. " + - "To publish an address, it needs to be set as a local address first.")}

      - {canonicalAliasSection} - +

      + { isSpaceRoom + ? _t("Published addresses can be used by anyone on any server to join your space.") + : _t("Published addresses can be used by anyone on any server to join your room.")} +   + { _t("To publish an address, it needs to be set as a local address first.") } +

      + { canonicalAliasSection } + { this.props.hidePublishSetting + ? null + : } {this.getLocalNonAltAliases().map(alias => { return