From 40b684d7de997f667d858ad13a1c8bb9d373c576 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Tue, 23 Feb 2021 19:41:23 -0500 Subject: [PATCH 001/464] Vectorize spinners This replaces most of the GIF spinners used throughout the app with an SVG that respects the user's theme. However, spinner.gif is still retained for the room timeline avatar uploader component, since it is more difficult to replace. Signed-off-by: Robin Townsend --- res/css/views/elements/_InlineSpinner.scss | 19 +- res/css/views/elements/_Spinner.scss | 16 ++ res/img/logo-spinner.svg | 141 +++++++++++ res/img/spinner.svg | 235 +++++++----------- .../views/elements/InlineSpinner.js | 26 +- src/components/views/elements/Spinner.js | 28 ++- 6 files changed, 304 insertions(+), 161 deletions(-) create mode 100644 res/img/logo-spinner.svg diff --git a/res/css/views/elements/_InlineSpinner.scss b/res/css/views/elements/_InlineSpinner.scss index 6b91e45923..c850191b93 100644 --- a/res/css/views/elements/_InlineSpinner.scss +++ b/res/css/views/elements/_InlineSpinner.scss @@ -18,7 +18,24 @@ limitations under the License. display: inline; } -.mx_InlineSpinner_spin img { +.mx_InlineSpinner img, .mx_InlineSpinner_icon { margin: 0px 6px; vertical-align: -3px; } + +@keyframes spin { + from { + transform: rotateZ(0deg); + } + to { + transform: rotateZ(360deg); + } +} + +.mx_InlineSpinner_icon { + display: inline-block; + background-color: $primary-fg-color; + mask: url('$(res)/img/spinner.svg'); + mask-size: contain; + animation: 1.1s steps(12, end) infinite spin; +} diff --git a/res/css/views/elements/_Spinner.scss b/res/css/views/elements/_Spinner.scss index 01b4f23c2c..93d5e2d96c 100644 --- a/res/css/views/elements/_Spinner.scss +++ b/res/css/views/elements/_Spinner.scss @@ -26,3 +26,19 @@ limitations under the License. .mx_MatrixChat_middlePanel .mx_Spinner { height: auto; } + +@keyframes spin { + from { + transform: rotateZ(0deg); + } + to { + transform: rotateZ(360deg); + } +} + +.mx_Spinner_icon { + background-color: $primary-fg-color; + mask: url('$(res)/img/spinner.svg'); + mask-size: contain; + animation: 1.1s steps(12, end) infinite spin; +} diff --git a/res/img/logo-spinner.svg b/res/img/logo-spinner.svg new file mode 100644 index 0000000000..08965e982e --- /dev/null +++ b/res/img/logo-spinner.svg @@ -0,0 +1,141 @@ + + + start + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/img/spinner.svg b/res/img/spinner.svg index 08965e982e..c3680f19d2 100644 --- a/res/img/spinner.svg +++ b/res/img/spinner.svg @@ -1,141 +1,96 @@ - - - start - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + diff --git a/src/components/views/elements/InlineSpinner.js b/src/components/views/elements/InlineSpinner.js index 73316157f4..2dad9ebe1e 100644 --- a/src/components/views/elements/InlineSpinner.js +++ b/src/components/views/elements/InlineSpinner.js @@ -24,23 +24,29 @@ export default class InlineSpinner extends React.Component { const h = this.props.h || 16; const imgClass = this.props.imgClassName || ""; - let imageSource; + let icon; if (SettingsStore.getValue('feature_new_spinner')) { - imageSource = require("../../../../res/img/spinner.svg"); - } else { - imageSource = require("../../../../res/img/spinner.gif"); - } - - return ( -
+ icon = ( -
+ ); + } else { + icon = ( +
+ ); + } + + return ( +
{ icon }
); } } diff --git a/src/components/views/elements/Spinner.js b/src/components/views/elements/Spinner.js index 4d2dcea90a..43030d01d5 100644 --- a/src/components/views/elements/Spinner.js +++ b/src/components/views/elements/Spinner.js @@ -21,23 +21,31 @@ import {_t} from "../../../languageHandler"; import SettingsStore from "../../../settings/SettingsStore"; const Spinner = ({w = 32, h = 32, imgClassName, message}) => { - let imageSource; + let icon; if (SettingsStore.getValue('feature_new_spinner')) { - imageSource = require("../../../../res/img/spinner.svg"); - } else { - imageSource = require("../../../../res/img/spinner.gif"); - } - - return ( -
- { message &&
{ message}
 
} + icon = ( + ); + } else { + icon = ( +
+ ); + } + + return ( +
+ { message &&
{ message }
 
} + { icon }
); }; From a308a5418323f9529986791a59e27e37ce2eebae Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Wed, 31 Mar 2021 12:28:24 +0100 Subject: [PATCH 002/464] 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 003/464] 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 004/464] 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 005/464] 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 006/464] 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 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 007/464] 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 008/464] 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 009/464] 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 010/464] 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 011/464] 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 012/464] 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 013/464] 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 014/464] 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 d3db5fe77f8cc17af9b53c040d4f6bd4592a16e0 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Mon, 26 Apr 2021 14:10:09 -0400 Subject: [PATCH 015/464] Vectorize mini avatar uploader spinner Signed-off-by: Robin Townsend --- .../views/elements/_MiniAvatarUploader.scss | 28 ++++++++----------- res/css/views/rooms/_NewRoomIntro.scss | 4 +-- .../views/elements/MiniAvatarUploader.tsx | 7 +++++ 3 files changed, 20 insertions(+), 19 deletions(-) diff --git a/res/css/views/elements/_MiniAvatarUploader.scss b/res/css/views/elements/_MiniAvatarUploader.scss index 698184a095..df4676ab56 100644 --- a/res/css/views/elements/_MiniAvatarUploader.scss +++ b/res/css/views/elements/_MiniAvatarUploader.scss @@ -28,8 +28,7 @@ limitations under the License. top: 0; } - &::before, &::after { - content: ''; + .mx_MiniAvatarUploader_indicator { position: absolute; height: 26px; @@ -37,27 +36,22 @@ limitations under the License. right: -6px; bottom: -6px; - } - &::before { background-color: $primary-bg-color; border-radius: 50%; z-index: 1; - } - &::after { - background-color: $secondary-fg-color; - mask-position: center; - mask-repeat: no-repeat; - mask-image: url('$(res)/img/element-icons/camera.svg'); - mask-size: 16px; - z-index: 2; - } + .mx_MiniAvatarUploader_cameraIcon { + height: 100%; + width: 100%; - &.mx_MiniAvatarUploader_busy::after { - background: url("$(res)/img/spinner.gif") no-repeat center; - background-size: 80%; - mask: unset; + background-color: $secondary-fg-color; + mask-position: center; + mask-repeat: no-repeat; + mask-image: url('$(res)/img/element-icons/camera.svg'); + mask-size: 16px; + z-index: 2; + } } } diff --git a/res/css/views/rooms/_NewRoomIntro.scss b/res/css/views/rooms/_NewRoomIntro.scss index 4322ba341c..b75e950361 100644 --- a/res/css/views/rooms/_NewRoomIntro.scss +++ b/res/css/views/rooms/_NewRoomIntro.scss @@ -18,8 +18,8 @@ limitations under the License. margin: 40px 0 48px 64px; .mx_MiniAvatarUploader_hasAvatar:not(.mx_MiniAvatarUploader_busy):not(:hover) { - &::before, &::after { - content: unset; + .mx_MiniAvatarUploader_indicator { + display: none; } } diff --git a/src/components/views/elements/MiniAvatarUploader.tsx b/src/components/views/elements/MiniAvatarUploader.tsx index b2609027d4..32ef0d4da2 100644 --- a/src/components/views/elements/MiniAvatarUploader.tsx +++ b/src/components/views/elements/MiniAvatarUploader.tsx @@ -19,6 +19,7 @@ import {EventType} from 'matrix-js-sdk/src/@types/event'; import classNames from 'classnames'; import AccessibleButton from "./AccessibleButton"; +import Spinner from "./Spinner"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import {useTimeout} from "../../../hooks/useTimeout"; import Analytics from "../../../Analytics"; @@ -88,6 +89,12 @@ const MiniAvatarUploader: React.FC = ({ hasAvatar, hasAvatarLabel, noAva > { children } +
      + { busy ? + : +
      } +
      +
      Date: Mon, 26 Apr 2021 14:10:30 -0400 Subject: [PATCH 016/464] Remove spinner.gif All spinners throughout the app have been vectorized, and thus this GIF is no longer necessary. Signed-off-by: Robin Townsend --- res/img/spinner.gif | Bin 34411 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 res/img/spinner.gif diff --git a/res/img/spinner.gif b/res/img/spinner.gif deleted file mode 100644 index ab4871214b14361e4491cd549bb05a7e49a190ac..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 34411 zcmeF(S6Gwng8%y`m68M!dezW7gx)b!>Ai~@I#M(sUDSjo-O!65y%$4AL``Ud(gi_8 zjf#jJ6cropto6=%XZFne=bE#<_jT=@CMTSbPk#6JevVpM=;->A020s$08lVAH92*B zbll9utf8)9^Z90gUx1{zWKluU^3rl&Pv73Bz38ZDS7%oZH4PCVk({iYnY%L`Z5{94 zybBKvudJxtSl<{K9{Kg-S5jhFFH2b)!kJ?OGD(4NKRhP%-l>zSI4^#@4`=qS6;2$c(O5aYvk9# zucQk}J|}z(j0|k;ZU6lF1N`4#Vt)|!=B_TL#*XG{$~Xv!`L5V^C>tA~2PppIlmGZL z0r&yFifE}Qba|>_$x~LExIX2i!6y2=cqGCM^CAP4n(n3Jyu!NSFvv>N16IVR> zK)S+kG0iG(gqDN^4_rc(&HHl8$u@Tx1r83;2?=tqhteQ?cu=*`ql{MI#U?ztF{w4< z&NqL3XqeUOb~)Wk=*}I5sVNx4Rp#aV@bJwQTB|4jM-iU4O{6D0YzwF5T+570ie28M zDjBO12$$k^O$2bLsV-_j$D~%YtBjIj-L>Ju!&-->^~<|+voW-F#Ym3j`nw_I%eo-6Flo6&LK znYt6?&xaIRi7F>=VPu+8-BRM&Qj0*3b#-LC*^ul$yam~y7oYBTnN|{xyu7Rpa_x=Q@<0RcTyzT$glnS>hCED6-&O??x(V%_Tg0@ABKuHC zW4M+esKtf{;L=4dpkE6hrf6P{91Kb;muw$ey62!HmI#CP^mw&aoGKw~qM`9-bhbwo z-FQUsG+_dvP@~Yq^2&%b#>-_p+;C&A%c~Wtsoaf6Uhl(oiSTV;9lc%47J!~Zq?u7| zcb=UOHx}pGwqW16j3F?74QB{gbgv(ZL}vI<>lGcl);n{YXhms+M4*@n>{L&APY1`*V`Nt0B`SGY(eZqLnf$LDGcX$6iRHlZ?_$fDN_iDfW!F}eFhg7-G7h)r4&dCD)d-x?7Wm?SURQT);XO{tv*Jy6Vz<> z&2nKc#bi8T#xeOuM%dhN0sp+o*uiGstDWyy41Oo3;wWcC3PWXr7P@)r2sODhx5jaQ zgOL3=UIw8dzXS2@SxCNCM8o5sM+e&voCMb>Bp{!v`x|$RG{0N&m&+0CsfWl$IHIuI zbK$^Y1I|3wSA4dk?J8a*u*RU#BI_Aj#eLu_RXkY|h6NBl_mGp(kU{boR4KzN(SiiI zBE<^9?A-$^4%Tv2?4-)t*0Os{jq>q73(M{?L6gI43RhGsS-dt{!yzO}ldJ|t3(N};@53#h)iYiS@s=H9f zYNzmEhEXs3_W(=1R_JJOm?G_7T%F^`k*Y-qn z30`@jwXup4ICtkPbV`18Zs2PdmLh*u{E^$L-Z$uCtFixQijVjRPq*94&;XUh1Mss$ zzV$v*N}+0?$-bTcHnb%zb~O&c-pMjA+0 zAZ?ZKQqx-R_~zO`esI*bNE(u1J=TO9ZLWUfYY5mnx3$pHv%-p2@{uy8%qM5AQ&6^%mrTTZTYp;>&d9MJc`(dv}D2! z3p6O}sC5dj44FuCI};c2;>D8RH+BK++r;ZL9`ZSEw>jKtLyou4ehMy)Cg;CJ zJQ-i#XT6}@1>R##`O@{hPEi5^XD1{J5)v@&ol(K2il>fe@0BWN$i>_In9p_o+EOxG zT76&P{>djr!binE-<;P9-y32Hs-)NY>F z5yFEQd$#uX^4Zaj)(c-R9iMq=`f~z||J`kQMdZ}Ev;Ez6xdZLI_|-atM~lr>FNaXK z-dwrveN)iTx9eN>yZrPr)&!F{$H6dJ95^yPoetTD=;(4Z9Rd=uSHO1swA#Ol~LT^SQUKALmDw+;H!1UDkhN zU9{0LnzzICCila!x7bA>FMLc_>`UIg{>FPx1|C*-E;SddKCLPv-?a)gE^XG{sz`kM zz@Pm~Zj#})%=RbV=NzB&>%G>lz}~DYJd2>e5kaKi{9!(OHR8?svHM|UI^(9$X$9P^ z_DX&*ibwfj#j)p0nEB+V^A(ArH2iCi6;AfDbIYGXpY5@%nt*r?l4Z^l;q7Of-uKRi zm#IAcO4b9h`h_h>+A`Wf9#XYcXG5z}j^TfcW4YFETB8n4d&;jd2A+I@F8 z^~p)4SNr#>CkqEJ^70YDXc7(Db%)LPU-Ucj@AOOiKlzr2i{~%mGG+U>a+$$gl2LVen6}Ko37$%;NHo9(KT2#!LGmTpLAuvHbL+_Mtn7tboz0> zZTL01aNW;87h&hZPM&loWn_AvI+qxBPAaRQ(B@RU^yN@kQDv1$NqlZ?{?*2#B47an zSp^M$_Hz-rsSRE_Y;c4UQoqR?C2KGW5)~87KAD>!eZ@-;!L7E)t)zI%Q+rHw)IhAu zGbgkc-tR8Bw7sZ?S#^gQfHuZ=p!{(wG)t;MZMwzBah9(9MKaN#|3cNW=pf3`)+DLih34SXj#7O ziX9{nT$Vsg0c!+_EhITbn$j~VqWk6%O4MLKJ`{~XhlCi>#_}#Y0WS-k>hK^DFCvBH z_qj;Z3kGJTfH-(vNv81gJ>OF}5PG;tq(sf@6ogUDDf&6r@g3!{+-49cT zKgB0r^yPeBIc??ik1mL`f5%!aSz}z#^kC2p?}WWvjm3sHTn0 zgI31xh%I z!!iH&9W3JQI3m2v15e3kE4NRzwT42iiouif{6)Al*j?C(^rt_)3qade)({Lf%3@$$=+N6n z0-ThydFXRISU7qS4$CkW5oEh#e|NVqH2}}4UYM-x9#xRIT#x!B0G6)9M$u5PqbNQ-Pb^BMKini}$MB>?! z-GF+b=~tp~q-D^!X zfYfvsNO{j7fla1KHGGp$y9jw!y_&(WcK}PbfrMzWcNP~3+wNsZHg$3)qt>Ddoe(yt z1#1s}o_QX4QkeMtc;}Orq%IN+&!PB+rS;Hfs-LDLOVudoFqDsqX2qX;MM^o#t=4UW*ESWv`(Z}SQ;0DuY2P-oqanjRkZIb=X>5!&t&0~3%9W19y(k~ zXCFQ0Z2(FMxeC}=3K-tO$tH!vlTq&;iCF?yALZ7uO5X#S<#}SZ14$g?f)LK|wO-93 z6VLK1-kNV1qo)#{vj^?5Y(6gmaan*t%2eOOKF^9BeBNKE@=ArzbW?POx;)j;4yIj% z9uoqH6N`R|@F8b18g|tV=fE(_YuBBn!3Z9rZJZjT-jzm%Pd-Xf2%uG8;h|r=OgL%4 z?aX3(0?eX##quRwfQ^gTK~yEeIED)9^swQWSi;OpJ3>Xe|fo_+R@tmk!Cj*Jf!*?wsZmIeOZEY#&ho{2(7#-Da^sD)Jh)6>X^wiv^ z9J$0mqetu281BuMD?{V}mQQdFJSf7Z*2b&A*WjL;W8h-cG4o2na5?8dPO_Ev=%RGtkB2r}FU9JU1jr!u!E0bRQVjDX_`&=^yQQDsejXz%)$a4tYDN<+LUW%=c zI@3CO`hNEfyir!!vzxEjSIa1mV)L*!ACL1oh;3X$ex5An+44Nl7B(x|k~}*f;^g%9 z+v73#Tl$qUBD?tYyMe3dpr?B8I^wve3*eTS^Qb$%fAGtoqxFxSPM?y|6E(7EmRDqR zh2^OH+Hyx5-_xHamEI5haOA1p+2S`}a_;C$GJeU39)HXsg1ecS=W*lZm#}XR9aVM6 zj2m)bhflnuky{9s;MW&{?-Rmmd%B?bvn~!DBaafl#{E$pNqCRA)_(yGJ#zQinfEy* zXO^Ccmw) zDw#I}V5zJ=1>>@@)E++E?70y6dlKMcAq5f6BfedPd%|>sWfL9@kX#tVgCYxa7&(KH zaGw7HpQV>u`Z-|%78C_6js?pB=ij5D|F(7-{g32j?zzlW_iyt4?cKjt-M`8EU%mS` zcK?TAm$~XPx81+V`?q)h+IIgY?|=2~-`M?6^Dc9({hPd&PRAVu|0XZjLPAV!(Lc%C zdPbYdMw#0g+Nem0xq0Pd;M~V=h|mbVa6N?!mooeXQWD}*^v+}!6k4A;5gr$j&m?bl zs$N7cPgSF}kg6I#x45&nTxv~RgEve|j~An(+vKmn&F|o#1-n+7&@Ft$L!4K7ejX!q z(+e@Ws@3I@U7Hhn&kZ)mBE7k(wd^4|DocOQFYiTta=Et8LvKNvck}1B*EW98v%xRc zQHt3@QulW+XJwpcnJHF~0oRPl$gQhDuC5du}yA4ZWHVy`uA>wpT~;!Q12^K7W;ja-XWWzVcuOg+^+3R;%oQSErk zI;uq|U1Nn<9uakte+utNQ{xQ<^>1V2RG zp6X)Jkpy?0d@qN5;LZ^=<|y^xN-2-*s!H^2c8$uz9^yEOm-mB<14@eTd_@cV2y*#2 zd{dHq_sOH1c2R||eWhY{-Cyz3{Lk$Qf(8}|dy5beE}?MzgYe&kox{9{Uv3MhOGQCk z_FpAyM}t=n@_X;3xXjc%I`tk+<;Ro=cplM?8Ow^~UkDAS-oCtT70N!yALN zb=I5FT$C?BwoNr%dzmU0EeRGGBs=8ClHKgYQD-q4$B!&| zd6b7Gu8a0k{O2`jsd%jzk`8^{5&%w#PXw&eONkwY%$3-qXwx&we4f!*vyzff_)6S?)xKh zQl|a{wrSowU1V$mPo7i6O`e=wPz@k;*hRx)uGSi}ZOOf{bvtu1&p>67@$)#5{JIE+%fvJ z3sD9dIJj#JwI|g;)?>2W6j`6oUar}!m!%6> zS})b)7TQRP54QB-RF|aWG8z=c>iq zz11>znvlskN>^-_o3vVAf|+DJlYYs1^z&o15)@xAd#QZV53ZK0A)(Y^x`=IJmU!8KsB}v-X5t`h6Pf4XTpkUpN@~N-=SPcH?Gb(ucaesKD zi!oeH&qz3l(Rtxs-UroU4Z}Qva944&y!q$hEIvz&1!yYCzuRZKqxx15YX&gU!bq z!pt3AQNOZpXF8G)%%e>Rk0#78KJ3)@3Rls+_I_IhQI4x`QuYNAkTyFo#1qfph-+h4 zBZ52x6`y8tV142mg@+B^Lf|}$p4wA5i~u^BKYC~ky}!jSj$L1T@D-{&1n%C7SN*I8 z8Ff(qBw*(jt&wnQKJu^QMT%SgB88|x2Ir}+a3B-mBkt!j5MIE2)3@+dLw z$d_19>XHeEvehPRCJ?4A3$SQXlR35ot)SS?h5ER3jKSAyGDJY4JkC;0E}5w5Ln=?h zgNm{cwk>Z-HD@Z74&q@ztD+{TG#8g5+(3f19XC$B(v*BdX|O}U^f*%M@iT6oVsma* z5D0!OOY0Dy?Q2n!#+yh($#+x}v24%WK)m<068WZlJXq$w5cFxd!1a3Y1A3hbgM~^r`9h zcEha2(vMp|y#Ynl(~{fLH6D83ght;`-2jPwLs-o6pUlo2L9h;BZguXQQlUdP-M>Q5 zPDhB;oLPI}>^)@?@FFZbN&l=xt2#gBddQy-k6rPg>xNi57?~F&?>WdNe_m0+-UD(N z8+Y!}i>D91X64?yrWN*bC*fQ$q*N_93FPCNb+@!<5O&rYy zg`E)s4cgy9?c5~wDIaf{f2ngR-!Nt=wd#5%te1E5z^5?f8+0mDmZR3v@s79j+%+5{T zZJ7sy!a0FML?cn5ArOZv2?7&2$@1>~Q{t&*e$3HQ$ zsyT5-&hO8kNs}+}frEoD1~6jXx9%N|Ju~q6x9_Fs+aIerk^d&xa{%UFf;Idf1^XYP zg#WbIGUfSS7u)}Ku4O`(IjS=E*1yw)|2W$IT_^mf(e@wF!$0eU|2W$Iw~OuHbpms= zz51`w){@&U2=gz&ss{FYI`TcTvr)b>zmm-V>x=UOQ?P9k)Q=?(ljdK!(BdN$&!?sZ zgvx5g9hcQezf7^wJ`q;TmzQ5&VF)9g%!(+xQg0!|4@T&+rFa)+Mik`+NFi`yd>Fk3 zZ$({%I^wp%8LvVr>3E$78%SS&R$oi#hNn1PVHBaz;ZYTqRny}Mo0}KjN?dl+pg(*s z?)Bta{9QNM`Ij$2%#$qs59;nvA4k89X7iK#N>&sV8vN3n-`G+ zNg*sl|5b+<0OFPc@$84u1r$DUqz@MuKQ#@ORB3Rn16?`Lk63MTi=4=CHc`;1d5?Wf z6@=F6=ezbzMG=y7MSag3nAB|A70ij7z07f;NYTgyjo=x2`~tfH!LrFcKUZ zi#f!5=4qevBDwDM*jY*KhM{(|ODZ`=WX!KeK~HAnFtkKXE7C4k>^ayEWXYH^mw(CX z5br1!YLxNvP}f%xNImr?BHH>#^vu0?u&olI(^|PJvX)Id&H+CA z;-DoRF70GxJ5X8@jWYUH>G{qkas5Q~SB*1I2pdgUhpH}%J?oGNeo=HF*K%JG(pk|tQEEDeqT_m0B8 z`R-nhqFX*Vj(G}ep}-=`&6eCoY#qsL;T2dEpO8VUyS(#>T2uJX*P1yPWCH~ZeAw)=Szy;_|92xBA$zgr(hH7C1xtD4;KUa@M zN+GC|cTR@`V9j7B6F9#A+~R%2I1uS7MI-br9$07fHDuA5WL2DHb8PZ>V1q)04&3c{ z1l;j4h6#FxsnD^PX9M*G8}ytwC|r-jXAZ=VgFr50#-<$aOsZai%L@yV7qJ?K4N^ zTTyeSCUpOVt1|*@#ea?&1MThgW3X)D%c7956OTzJ{ZCOyUHcep0*qV4VYbltnTN>_ z+2{x#Hr))-SVc6c__!tH>%GN~uH!Vg-VPa^^H0Ibg^=JWcd>Bo7CJ{1I)yEV9j1&Q z{bz4r@T|6LwT^Fq2+tdvM+RU)LhICYosn9Ovz}u@9q+&ekriudQfAW~SaW|2jiYqR zRC=AN^Nu7m*QC?zrFsSwtZomkTpL$bb^`IVMcYT>-&tPm3DG1$C*NI`QlK7J^WG&XMkVN%gclnH`b& zWy`gw2(-CzNhC$l$)3-!cfz(}DP1idf|fW%b$3>^RLA?d7RBFndEUaJ_^UyAel1U+ zE*Rb)Ue9T>Ys@KhHf1V+CM#%~!c%~Q&5r;Y&2qD$)@RFkXPUGIr)N(me!jwG#z+$& zPTc?;Ns|vr?6*=6!D~!}i#NSf z0BZ-%g|kovwAfmM4jS^Qw-)#4$<3)rs$7Uw+CI$}Fv{j+xb_KE(c%Ml@l zAIqyl*rFmqst>uM(W^StLf?>x@DPb(Zu~pgxSJK?55G$EyeRHyKfi!yJwd!DDfzO^ zE3n-v`!3{T8omQ_%(53X1t4m)ai7hj~;0So=N~f1wi{fZpV}h@X&m|HR8RL!KJI zH`optvs>>xIRsb@L&OR&Z*pE)zrD^P$J$E~`~@7a^p!aDml-?8(ZzdygNF=jq8mRZ zy*{LYpCWBVv-V|nl5nF?n(c)ofE!XyS8u{9 zMGf%Z_gdr*Og$Z}=CUx@v#SIJkFP)ccmc#AR8|@;8w`73F*;Rh z2opu(Uv$HxBNy-Ej!(vH$2MI@LrI^GJa1lms7#5IZt~-+?x?AE(fKyhx$yDgpJAu{ zQ)LtrZjyut^;z1X1f$w}hL>^zbom>k06UN231kZfoFw|PJuZSc)d;jF;aiif%#iBz9RUkG? zfmnM2ddi{e&ada$I5O_O2Z*yd9e$do>jpi^dTri>J(Q8mIfZX#@C<*b=gi!6sYXAz zZFE?ilyEQXBtj^j)FZlc-qaKSFjeJ`hzS5TNWn7RXq$n(aU{$;+Rxvt;D6g&@&B&X z^uH1VWmKhf?w^wFS%j|0Z(khdx%$AmkRAyzs+*_GN?O&5CQ>)CPmKhf? zhgW7%%j|0Znp~M$Wfrwe?=pv1W>L%RYX1rim}4t5t!4HF%=wj>)-nUzzuC(CP5J-D z7s~+&pPZ7KmVPNCGb^1FVF*!B$u2A^E-5W5Pt#XIT}3I$RMa;#HWl+<)kJAlDRDM; z_w<$=sk%DYZrC?+^CoA9QhR6h;ahj6>Sd~H+D56gwK3_IrdHI&ZsDpoe}3aT|YmerEG&zD&+l2?-- zH-^qU57f&~5gX&Vf%P8V&0K572w(By{;KGbcA3p3x=2QC5grfI&m7KBmzXsd=98Zh zi++|>QRH4UzSVlk$mU*UXUDGkVm6m4ccN`K@VY@%r}OtC`u&D^8wtM)k`Z=? z%0&{B(5rD4urDfVQ~m8)zHpiS>ZPwS3VMs27iXdp*00OTKPf3hpMKd+HGgj<*{t^7 zNjv|>N{7r%#=U{aya#t+vr{_#^mMDz;=xgl0DN%2oMW)Zo{3ttBOj)9Zi@XUYF>^1 z?#yD6VcnIoOwBRz`QIh8;@Pi{ikM#h&M$aASSm^|wL+@L)O{?_(Kv)dyeB7K5rv;U za#u8Ciqo_6rQjJ+d2vi?HPzG7b-^uu3MqJj$2mLO0I1Ux-P5*ol^P&I#P3fWm{mIJ zj^FVYe?;H5N=cjXD3CdN$nVgF&{mK85dCl>iBi({O<0vz)ER63LNuz{yGP0uC@&tt zjRD*N<6p6RwC6OZ zD372mK5c|L#-VMnGm7B!A+}NU$D686G;`EUp~Y|py#WSoVrO+&<6eeRQ9t$cb}x7TsrmYOM{#2qdRYLwNUbuN#Wqw6$}> zd`#+2*?zyC(sKzY6qfbB>nAge(7gT**l`|i?_ZfDS|Pqy1i9-q960xohV+Zt*{X4Z zp^?bz!(p8x$e)x^Deh?f{JY#bnU+3d!?yQOnv&3pJ;b1obzs`L@MoUql80M81OVO3Zh3p zbR|a5_jW(H|FcPF>_01A@^v}~2T}ge2kIsK zuUho^c%XE`!P>Hcvis{kh+Ah73McyU)XGlCJMU&WAwdyZ*p8vrE-ZluA*;My&E~nA z8Dd*6v={;)GBPB~Uz9x9Y{`OWoG}z8*1_u5EFgQkm(M{P4u6w1M{E%EgTB}CkFDv8 z+mN{Sga3HdUdi@bA%FU7&I=f9M^i!RlJZQ&NYr{v~q}4_0N2JN?c!JdOk3! zw*bycTQb6rC9f}}<7|sHFGHFj;fzZLIkILl@=k~N?xzXNwpeK2UCc}DOT}(3LIT*z zk*9I`7%CYgx(7s)p4UC5kf8zS&oWdGuD@s?wc1p*LZXLJpTt7NJJIsGTBMbpAFgm!omZ-8S^ieraU1e? z5~|1By-B_Xj9VG!FwvYd|`$Wx$ z6C089)_RT)e{13)1;vUkE{rvEPnF)UGpvi2dU-59ntiFoWOgB%w>k85$exq^J-uk) z@HpQ9-xQym_Yb!xs`V_Pyt7c`&avLFeNABcy9Xsveb+i&g?xC&Swd^TYs$Rs@;jbV zzgFBQdQwA#Y(T6c(z%BvNgxNiaTFKhF};n}lwC&h^k%EVuzrEFGQXV7fHlu>JPWdh z4AvWj9Plq|x9i3WoQ5nzeo2o)!F)KV3HE`F0pli;RTcHdE%T57gRa(3zKK@vuJ4Y8 z3+Ab^J1;yt^peCP9%P)MOYoH}#)D?7T0wno6)SA@A*BrCoKhr&zo-PQ%?e^MsU(Qr zC&4sW0cmA4=%Y0aQaUs}o?+Z!+eerVX)-=R-T||1?$3&Kg-W^OR_S-~aM7XECR`y$ zQV=^!$cd;VSR@I|Lx3ZOXl#5qkm<2bnAkv1Ui%D5EUXTqm(hU;y|?u#66ILWYFy*r zk!%_bz;tK;yX$NyPvtV8k)hGZJwvJv^wYBr426awUy61xpb3Tm)Iy1XJ{akObhyW~ zIpvZIPt)$YUhQX%x65FO#B+UC}IAMiq721JCPAr)cE4fo+IJNIXOq6G>M8`3BztbPne2K-&+7QguUgIOSDi5>$jg z9$6rD)Z4u@KC<&-gX8-ksyciG;tTef z*mJQu=n#RaaxMw&i;^A}lc|7W%{kAl2&E{nBsBm_2zw@Tdg4wfRF+k##$O>AybQ;v zP+|Z)SlW@0g}v~?BeI?fx-|iY6V(qFvo@@TPI!=&j+U{3ETd$!Q>Go`!9$^H87U<= zh~&`=vbb|bI_yXA5Y6ULF2~eqM=-7>Jnv3w9|hPQ2$ggMI}4x`&|vu*2!;_%!La`S zqW^y#`v0F1Jwj9oqW_;n52qchmV zS*3cf=wV--@KNor8QD{!ujUHjg{bwVuXTd@yxQ@@tdZ-5*I=`jvNK!kFA7gM-po9|Uh_9V(P_L47tH0-$8Ek3?U=N7!6`*L6jSpLOb z9o(YIyNrT5n;ed-E^^PXul#;#D$yt6)rr(>gB>4lqUhvR1@xi1$4(echG<46_UJR2 zRn4YS6FH$CTJFlq_CKKxBw9J~KQ+tMJa}oXkp3*_Vfc_`$ux_?4AmsscX7}fA#9nz zW-FNZNleUw9%qx6if=%xk9&qpvq)@G=#_Hs(x+Jt`pWXI6#b$W`9b@+9K}@LQO8i6 zBRiuIeu1nL<)kI8yX>$4N^;}bpoQep?2fo{OHUy%)^Y}Ed6&f|av~_Qw9=8@Sn9{F zaJG0qB{#oH5#JylIybPye#8CKD0nDcLt2V+k8+A#cbq*8j`FD^#MF}uE=$p%YonB& zB-(VFrTZs3GHO&_rm5tv*QnH1XXvVoA<}vpte{CXr8bNJu9kfkZs4ldM)wq(czx8! z$9z^E(V!~g;v1}}`vu`Lu?O~1rl=m}8{xfW4=kqRJ9pREsZRaGlWIqRiD0pKFmi|1 zy;N?>QOlL=X#3JP&Bh_?mt(JD$e~YT;Fq^Lx@m%&G<9k18i`|3s$X{0U0Hs;4IdWt zKQGq~j#;pcVSl?pLt-47FvD|ENOc--Wzrk=l~z5s^FGhtVk#~CtG+erD$-8Lsfhe4 zQ&*3r_8e8<>5P1!Y??HKs_jhlvviR;3#^w(%!Z?ke}3vL;K>WG=n;p#&$jv1kFk6|2~54oAXi4-;PB z-07**mHc?0MgPE>@#)~z7H;%ulp+4`mwiK$-0JJ9g_O(tUmWSrCeITFeN-r%FAjcv z`^dZrjI04zEHgQ=Z%CZ@8Zh@14fbSSLtBT=#(|@=JMSd(&0yh+{v(133n`j4)sYsA zl)psp%dSrCL&=(HaU}bm$1m!Fm9T&jD~W~JVtVKU?$VKWV(1vpQ6PHUN=G>F>gfq- z{x2T^qa6}kqo;|yMP!zzVjUZV2-ze^r4tY@7(zR==}PP+Q`>sxXJTsa zioQIEvN+J19Y&h)0rZz^? zfG+ds;*nQdNpU~-7m|-kH^5gE+pMuuhB9QVuNL(6UeYvXzZ#s$W%hM|!`v9oXWSUM z*DfN8QzYshTb-YC$8Zzmqd)+dg&@bGj_Nw))d&fWx)Xq1bw zqd3@5@IBI`GfDnA5P?^vP!RqN_B8W|Fna&4eu*)%tc3ja>dN@B0W;vq-ITZTbr4)^ zRN-d@a1NI>i)UW%rMLQ-5Gdo`t;UwqXEv6qp4Y%D_5p<7On_iLVjAKOV6zySRe4WsxR~AY)1FtZax{tsOA~gJY~vg8r4=PDOt5#p36|EUGp7;PsAa@iFLVS+h7}Eo^#AZCm zpm&-=T4lJ}&sRc`sSnX}=>vT$cEQ+j#`;hRo@4oXvZk>2iYx)}UmyP9(My8=dGG|r z1AOGTS99le5vXky>5rEfUFkV7*mtTbgPo^xj4J~Gg<($zmL7v=F$Ql-aO}iFp24Jt z`suq|G!`6b1L2@~@9;q=31aX-nq?HK{KQu{NzYI<<9eGaA;nzh+;hq5uj1gED2_pZ zbVoY+j@(Rm{rrzDYDw%I+3|f-Sk0PpH!yR@-j_Njvo85ZdxAedx#`}ci`*pLv`9k8 zp^a;u-50bj!gc{(pMzhFrzQ7T%xbnqkDTX14}QNbb1TNUkMxSma`(VO;f2N>Pgt=| z;**G5amK%Ba64_4oJ07ONPR(w4(_bjPFH-Thac!PKQ6-r_bp21%7-!wgLU7|{YlQ` z*9MpQ$jYQMt`23;BR1cjfBtaJxRN#|zlv|YZk!M)eDO&MQR^fB$xk*#gRe8R@;L3Q z-?8;FUZpU!9~`gd(K)&C46mYbD-w8SgEx3zdB?0#<;0)8@yC0uO`vjYlqE~p>`~Sl zmNRPT_(QeCms)X>3km6z`29Oz79CavLf~psBqj=EKum;E5s-{n*idZQ0%T#(U%4iN zV;9VZ1qsj~%Dz}~r*s&vCG*z~t1ub@$3m1zmODIA5~XoSBH37n{f!6NB0jaeB*ttg z)wYFKd;g3zUs}Ts1dLC;)&y}cLuxvn-M9mOwwg9Vfhdxq4S`Ryf{(s%-|KqbQrYx7CP0Rmw zK`=7iJDy)Q|DUxq_s6i^H+2I4xFFacbMsqFF0+;v43`ayI4Ss#T3Y-hAvBqonO|Ui zDk3EgK`FdaDIQl=Qc+odRFDsh&}4J==1w8!)Oq081Z2VLS`Gekn&Ele-r1+gwcfDf zVJE=pH#`r=Q)N3migUWXaAj1bx!#khCl7m#O(b9Tx>b^J=7}30bqnFQvb);jlD+y2 z_Q6$}G0?aaXkM-B@9s<(;_1qTU33W=@7)WtxJmrh1%&e~APh6C)Tlo{wF(wTy+u1) z)mj`9MoJrtKiuA4N`}JT`?iB~Jr9m%s}*PT-QhUQ<)Ar#6PGwHG^Ve~3b%8v!AuZe zw77C@r=7wGYi2}*eU6mpqON|5HxR@%dWdsfyZX6crGra=WpE`UbDVdz%O$-4xi?S%xi~xCO26cNvMN+O%pb}QW4~2)nPy|%Ei7enSzU?j z9xlGHZ1U%WLEE`ju7(C*^NA{zk9we@3lm?nN__DG zV`#{N}0f#Iaouw_PNj7GW08Jwjsf}3SCx^{qH0s2u) zY>lOViH`SdrN~1e;Et@Izz9abL`cN$YI+r>wnkc93n=o(2x5c$j%IDOLDhDPCglZw z%lf92oUMsZMIG=ZEj^$L+uwj9Tc*AQ>u6FC1*}>eKV&3^O2m?j5W{;Bh;{e`Ejo8Q zGpIR|=J~LUdu6ir+Km88aL-fgv)dwaGu?AgG~NF;bK&pnwRLB!F~9dJ&9t6qODNIuk%TQk9NTKoLH;u|u0wB?p z=gDzB^JBH(#7EVOs&?!jZy8CNv1>KAiD00n`u@&(U6V}LYh(_Yv?d|?4rqP4_+@PV@+`dDRqNt(&kXO=5Qdk2@4Q^rgnZ43j(}g8l+%_|b z_8B=A`_}fzj_8a9^bKFE&NlE>#?5@=)j^(4&(fbehr0N?UcscLy+4~c0Sz1tycsb>=kb1EuKL%Jq#ZS%#*Ls9Nxgoi>jFF+JXHEg|EG{856B7>v z5O1W`QTr9NA(9E3xyEjlx>k$E$T zGp3H2P7w2U`9YwN+GAcjUtn+5L3f&WDRe}D9(>Q^z1ZypDIpyDW@=82e$$UWqWmi; z9C6x?n`@<&K*!6)tNBAXw2MmHGOkH7a}fP^}vkeoXAb|bxe?ULAh*zFLXOc)IT>kd0+{Ex+eRn@a+tW zw5OtE+V4W7>p>DR5sHH1ASimwwZ}yc#K-iLtsI;LaBLL9KSbpm4w5Fx;=!*`AtpF? zerw8YJc)$f8Jram$HBfGrU5~PN1Jyda$IJp3Q3ZXTcRg9mG3P~0}G zdL%a!On%Y`DSb-EcmXdvCcd%ojCVYCz93%faj+5rV}TpxFh+|T zKMVOWW=xR%0-=*x7qV*Vj*)%^(F`Z?ob6nec^`$4S-YEwVJ|+hq>71$uZ8xFfhA(t zP^~3FEE|6u@qk)lLZx17TkzfE`v6!{JRA1-E`7+S4Ju9r@@yg?@I$Uo#yhq1-)?~U z1R33(AHvQ52%xQ5ZnJP>d3han0em`?j%|^~^Q!EHHF09AQ{E?(U`PUK^kLt!6)o&@ z=gVEA{a9{y@NThO2%yL=81uY;&RTD5@fSU&3GeTPk|%h+o0tJLXo7jji^~WVebAFt z%~A;Y#DWvC_42jQkg$3l+j5WRhl$ioPxB9bnrFaLHJinIsPX!Iucq?9Z3;5cuPHyH zo3xY9lJ)rC?R&uP9Kjk^RTHUJx8}PqEjGRO;1BYgoPj<8e{Xs_NEJ`N@JCT4U>)xh z&$~Up*k4F*(po$LS}APX*~1DHYNJa0G<~wSrtAJH=7Zu44)A#rQ$LVeEKH>|I$f=Y z%)}+6i0$n8snMAECrXCL&ZYPA)?c;xgdh5_Nln6cKr~eh1VT#Jcd>F1HKIw2R-%-| z#or_NoZDK@&Ngv>vV11?Sgz0}n3|R?P%qq!|Do)q5r3WyD4r8ITB|f@O?~ zBvz$=z0$gZE*ow5a_d&&jW2VYseK91KL1k-|#wwxZ8)G!@y<}q>;9@k23Rt{Rbh3tIZL%D^SndmEbfp)}OF5bB z+G?eM*(y&~9rd&WFS#vC`Sg8#(pjEo#)E?y~~Dd3m#cu2>O1|51qlNrB<$r^T(3499j zJdTfX*8w5EK10#rR%gL@oqfB2VzrzsuKJWK$Ro}Mi#FU)MMF{u6Tm+KIx;VGt(@yHVFHQ9&CEsWq;UCx*yqB*-;G_-uk8G@Ce8j<)eFHfr^ z%Ld%}@@?V#r{CXq*-2Wv+gHbQyTNa(ukY@xw~bz&A{g9R`^7gFcIEf>{B>_WX)Yg+ z+J&(H1Y0vfl4R=XWtqZLJxqy)GyK{r3_w|hg~IYfS*e*vmslj|h0OSwwE;9XfP~88 z(2bbHW36z-s)uAft(C#U-V_i}#JQ@7rl9OI&2F&|8eqXbmoyu}pzTjrO#iaMC4&_W z#;N(m!<$}LGt{ts&KPkJpJuX^bSZz5JsRzqVBH&^aR(|?^GfjH_ETwaeeAZtxZ(W* zHT;th=-!YrjAOr*vWb0?N`FbHhq#rlXe0hbDh?T1&<#=2K0WUV)826F!yBwR$FG5S zXLf7)4q8OeF=U+ey5qqPo@H7hLH4JMas1Xp!Al?FePg}D+dz@!r|Y zXQHbJNr8{w&=lk5-_y{7KpEDFm`RzXa&J8Vb}LiLu%C$1VQVj&;drL}0E`=1=NB3W zHSQ+`MbjSt#nH9+*GwmF!0AE zX(kNpPcrHr00m_2Ek-^+W%9j{CbN(&%9l*DdTKloQRGx+P(K1!qmm<)rYh%$S?K1Z zbV&rI?2B|QvDO_tX-B**aX{Jfc_B^EZdgorjA-?)&@uK}4iwKbgfyDlbtrq%T#3o7 zR6WY3-8{NgaYCe;$AeIqS}9cHq!{E3=Zv@bQ6Waq_YR7KG=GV=5cu=c*n~&+87}&8 zJR?xagiG6J=F}c}uBv~WVt+kAp+@UNu(K|iRukj{F<}P!{4q$6xWhR2ZWd&FPt9rK za9*)_2*`_X-o0cn7bH*gl)p@I51e|zzt4^7t$$~1;-~?VN{ ze$Kq$_n8h#pfX`f;Xt!Ta(d`i9p?z?n1%rP=~)AwP$(U)Y3GYs!SyBry-COokc^a3 zPCjRv{yp$G&Q%_M)%=}}M);JC9nej#YTyYWzG7Q3nV>c8yf|rUFZMN9-Mqm=7nrl^ zBtnb)spyrc6h3j{px#k&xibXd;s)a~e1=pK(MaY0IWr1AM zc%YEeqZcf5R&~n;MDr&OTqWAWYe1lj>}JMDYR5!fajo0ySboP{sQ zzr^NboZQ1nd(nDduQ?2}1Q#~y@%qNBeh?@rM%)=kly2SY&IlC3g#u<4=aZW@D4)4+ z2C-f*MxJ?2#-&{SKB>oB)OTz-!Ud3TaMkv8QVMbRWvx@V@(S&-?7tzut&ax1Vh-hDY6%>Rn0$27X@p>>;%P!1({3heYEu?L2=L@K3fR*m`l9_kTTfl)608UlIu`rFp2&&tNUB4aHP$qr- z0wd^~>V1FQzSws855skDtz>t;J~a+J+xrNw`XQ$FAuD-0``|Z4T;TYD@VgpU#*)1> zIS15al4|bpOXG)l*~fo2zrGNPxFpwpP}uF9a+hK6$B^gp56)Xz$h>#+GhO|9VDHk6 zP{y{oZ{MW@2W1@Z8YlHTi0AeeUdhhkh{G)!42CZy{~80C@feJW#%G6f+(0QaqOQ6} zLkYPDF^K-#^15bMEhDbxBuOP+?WB;e67JU|W3T@RXIxN_*Xec>Z=5J`#tZNAlgQqKcLGM!ML5 z{b8wKtiKP#(m7v`BaXS`(YTOd8X7YI(Yye4ypj`6eDk1YHx-1{q#7gmf>`7bRV{U@ z8$r!svnx}=q$yC5Ri;)`>z_p?)+IFu2%(TGZi8^Ci-V;PQyIm9RfMQ;B(f6a@u^ju z!w67RvgWZwY?NjxU#)u)EqB$_xG7B$hUn11R>9I}pw3;Tvk>quf~>98env^n_nhX6 zT?XH8oS>8Eh}X|BCis#n%T8` zB4;gM#NKKRrzzpB$8?^k32Bg;caLY{yQ1<8Sfja9OrReB;9^v*DErqO>olZaZ2tC3T6)wZ>L46eNTDr-b80trsKfl zzEv0Xu~5Fm*^Wq2V|qh{FfnY7pD$Exq|~0b#S-=C(lErr&r@qoJeg^1EE|L3`}o}X z(J0o(XN*=oi7d|PU168;IQ#bMvmB>7x-dmsM$y1Jw`d<1@AVq)1+LyFGK^Kc* zruity^LDBnGVvL|<}S>(o6tR=x?`_qzj6(gQ*%Iaq9IeKc^P7Cl^<63IXy@rOJ9j# zttqp4&9iP(wZ(@YAXm*^nt(J7bhf@s zA~SoxX*d_{W&l?_pyH+C^KmknU0=%Q-DXPpb2d62|DW@xp;a zj^D+KR^`hY-IFj)QP37;+qjD`D|C*8xQdYJUDuW&Y)GmKCd;VKrwk6`-0?At8%H3X zn}+`s)(|??=AbZDcEPz0Q}(@7sNL%R!+txf8y?9ZLe%Y>i*>N@f@Im7xnj4>6He?= z9e6&Y*q%K@J>{4-FKrYFh_XX~ z^=vG$O09;L1RtmW+34Q;c7!Qsz>YucxihhTkCuC=^DVkQPw>K>qp6}A2q6&)V5HQ8 z38{Z0X*&CSZ$&d7m<{q6eKfKDm`XFwSnWL%2*=Nmj-P(@Qg3%ue1xjhg9#G`e_qPRB7c@H{-XZs^aj=K9%DAYRlix592yihH>miGlI}Tj#^4xEVCYQrJ@fEj z9ZhP_#if?qD_$(*ukQnPReR*0d`D5=p%14WJQX29m-U`GOXp=I6)%xwdjN|k9LR~{{ZhoH^56~HtAW_%)~c^z`^aZ5 zImXSbY?FX2EZg-@Ll*x0qt1vUDI)69FO6sP42>1YO0v5h1K<-q(JD0U3^;CFZ&u!1>eRGMV(BR{F=pqKyjT3yARp zNr4vP7&tCWqAjAW#JpWIi2zS!t-%Ba~EhUML{Cg?oAFbYho20qJCjZ(?;dVU#HtYT6kk(Y^)-eCK zk%Zd`{oh6sZXxvVy_A2ndjCjCxZTqKmz0Ft@%*2-2p1IjM@o|1Xe3?N^L?C?i^>Aa4oJ-{{3cMYiV5vGc)rM{*L-*FGb|-YF zt{U&NL8gshgBIU(y+q&bYwp$0I$rVOC!Gl0e45>AZfc7YhaZiOZH+m+v6ZRUdjI=3 zpU1C8y4?{o1u4Dca>HkC7K%I*ED!ilZ|?cqSQ%17{ZiLHzHBGXp&Ktvg!B+yO^x(Y z^XqCZiFRQ5Jxn)@)2U{8lGpCz2wv%DR$Ywvfh@qD;ck<7@R0Xcy?*IbzaFa)%`+C( z4Uun!_kd67IclFd2)%PSa>LqH(4|9FTq;o7fl{}~ACT{qy*--9RE1)W%yR0~_lvA< zs%cZ5@dw1i3)O5WZRHM6H|@M~*T8;R%rb5p0p;|RIBX;Dwih|pmDQfRrHOk#UZ{J! zCPi{p`n`_e*pk(?xQF<@sRip()_OmPR|VPdsGIa}*&9tc{rUX?H(NfWk7i|1Vo*`N z3max+vRBg70XXcBZXr3Z`A51vYt?SzFAPOj_KwL3bZho}em$=zJ{(SmaWY7jp6lpI z3F;=2g^p^St!4$f?}m^86jZMOiaBaF&H@a=W1ez*Xoqfq4l$#L1myS;B3f!OSD(V9 zHH70+9qTe_kUeH6E6~k;4VJvZfg-%#2NuG+kl=)H2uyH=uPZMQ0u%>B-I&;n^-q z>K?r)(V)MzMue{7i$2XCKXWA=Bkg#`7wxltB)e$m`(()p8z=T^e*~U=~;0 zH~v&4e=*+$oVA^jfNhYuD1N~>hz7zkC*BiUkFkl`u_AVF1@0#$X4aburQ3ydhgh({ z`32hKy&W$;--JBNmLH?{>tqM5Veh?ZAyrN}_co#K^pGqea+7vM3enmBAZa(Kx#l-kpAhQ(_6~_jCjgLcedVJH zSYJr?Sn1KbRM4GdVG64_)Px#c6Y_TXTmGvYiSD7S1O~A`#G}IPdgG~r{ZE){MCLAb zwzVGePOinO#5Un=H-IQN2`8O_05pVvq`;*hh_s6$+U1I$2?UxU(?Ed<643eo$prVxsOt2t_ z3bVt3kt$W3#nfZ`>Ff+|LmremnI_)J2ER;{);hTs?}L;>Xj}2AGH~%KQw%UIZ%=jq zx&-0kZM3g=_u@US+I09fN_p&YCjpm0<(218+7^=10HBIvw*|$L&144$laHmBnX_R= zNQHSk+Q<|1%;BxjSlo2-3)JtjCcT z{{zt@RSQ_`?f2PZTu^+W`9=oW%+4r5kZCf}K>Kk4C)?eAC~k!qw~;N2nrj<4>ch+~ z?E!w1o4|j5{ghKx!TNKmZ_wq@W^R3YrTX(Wnx4f<+^aMYufoAj+f&tm0D%fROc@F! zhKWrK%_0&8hM_B0bvjICoFf?^UP5-EzKW6(Lpe|N+cgmt4pYDHM_wl}JK<@Bw}Kj# z8u6%!r(CZu(Xy|4JTx1u7+!7h;7~56COzR&j`a2>KV5R|3j;>=vsMurGf$~BdYKM^ zfvMz#JX~3Rc(Fjr%p>obFKE4X;sH!1O)zJxLa_$aV7cpqkB_=@8ZYE2ZBS3WDBoPs z)%=biR)4@w9mD9+9u9>KX2YC>tGkDZN43Ha-61fPC+W4~S6pC%RCbNYNG%A=QO^k^ zrsKcNfxW7r_-jtpN=Irfuh-M6VDhDw%mM~Qr8ntW?>(D#lFMR>LslMxM|gXUUxA-` z3~33^Vb$;(@I$WV9J-@9uEckgY+qdU96&JfTnQaWG=>Ut&2p~Ls?!4!gddQA*m3*_llGDFJUU8^O- z>BKg!Cx2_F?cs?3M`ZG&gg%@4V|z^!RI ziUD$ZWbAq`6@(!Z!OPBI2$;J3<(Z3v*%KhMMS6vI>{Aj&Z5V7|1>?KThS?wO0S5&8 zqT$3~D-|IB7$sf+U#NwYM>kTuOJLJ!1OE%*YCJCjFHn=~)5xtl95bfmr#Oi&DwVn$S)!z5tiQ z$@iP`S3g^kM5>>r4v?sj5??+eN2Gb{m)p9J6C_hwWH8T0J|%~)u3l>!i#!??pjDwQ zeIQ>=I5{#GQvYHvCGMFQR)bH{wmxZ7LLb^6el<6Zsv%08-DlB!O01)y&m>mT#E8U* zFq>bLpC4O$(fH^>_+99it|vXZ56(`$2R%RXYN__9p-4qjV+|AnfHiyole1~W!Gmt0kL8nsFyFN+n*Sd1nC)*5H#Bp2Se81CF0 z(&W)%<(NnxwCxPmhyeT1))Auyi;0<#q*3dTu%fdI zOK|^Ym;cYEXYTmOzsCExdGs$C^FQMz-2IV%P55ymDOV2i-;tENKl1m4-H-vVY%87u5*MNNx8xFAK5cE$a1wKT;~Wkm2$J`KdF?PhX3|Gb5rTx z+7YgEglk3lTlxG?F#VsV=Ylv4?DoXJBkB2$7%io%|B9qiTI-Dm7lr>~msewEwm&6v z*=3R75W~=5k-ymGz%xN7RMfcavaQsa(3mrSvC9@|bjJ0Pa%&A9Ni44b)K~p@UWQna zJ4zD9Z;q6%@X+VQGG(xPi;Uv#~MB zDHokt8O;yVqVBoufktsY~O10y!Zq-N6RJflMz)$V=Di{wwwi>gn z%Hjcm8qW!I4QkuP>v>3g7W6=uAND}k{}`oBpaU$USYN*MU%NAUW}~Ypf#ym9BEY6p zm`6FB{(I`B%EnU63V#bvDNl+p_RIQ}mW#Fk<(4b%~cQ*}De=WCj zO_H;xThY{sK4G@fvjo4L?^L7OzCzf4I7Ww_Ym}1ym{9f9B}-n%>ZB}LDzaw|?%b?; zRcl;rwgNI{9XUNzk-+li%dOlyOZJc_?#Yu#c^a1xPkWsB6hD5l!z@i($n+IRJ z$g9D#j|DzUd8nipK;h6DGBw;?kHbO;aoPdQtK+n8ijoPZi_9+9dUaA$zzS%k2LPbfk6rtMpl*qQG|oseG2kwj$(8RSF2dT>2oP8@2|7LKN2Tx$DP=&BWaGviJKNyXoad0Biew6o3&J&Ec zSl2qvyB4!joT`Z2%oGIMQ)J%_`Jv~00J(c%Jut92|L6tP7MBFZ`%d#sai|Dp3m8KV z?KB+qcqKYctZZow4sLcdaJ7QzdYOL8)8%KZ3kbJ!DiowM=UoM*ht)K0O*D2W-%_`; zL)Kuc>sT;b4s}sMu=ysbBmd)tT2_IJYPE>*Oy*{{`5dF(USuQl+(_a5;=m5Uf{;W& z^4A83M_^;a6(T=n3_D7w^OaT^`&cUf;BF-Lk5YSDr^D!bMNiom zb1fYT%F^``r~sSR`(`q8WaOCUqCuDZ6b{Mz0{hJOx|r0e_Os({KlPP0D1_E2(O*YU z@Plb}F%#;3xJR;z7YWz?>6ZeJx)QFSlC&Hs1?Y3z+Ox1fnazqKB9lXv`~$? z{S6HVi0aCtpW!H#dtK{OejM<1UEH5$w~h=c)3Mby$`B6!4wLp>CxZX{4yRA+&tF^9 zVSoPKGR)i$GTwN(@6TOc901?`!+xNeYXu~M#cG%^9S#j!2g-Be&>?PPbQ2~6#>e%r zovDvENp{~kOXv_R3Wpo{u%Nm#EMD%o389l+g-dA9tR4E7C+|>is&ov8M}kKlPOE{) zOWMN^SiKgAhu=mKKBXAOmkF1PAMl96#;Z%q3-0Sbz!#f&DI}1I?JL+Xt(kF54MF7h zoUzoR*Qbf~$hhqGSU**rYe>ZnlTp8jza4VAqT12G3JQUG{D zx}Vs~wO&Jd#I^IoetWL>0wODvG|#EmDCKuuSdSwn`t2tt_I zPUKnMe07EYyVa8)jrq)#6}0LaOMOBoO?|jbj1OmtxIyr{pHQWjdkVjWOusVo>(-x* z10$gyN2#w#6dpP2XNVUJ*r)DW|LzBJa$fGNEI-+*#lh5tMt>9--4x_!Lu3&w#oBQ& z2NNj_#Q;PI2ORUXDt6t6^>wBTf=mDyvN0?;n4i91-YIzaocA}u`Y|yT;quR)d98wX zU5Jj}pyF%-fqS&9}7z42sa0Ri(DkI3vF!r#wQG}m|Z!wD}d{S4B3f9S8Cs(7!5KNYe zS-&UiV6SV1Dzp5;EQxFwG;zB(svN{GFgI1zR(C(+RCroE*oWX)Ht1X>%&zvW?{#=H ztA6+59Laq&k(OxLQh)Nf3e0EvSUK%m%B2#A?SP1JMyh^`eFo9_IukN-uibm*yB_l40GtcL z@KWPohmQopXYSGv!v!v$NAf+{$IqNBFQVpakN|OO5;cM|ZDEyi*m3iL;9*L0-^a+~ zswH$weaYwZFPy=4=9U=n@GC%wQWg>L;^%xko#a*HUXHXwNQyy^XYWCI( zH>MFkh>|u2?!7ilnZHA?UbnP1Cueo@P;6Cn`da*(NmZD;{FGZ%MvP4wZ?W3#hYIv$Aj>EhrbVA|6y=R8pkYy zcD~Wi7n@h)(bUR54B0n%4EC^98_Tn>w)f`z`3(_P)p26);JKEg!5OY7 zwA;wO^X&p^xDnmbizo$jYh^UPBz(U+8?-b-Z_>EuvHL)(HRGmW)vRDx(6s5}Pf9<= zXL`T5?ECq6&)Woq@Z(eZvKLTRCzdK>KE~HUJsn^9HMJ{W?$2t=`1tHiMX3Sd++{1n zm7r6`BepSL@YXiV`FYRg#P;~3gfAlo4NCIvaV8Tz53l9DC$x~R^C&x%e6&;ntyK4T zw6_0RT|b^XVQ?*0uD!MYkSrF%uwc(=U5?JUH8mIXHMFmp3O;e--23XxAFew~a46IF z_MhGH?v+%qe9RL*&f_+Am^)X3P6OnnMkjuLJav-m$y3KRs#c!(9TBtDn_qQl+fgpp zNXotWLl{&h=E=)nNeC!l7dPPta)ar9@vS&dJyVJLYraIS{z~UR^Cka*C;o4RC*X1> zl1Q!LD%f~l;!#Xq(ujt15ikOoDCkhe6r+)nSUFVaPd!kpD zDzvr3%A`-}!tCT?M7mCfiM`t%e0T=im+HN?W=e)VwBZ_^dZHf{*;GbK`A}GVk!9E$ zki#4@@}_@8p}?hjWJiIYMeYr2u(*4Kwd!KnGixyD z%@a~tqeoT1=OvEbC4~)9cI3nkf0@1{PXArTiwdu%nx<@5&BdhE5nV5zBUxEec2ym^ z;GTN$kRf^OImV2u=g+<9J5I&=;xK* zaCJ@N&%iA!XigasV=mE!+4jYZV<^F{rc6)Z1n1;j6);xk>8GAQ`y z8LbYxK!oX$SuipguWrfCwqw&L4qYkP>XuM>{}Ltdx1@0`ZC#!--{?zYXfwl({UE^yzw1Mc0O~Pg(x;h3`smCX5R~D%L-eBu(wLA3jUym= zs)iIbTwRKii#E#7=k}*A2}}T(Yr2(u3j{60{#%84{XKs#2bKv}t0apFzfXlT%S4;{ zpK8dA8)(HaF+JA3+C>fN0cO5}m5fDs&xy<+eK8tS+@H9z4 zn)^?_rDS7fGIuyo=w19^;X5AgyMQ-{f565iD=Vv~LQ!|ueZcJF3J|7H!DmJobjaGw zsj^d4gAg9eEmE#)jQOg6F&ctRZRTC5@Z)_g)^}hh>qeSFrC`&SL0ie}?EZxc#n8e& zkqSm;g;S-Tz0Hw;9b(M>SU@*Wb?D65FU5T~nYy=s44u`<$^mfz1OF|$wTxq7QTl1+ z??!Y~h0wLrmj{{jP_tBM)Y0M(a&tmu&1~p6OmU4^ZTn38$9s zY*iD#1~#;>IbJbQN)NItcX)1eu-xHuEjiL}+oO#4?zwo^H|sg_VrxnPiz#;I zSVmKQ43RIZ*!`)JYl9;Dq#+g9DTJ--+9vnXC-CQn(mfONsX zczEFOS@`$IJ+Q;<#YN4wwo~^)5@ZPU-LbO#c5__jzM!#%r$MdEPxq)1q36Ie4z7i} z_f`?s_+(6{ShwgYb+2=r`Tf6uj{X=;xqe%>SBLZR1gmiJn2f zaP7txJ-i!1ICsl(CP$8JBDrd5@6{S-x;~vmrY`J?tg^ zCE5w7NTcXz7}hV@Uc|K*ga8$&2s(CSx|9D2PwsN@oU}TzKcRi&h$r{es`VNoRimAA zvW1=oHp#Zyu*rQ@91qHDWicu%)IWQqim6lo7^(&-`dFdQdauidos{A+IaPjuf+;oHKM_B?;b8y!y7mb6cwY86#bHW?6= zwh=O2YpIM+N3zbmcv8;vB_6vIRMZ)yljmW@( zhR{Bj>m|R{%}%GNI$UJBnfB`#%9}3k6PvXimTp&QxO*b}Y~xe%MQTlk>d?Z0w&yo` z&M@^=$%3ch!-gNYUHYImZe4DRS3WAZ-ERLtk2byjVE6gx2-+93K2e;5ZcUFViLmu` z7tn)mO$tTOH?z$09$kJ7b6fIr7J9v6q4l6g>Q``yS=}G}U3~pam>a+O z9}$D9q!s>ShHE$zYW!PdtW|r*^K^LE$~%oO@}eDYYNz8y&Pn{tHgvz9Bs=kPRLVnd z|M3sGH%$}|lE1B&mdJZnDNcvFpT6Nz`YkV@+7vzqw))~%CDOL`IrKwh`-#_|J`FFg zUTFO3|9WXBo?vv}aCAv@O1!zxFzJzM*ad2XcUIQ}tI_Ebkhu9bFl9xbLY)1PQ#K8U z5Hbg^NbTKA5cABAMrLls##Tfog}c9>2E5@1>!Z9iGW~Mc7vufOaBBpdeNLYMj+*9^ z<$mwRz~=S9-u^Hphv$cclM#Fbh=vgWoN`XV*~`s@O7+E0j79HlJEX6YPys}XiX>Qz x`_}gyx-p%QO^M{q4|JH})rB6#5H7nKp{%NG(qANcN2?3fgYDNaT-%1}{{u;U#;E`R From fa534e4755df403ee23f82ecd420b94c8c184d79 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Tue, 27 Apr 2021 14:47:56 +0100 Subject: [PATCH 017/464] 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 b9b237fc9a531eb3d42d1203198bcb03848c86c9 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Sat, 8 May 2021 19:48:34 -0400 Subject: [PATCH 018/464] Replace forwarding UI with dialog Replaces the old forwarding UI with a dialog based on designs from https://github.com/vector-im/element-web/issues/14690. Signed-off-by: Robin Townsend --- res/css/_components.scss | 1 + res/css/views/dialogs/_ForwardDialog.scss | 115 ++++++++++ .../views/context_menus/MessageContextMenu.js | 8 +- .../views/dialogs/ForwardDialog.tsx | 213 ++++++++++++++++++ src/i18n/strings/en_EN.json | 5 + 5 files changed, 338 insertions(+), 4 deletions(-) create mode 100644 res/css/views/dialogs/_ForwardDialog.scss create mode 100644 src/components/views/dialogs/ForwardDialog.tsx diff --git a/res/css/_components.scss b/res/css/_components.scss index ec9592f3a1..ebb025d880 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -74,6 +74,7 @@ @import "./views/dialogs/_DevtoolsDialog.scss"; @import "./views/dialogs/_EditCommunityPrototypeDialog.scss"; @import "./views/dialogs/_FeedbackDialog.scss"; +@import "./views/dialogs/_ForwardDialog.scss"; @import "./views/dialogs/_GroupAddressPicker.scss"; @import "./views/dialogs/_HostSignupDialog.scss"; @import "./views/dialogs/_IncomingSasDialog.scss"; diff --git a/res/css/views/dialogs/_ForwardDialog.scss b/res/css/views/dialogs/_ForwardDialog.scss new file mode 100644 index 0000000000..a3aa08d04f --- /dev/null +++ b/res/css/views/dialogs/_ForwardDialog.scss @@ -0,0 +1,115 @@ +/* +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_ForwardDialog { + width: 520px; + color: $primary-fg-color; + display: flex; + flex-direction: column; + flex-wrap: nowrap; + min-height: 0; + height: 80vh; + + .mx_ForwardDialog_preview { + max-height: 30%; + flex-shrink: 0; + overflow: scroll; + + div { + pointer-events: none; + } + + .mx_EventTile_msgOption { + display: none; + } + } + + .mx_ForwardList { + display: contents; + + .mx_SearchBox { + // To match the space around the title + margin: 0 0 15px 0; + flex-grow: 0; + } + + .mx_ForwardList_content { + flex-grow: 1; + } + + .mx_ForwardList_noResults { + display: block; + margin-top: 24px; + } + + .mx_ForwardList_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_ForwardList_entry { + display: flex; + margin-top: 12px; + + .mx_BaseAvatar { + 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; + } + + .mx_AccessibleButton { + &.mx_ForwardList_sending, &.mx_ForwardList_sent { + &::before { + content: ''; + display: inline-block; + background-color: $button-primary-bg-color; + mask-position: center; + mask-repeat: no-repeat; + mask-size: 14px; + width: 14px; + height: 14px; + margin-right: 5px; + } + } + + &.mx_ForwardList_sending::before { + mask-image: url('$(res)/img/element-icons/circle-sending.svg'); + } + + &.mx_ForwardList_sent::before { + mask-image: url('$(res)/img/element-icons/circle-sent.svg'); + } + } + } + } + } +} diff --git a/src/components/views/context_menus/MessageContextMenu.js b/src/components/views/context_menus/MessageContextMenu.js index 35efd12c9c..e069bba975 100644 --- a/src/components/views/context_menus/MessageContextMenu.js +++ b/src/components/views/context_menus/MessageContextMenu.js @@ -154,11 +154,11 @@ export default class MessageContextMenu extends React.Component { }; onForwardClick = () => { - if (this.props.onCloseDialog) this.props.onCloseDialog(); - dis.dispatch({ - action: 'forward_event', + const ForwardDialog = sdk.getComponent("dialogs.ForwardDialog"); + Modal.createTrackedDialog('Forward Message', '', ForwardDialog, { + cli: MatrixClientPeg.get(), event: this.props.mxEvent, - }); + }, 'mx_Dialog_forwardmessage'); this.closeMenu(); }; diff --git a/src/components/views/dialogs/ForwardDialog.tsx b/src/components/views/dialogs/ForwardDialog.tsx new file mode 100644 index 0000000000..ce38a783b3 --- /dev/null +++ b/src/components/views/dialogs/ForwardDialog.tsx @@ -0,0 +1,213 @@ +/* +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, {useMemo, useState, useEffect} from "react"; +import classnames from "classnames"; +import {MatrixEvent} from "matrix-js-sdk/src/models/event"; +import {Room} from "matrix-js-sdk/src/models/room"; +import {MatrixClient} from "matrix-js-sdk/src/client"; + +import {_t} from "../../../languageHandler"; +import SettingsStore from "../../../settings/SettingsStore"; +import {UIFeature} from "../../../settings/UIFeature"; +import {Layout} from "../../../settings/Layout"; +import {IDialogProps} from "./IDialogProps"; +import BaseDialog from "./BaseDialog"; +import {avatarUrlForUser} from "../../../Avatar"; +import EventTile from "../rooms/EventTile"; +import SearchBox from "../../structures/SearchBox"; +import RoomAvatar from "../avatars/RoomAvatar"; +import AccessibleButton from "../elements/AccessibleButton"; +import AutoHideScrollbar from "../../structures/AutoHideScrollbar"; +import DMRoomMap from "../../../utils/DMRoomMap"; +import {sortRooms} from "../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm"; + +const AVATAR_SIZE = 30; + +interface IProps extends IDialogProps { + cli: MatrixClient; + event: MatrixEvent; +} + +interface IEntryProps { + room: Room; + // Callback to forward the message to this room + send(): Promise; +} + +enum SendState { + CanSend, + Sending, + Sent, + Failed, +} + +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"; + } else { + label = _t("Failed to send"); + className = "mx_ForwardList_sendFailed"; + } + + return
      + + { room.name } + { + setSendState(SendState.Sending); + try { + await send(); + setSendState(SendState.Sent); + } catch (e) { + setSendState(SendState.Failed); + } + }} + disabled={sendState !== SendState.CanSend} + > + { label } + +
      ; +}; + +const ForwardDialog: React.FC = ({ cli, event, onFinished }) => { + const userId = cli.getUserId(); + const [profileInfo, setProfileInfo] = useState({}); + useEffect(() => { + cli.getProfileInfo(userId).then(info => setProfileInfo(info)); + }, [cli, userId]); + + // For the message preview we fake the sender as ourselves + const mockEvent = new MatrixEvent({ + type: "m.room.message", + sender: userId, + content: event.getContent(), + unsigned: { + age: 97, + }, + event_id: "$9999999999999999999999999999999999999999999", + room_id: "!999999999999999999:example.org", + }); + mockEvent.sender = { + name: profileInfo.displayname || userId, + userId: userId, + getAvatarUrl: (..._) => { + return avatarUrlForUser( + { avatarUrl: profileInfo.avatar_url }, + AVATAR_SIZE, AVATAR_SIZE, "crop", + ); + }, + getMxcAvatarUrl: () => profileInfo.avatar_url, + }; + + const visibleRooms = 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; + }, [[], []]); + + const previewLayout = SettingsStore.getValue("layout"); + + return +
      + +
      +
      +

      { _t("Forward to") }

      + + + { 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 } + + { rooms.length + dms.length < 1 ? + { _t("No results") } + : undefined } +
      +
      +
      ; +}; + +export default ForwardDialog; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index dcad970300..ed49fb6b44 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2187,6 +2187,11 @@ "Report a bug": "Report a bug", "Please view existing bugs on Github first. No match? Start a new one.": "Please view existing bugs on Github first. No match? Start a new one.", "Send feedback": "Send feedback", + "Sending…": "Sending…", + "Sent": "Sent", + "Forward message": "Forward message", + "Forward to": "Forward to", + "Filter your rooms and DMs": "Filter your rooms and DMs", "Confirm abort of host creation": "Confirm abort of host creation", "Are you sure you wish to abort creation of the host? The process cannot be continued.": "Are you sure you wish to abort creation of the host? The process cannot be continued.", "Abort": "Abort", From 74925b2c6d1f7de459bb6f35bb8cf472f476ccc3 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Sat, 8 May 2021 19:51:51 -0400 Subject: [PATCH 019/464] Test ForwardDialog Signed-off-by: Robin Townsend --- .../views/dialogs/ForwardDialog-test.js | 124 ++++++++++++++++++ test/test-utils.js | 3 +- 2 files changed, 126 insertions(+), 1 deletion(-) create mode 100644 test/components/views/dialogs/ForwardDialog-test.js diff --git a/test/components/views/dialogs/ForwardDialog-test.js b/test/components/views/dialogs/ForwardDialog-test.js new file mode 100644 index 0000000000..cabcbb0325 --- /dev/null +++ b/test/components/views/dialogs/ForwardDialog-test.js @@ -0,0 +1,124 @@ +/* +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 "../../../skinned-sdk"; + +import React from "react"; +import {configure, mount} from "enzyme"; +import Adapter from "enzyme-adapter-react-16"; +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 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", + user: "@alice:example.org", + msg: "Hello world!", + event: true, + }); + + 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"); + + 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)); + }); + }); + + it("shows a preview with us as the sender", () => { + const previewBody = wrapper.find(".mx_EventTile_body"); + expect(previewBody.text()).toBe("Hello world!"); + + // We would just test SenderProfile for the user ID, but it's stubbed + const previewAvatar = wrapper.find(".mx_EventTile_avatar .mx_BaseAvatar_image"); + expect(previewAvatar.prop("title")).toBe("@bob:example.org"); + }); + + it("filters the rooms", () => { + const roomsBefore = wrapper.find(".mx_ForwardList_entry"); + expect(roomsBefore).toHaveLength(3); + + const searchInput = wrapper.find(".mx_SearchBox input"); + searchInput.instance().value = "a"; + searchInput.simulate("change"); + + const roomsAfter = wrapper.find(".mx_ForwardList_entry"); + expect(roomsAfter).toHaveLength(2); + }); + + it("tracks message sending progress across multiple rooms", async () => { + // Make sendEvent require manual resolution so we can see the sending state + let finishSend; + let cancelSend; + client.sendEvent = jest.fn(() => new Promise((resolve, reject) => { + finishSend = resolve; + cancelSend = reject; + })); + + const firstRoom = wrapper.find(".mx_ForwardList_entry").first(); + expect(firstRoom.find(".mx_AccessibleButton").text()).toBe("Send"); + + act(() => { + firstRoom.find(".mx_AccessibleButton").simulate("click"); + }); + expect(firstRoom.find(".mx_AccessibleButton").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"); + + const secondRoom = wrapper.find(".mx_ForwardList_entry").at(1); + expect(secondRoom.find(".mx_AccessibleButton").text()).toBe("Send"); + + act(() => { + secondRoom.find(".mx_AccessibleButton").simulate("click"); + }); + expect(secondRoom.find(".mx_AccessibleButton").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"); + }); +}); diff --git a/test/test-utils.js b/test/test-utils.js index 6dc02463a5..985e9f804b 100644 --- a/test/test-utils.js +++ b/test/test-utils.js @@ -218,7 +218,7 @@ export function mkMessage(opts) { return mkEvent(opts); } -export function mkStubRoom(roomId = null) { +export function mkStubRoom(roomId = null, name) { const stubTimeline = { getEvents: () => [] }; return { roomId, @@ -254,6 +254,7 @@ export function mkStubRoom(roomId = null) { on: jest.fn(), removeListener: jest.fn(), getDMInviter: jest.fn(), + name, getAvatarUrl: () => 'mxc://avatar.url/room.png', getMxcAvatarUrl: () => 'mxc://avatar.url/room.png', isSpaceRoom: jest.fn(() => false), From 7fa81766db34449534d265cf7c2e60069049dd5b Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Sat, 8 May 2021 20:07:51 -0400 Subject: [PATCH 020/464] Remove old forwarding code This has been replaced by ForwardDialog. Signed-off-by: Robin Townsend --- src/components/structures/RoomView.tsx | 30 +---------- src/components/views/rooms/ForwardMessage.js | 53 ------------------- src/components/views/rooms/RoomHeader.js | 9 ---- .../views/rooms/SimpleRoomHeader.js | 20 ------- src/i18n/strings/en_EN.json | 1 - src/stores/RoomViewStore.tsx | 21 -------- 6 files changed, 2 insertions(+), 132 deletions(-) delete mode 100644 src/components/views/rooms/ForwardMessage.js diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 58a87e6641..d16247e4ec 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -60,7 +60,6 @@ import ScrollPanel from "./ScrollPanel"; import TimelinePanel from "./TimelinePanel"; import ErrorBoundary from "../views/elements/ErrorBoundary"; 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"; @@ -136,7 +135,6 @@ export interface IState { // Whether to highlight the event scrolled to isInitialEventHighlighted?: boolean; replyToEvent?: MatrixEvent; - forwardingEvent?: MatrixEvent; numUnreadMessages: number; draggingFile: boolean; searching: boolean; @@ -324,7 +322,6 @@ export default class RoomView extends React.Component { initialEventId: RoomViewStore.getInitialEventId(), isInitialEventHighlighted: RoomViewStore.isInitialEventHighlighted(), replyToEvent: RoomViewStore.getQuotingEvent(), - 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), @@ -1394,18 +1391,6 @@ export default class RoomView extends React.Component { dis.dispatch({ action: "open_room_settings" }); }; - private onCancelClick = () => { - console.log("updateTint from onCancelClick"); - this.updateTint(); - if (this.state.forwardingEvent) { - dis.dispatch({ - action: 'forward_event', - event: null, - }); - } - dis.fire(Action.FocusComposer); - }; - private onAppsClick = () => { dis.dispatch({ action: "appsDrawer", @@ -1856,11 +1841,7 @@ export default class RoomView extends React.Component { let aux = null; let previewBar; - let hideCancel = false; - if (this.state.forwardingEvent) { - aux = ; - } else if (this.state.searching) { - hideCancel = true; // has own cancel + if (this.state.searching) { aux = { />; } 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. @@ -1881,7 +1860,6 @@ export default class RoomView extends React.Component { inviterName = this.props.oobData.inviterName; } const invitedEmail = this.props.threepidInvite?.toEmail; - hideCancel = true; previewBar = ( { hideMessagePanel = true; } - const shouldHighlight = this.state.isInitialEventHighlighted; let highlightedEventId = null; - if (this.state.forwardingEvent) { - highlightedEventId = this.state.forwardingEvent.getId(); - } else if (shouldHighlight) { + if (this.state.isInitialEventHighlighted) { highlightedEventId = this.state.initialEventId; } @@ -2091,7 +2066,6 @@ export default class RoomView extends React.Component { 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} e2eStatus={this.state.e2eStatus} diff --git a/src/components/views/rooms/ForwardMessage.js b/src/components/views/rooms/ForwardMessage.js deleted file mode 100644 index dd894c0dcf..0000000000 --- a/src/components/views/rooms/ForwardMessage.js +++ /dev/null @@ -1,53 +0,0 @@ -/* - Copyright 2017 Vector Creations Ltd - Copyright 2017 Michael Telatynski - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ - -import React from 'react'; -import PropTypes from 'prop-types'; -import { _t } from '../../../languageHandler'; -import {Key} from '../../../Keyboard'; -import {replaceableComponent} from "../../../utils/replaceableComponent"; - -@replaceableComponent("views.rooms.ForwardMessage") -export default class ForwardMessage extends React.Component { - static propTypes = { - onCancelClick: PropTypes.func.isRequired, - }; - - componentDidMount() { - document.addEventListener('keydown', this._onKeyDown); - } - - componentWillUnmount() { - document.removeEventListener('keydown', this._onKeyDown); - } - - _onKeyDown = ev => { - switch (ev.key) { - case Key.ESCAPE: - this.props.onCancelClick(); - break; - } - }; - - render() { - return ( -
      -

      { _t('Please select the destination room for this message') }

      -
      - ); - } -} diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js index f856f7f6ef..cd4f89f964 100644 --- a/src/components/views/rooms/RoomHeader.js +++ b/src/components/views/rooms/RoomHeader.js @@ -22,7 +22,6 @@ import { _t } from '../../../languageHandler'; import {MatrixClientPeg} from '../../../MatrixClientPeg'; import RateLimitedFunc from '../../../ratelimitedfunc'; -import {CancelButton} from './SimpleRoomHeader'; import SettingsStore from "../../../settings/SettingsStore"; import RoomHeaderButtons from '../right_panel/RoomHeaderButtons'; import E2EIcon from './E2EIcon'; @@ -44,7 +43,6 @@ export default class RoomHeader extends React.Component { onPinnedClick: PropTypes.func, onSearchClick: PropTypes.func, onLeaveClick: PropTypes.func, - onCancelClick: PropTypes.func, e2eStatus: PropTypes.string, onAppsClick: PropTypes.func, appsShown: PropTypes.bool, @@ -54,7 +52,6 @@ export default class RoomHeader extends React.Component { static defaultProps = { editing: false, inRoom: false, - onCancelClick: null, }; componentDidMount() { @@ -120,13 +117,8 @@ export default class RoomHeader extends React.Component { render() { let searchStatus = null; - let cancelButton = null; let pinnedEventsButton = null; - if (this.props.onCancelClick) { - cancelButton = ; - } - // don't display the search count until the search completes and // gives us a valid (possibly zero) searchCount. if (this.props.searchInfo && @@ -265,7 +257,6 @@ export default class RoomHeader extends React.Component {
      { e2eIcon }
      { name } { topicElement } - { cancelButton } { rightRow }
      diff --git a/src/components/views/rooms/SimpleRoomHeader.js b/src/components/views/rooms/SimpleRoomHeader.js index b2a66f6670..02c1e1e8a1 100644 --- a/src/components/views/rooms/SimpleRoomHeader.js +++ b/src/components/views/rooms/SimpleRoomHeader.js @@ -16,23 +16,9 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; -import AccessibleButton from '../elements/AccessibleButton'; import * as sdk from '../../../index'; -import { _t } from '../../../languageHandler'; import {replaceableComponent} from "../../../utils/replaceableComponent"; -// cancel button which is shared between room header and simple room header -export function CancelButton(props) { - const {onClick} = props; - - return ( - - {_t("Cancel")} - - ); -} - /* * A stripped-down room header used for things like the user settings * and room directory. @@ -41,18 +27,13 @@ export function CancelButton(props) { export default class SimpleRoomHeader extends React.Component { static propTypes = { title: PropTypes.string, - onCancelClick: PropTypes.func, // `src` to a TintableSvg. Optional. icon: PropTypes.string, }; render() { - let cancelButton; let icon; - if (this.props.onCancelClick) { - cancelButton = ; - } if (this.props.icon) { const TintableSvg = sdk.getComponent('elements.TintableSvg'); icon = { icon } { this.props.title } - { cancelButton }
  • 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 219c983d191d9172e3c68aa11b1765f91ac5c6cd Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Sun, 9 May 2021 10:58:44 -0400 Subject: [PATCH 021/464] 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 022/464] 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 023/464] 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 024/464] 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 025/464] 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 026/464] 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 027/464] 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 028/464] 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 029/464] 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 030/464] 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 031/464] 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 c9988d59a155fa37b7459d2f72fb7a59ff65bb3b Mon Sep 17 00:00:00 2001 From: libexus Date: Wed, 12 May 2021 20:26:30 +0000 Subject: [PATCH 032/464] Translated using Weblate (German) Currently translated at 99.3% (2939 of 2959 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 | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index a3d6027fba..23f6071efc 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -3293,5 +3293,34 @@ "Stop the recording": "Aufnahme stoppen", "%(count)s results in all spaces|one": "%(count)s Ergebnis in allen Bereichen", "%(count)s results in all spaces|other": "%(count)s Ergebnisse in allen Bereichen", - "You have no ignored users.": "Du ignorierst keine Benutzer." + "You have no ignored users.": "Du ignorierst keine Benutzer.", + "Error processing voice message": "Fehler beim Verarbeiten der Sprachnachricht", + "To join %(spaceName)s, turn on the Spaces beta": "Um %(spaceName)s beizutreten, aktiviere die Spaces Betaversion", + "To view %(spaceName)s, turn on the Spaces beta": "Um %(spaceName)s zu betreten, aktiviere die Spaces Betaversion", + "Select a room below first": "Wähle zuerst einen Raum aus", + "Communities are changing to Spaces": "Spaces ersetzen Communities", + "Join the beta": "Beta beitreten", + "Leave the beta": "Beta verlassen", + "Beta": "Beta", + "Tap for more info": "Klicke für mehr Infos", + "Spaces is a beta feature": "Spaces sind noch in der Entwicklung und möglicherweise instabil", + "Want to add a new room instead?": "Willst du einen neuen Raum hinzufügen?", + "Adding rooms... (%(progress)s out of %(count)s)|one": "Raum wird hinzugefügt...", + "Adding rooms... (%(progress)s out of %(count)s)|other": "Räume werden hinzugefügt... (%(progress)s von %(count)s)", + "You can add existing spaces to a space.": "Du kannst existierende Spaces zu einem Space hinzfügen.", + "Feeling experimental?": "Lust auf Experimente?", + "You are not allowed to view this server's rooms list": "Du darfst diese Raumliste nicht sehen", + "We didn't find a microphone on your device. Please check your settings and try again.": "Auf deinem Gerät kann kein Mikrofon gefunden werden. Bitte überprüfe deine Einstellungen und versuche es nochmal.", + "No microphone found": "Kein Mikrofon gefunden", + "We were unable to access your microphone. Please check your browser settings and try again.": "Fehler beim Zugriff auf dein Mikrofon. Überprüfe deine Browsereinstellungen und versuche es nochmal.", + "Unable to access your microphone": "Fehler beim Zugriff auf Mikrofon", + "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.": "Lust auf ein paar Experimente? In den Laboreinstellungen kannst du zukünftige Features vor der Veröffentlichung testen und uns mit Feedback beim Verbessern helfen. Mehr Infos.", + "Please enter a name for the space": "Gib den Namen des Spaces ein", + "Connecting": "Verbinden", + "Allow Peer-to-Peer for 1:1 calls (if you enable this, the other party might be able to see your IP address)": "Direktverbindung für Direktanrufe aktivieren. Dadurch sieht dein Gegenüber möglicherweise deine IP-Adresse.", + "Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.": "Die Betaversion ist verfügbar für Browser, Desktop und Android verfügbar. Je nach Homeserver sind einige Funktionen möglicherweise nicht verfügbar.", + "You can leave the beta any time from settings or tapping on a beta badge, like the one above.": "Du kannst die Betaversion jederzeit verlassen. Mache dies entweder in den Einstellungen oder klicke auf Beta-Icons wie dieses hier.", + "%(brand)s will reload with Spaces enabled. Communities and custom tags will be hidden.": "%(brand)s wird mit aktivierten Spaces neuladen. Danach kannst Communities und Custom Tags nicht verwenden.", + "Beta available for web, desktop and Android. Thank you for trying the beta.": "Die Betaversion ist für Browser, Desktop und Android verfügbar. Danke, dass Du die Betaversion testest.", + "%(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "%(brand)s wird mit deaktivierten Spaces neuladen und du kannst Communities und Custom Tags wieder verwenden können." } From bf9dd05b3c48f64e14d2a31762ea8c94d7ded609 Mon Sep 17 00:00:00 2001 From: iaiz Date: Wed, 12 May 2021 13:12:03 +0000 Subject: [PATCH 033/464] Translated using Weblate (Spanish) Currently translated at 99.8% (2956 of 2959 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 | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/es.json b/src/i18n/strings/es.json index 3e4b7b52ce..86749e7b3f 100644 --- a/src/i18n/strings/es.json +++ b/src/i18n/strings/es.json @@ -3259,5 +3259,37 @@ "%(count)s results in all spaces|other": "%(count)s resultados en todos los espacios", "You have no ignored users.": "No has ignorado a nadie.", "Pause": "Pausar", - "Play": "Reproducir" + "Play": "Reproducir", + "This is an experimental feature. For now, new users receiving an invite will have to open the invite on to actually join.": "Esto es una funcionalidad experimental. Por ahora, los usuarios nuevos que reciban una invitación tendrán que abrirla en para unirse.", + "To view %(spaceName)s, turn on the Spaces beta": "Para ver %(spaceName)s, activa la beta de los espacios", + "To join %(spaceName)s, turn on the Spaces beta": "Para unirte a %(spaceName)s, activa la beta de los espacios", + "Select a room below first": "Selecciona una sala de abajo primero", + "Communities are changing to Spaces": "Las comunidades se van a convertir en espacios", + "Join the beta": "Unirse a la beta", + "Leave the beta": "Salir de la beta", + "Beta": "Beta", + "Tap for more info": "Pulsa para más información", + "Spaces is a beta feature": "Los espacios son una funcionalidad en beta", + "Want to add a new room instead?": "¿Quieres añadir una sala nueva en su lugar?", + "Adding rooms... (%(progress)s out of %(count)s)|other": "Añadiendo salas… (%(progress)s de %(count)s)", + "Adding rooms... (%(progress)s out of %(count)s)|one": "Añadiendo sala…", + "Not all selected were added": "No se han añadido todas las seleccionadas", + "You can add existing spaces to a space.": "Puedes añadir espacios ya existentes dentro de otros espacios.", + "Feeling experimental?": "¿Te animas a probar cosas nuevas?", + "You are not allowed to view this server's rooms list": "No tienes permiso para ver la lista de salas de este servidor", + "Error processing voice message": "Ha ocurrido un error al procesar el mensaje de voz", + "We didn't find a microphone on your device. Please check your settings and try again.": "No hemos encontrado un micrófono en tu dispositivo. Por favor, consulta tus ajustes e inténtalo de nuevo.", + "No microphone found": "No se ha encontrado ningún micrófono", + "We were unable to access your microphone. Please check your browser settings and try again.": "No hemos podido acceder a tu micrófono. Por favor, comprueba los ajustes de tu navegador e inténtalo de nuevo.", + "Unable to access your microphone": "No se ha podido acceder a tu micrófono", + "Your access token gives full access to your account. Do not share it with anyone.": "Tu token de acceso da acceso completo a tu cuenta. No lo compartas con nadie.", + "Access Token": "Token de acceso", + "Spaces are a new way to group rooms and people. To join an existing space you'll need an invite.": "Los espacios son una nueva forma de agrupar salas y personas. Para unirte a uno ya existente, necesitarás que te inviten a él.", + "Please enter a name for the space": "Por favor, elige un nombre para el espacio", + "Connecting": "Conectando", + "You can leave the beta any time from settings or tapping on a beta badge, like the one above.": "Puedes salirte de la beta en cualquier momento desde tus ajustes o pulsando sobre la etiqueta de beta, como la que hay arriba.", + "%(brand)s will reload with Spaces enabled. Communities and custom tags will be hidden.": "%(brand)s se volverá a cargar con los espacios activados. Las comunidades y etiquetas personalizadas se ocultarán.", + "Beta available for web, desktop and Android. Thank you for trying the beta.": "Versión beta disponible para web, escritorio y Android. Gracias por usar la beta.", + "%(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "%(brand)s volverá a cargarse con los espacios desactivados. Las comunidades y etiquetas personalizadas serán visibles de nuevo.", + "Spaces are a new way to group rooms and people.": "Los espacios son una nueva manera de agrupar salas y gente." } From 93ca9e689186ac5bf14f1e9bff7edb117a38d86f Mon Sep 17 00:00:00 2001 From: Thibault Martin Date: Wed, 12 May 2021 12:36:46 +0000 Subject: [PATCH 034/464] Translated using Weblate (French) Currently translated at 100.0% (2959 of 2959 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 | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index 4aee15691b..4b36d5b7ed 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -3296,5 +3296,40 @@ "Stop the recording": "Arrêter l’enregistrement", "%(count)s results in all spaces|one": "%(count)s résultat dans tous les espaces", "%(count)s results in all spaces|other": "%(count)s résultats dans tous les espaces", - "You have no ignored users.": "Vous n’avez ignoré personne." + "You have no ignored users.": "Vous n’avez ignoré personne.", + "Your access token gives full access to your account. Do not share it with anyone.": "Votre jeton d’accès donne un accès intégral à votre compte. Ne le partagez avec personne.", + "This is an experimental feature. For now, new users receiving an invite will have to open the invite on to actually join.": "Ceci est une fonctionnalité expérimentale. Pour l’instant, les nouveaux utilisateurs recevant une invitation devront l’ouvrir sur pour poursuivre.", + "To join %(spaceName)s, turn on the Spaces beta": "Pour rejoindre %(spaceName)s, activez les espaces en bêta", + "To view %(spaceName)s, turn on the Spaces beta": "Pour visualiser %(spaceName)s, activez les espaces en bêta", + "Select a room below first": "Sélectionnez un salon ci-dessous d’abord", + "Communities are changing to Spaces": "Les communautés deviennent des espaces", + "Join the beta": "Rejoindre la bêta", + "Leave the beta": "Quitter la bêta", + "Beta": "Bêta", + "Tap for more info": "Appuyez pour plus d’information", + "Spaces is a beta feature": "Les espaces sont une fonctionnalité en bêta", + "Want to add a new room instead?": "Voulez-vous plutôt ajouter un nouveau salon ?", + "Adding rooms... (%(progress)s out of %(count)s)|one": "Ajout du salon…", + "Adding rooms... (%(progress)s out of %(count)s)|other": "Ajout des salons… (%(progress)s sur %(count)s)", + "Not all selected were added": "Toute la sélection n’a pas été ajoutée", + "You can add existing spaces to a space.": "Vous pouvez ajouter des espaces existants à un espace.", + "Feeling experimental?": "L’esprit aventurier ?", + "You are not allowed to view this server's rooms list": "Vous n’avez pas l’autorisation d’accéder à la liste des salons de ce serveur", + "Error processing voice message": "Erreur lors du traitement du message vocal", + "We didn't find a microphone on your device. Please check your settings and try again.": "Nous n’avons pas détecté de microphone sur votre appareil. Merci de vérifier vos paramètres et de réessayer.", + "No microphone found": "Aucun microphone détecté", + "We were unable to access your microphone. Please check your browser settings and try again.": "Nous n’avons pas pu accéder à votre microphone. Merci de vérifier les paramètres de votre navigateur et de réessayer.", + "Unable to access your microphone": "Impossible d’accéder à votre microphone", + "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.": "L’esprit aventurier ? Les fonctionnalités expérimentales vous permettent de tester les nouveautés et aider à les polir avant leur lancement. En apprendre plus.", + "Access Token": "Jeton d’accès", + "Spaces are a new way to group rooms and people. To join an existing space you'll need an invite.": "Les espaces sont un nouveau moyen de grouper les salons et les personnes. Une invitation est nécessaire pour rejoindre un espace existant.", + "Please enter a name for the space": "Veuillez renseigner un nom pour l’espace", + "Connecting": "Connexion", + "Allow Peer-to-Peer for 1:1 calls (if you enable this, the other party might be able to see your IP address)": "Autoriser le pair-à-pair (p2p) pour les appels individuels (si activé, votre correspondant pourra voir votre adresse IP)", + "Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.": "Bêta disponible pour l’application web, de bureau et Android. Certains fonctionnalités pourraient ne pas être disponibles sur votre serveur d’accueil.", + "You can leave the beta any time from settings or tapping on a beta badge, like the one above.": "Vous pouvez quitter la bêta n’importe quand à partir des paramètres, ou en appuyant sur le badge bêta comme celui ci-dessus.", + "%(brand)s will reload with Spaces enabled. Communities and custom tags will be hidden.": "%(brand)s va être redémarré avec les espaces activés. Les communautés et les étiquettes personnalisées seront cachés.", + "Beta available for web, desktop and Android. Thank you for trying the beta.": "Bêta disponible pour l’application web, de bureau et Android. Merci d’essayer la bêta.", + "%(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "%(brand)s va être redémarré avec les espaces désactivés. Les communautés et les étiquettes personnalisées seront de nouveau visibles.", + "Spaces are a new way to group rooms and people.": "Les espaces sont un nouveau moyen de regrouper les salons et les personnes." } From 61f90cdc45a7d32c9f95a6c5aeea0f893bfefee3 Mon Sep 17 00:00:00 2001 From: Szimszon Date: Tue, 11 May 2021 16:48:05 +0000 Subject: [PATCH 035/464] Translated using Weblate (Hungarian) Currently translated at 100.0% (2959 of 2959 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 | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json index 6b33ecb81e..0230ecf52a 100644 --- a/src/i18n/strings/hu.json +++ b/src/i18n/strings/hu.json @@ -3314,5 +3314,40 @@ "%(count)s results in all spaces|other": "%(count)s találat a terekben", "You have no ignored users.": "Nincs figyelmen kívül hagyott felhasználó.", "Play": "Lejátszás", - "Pause": "Szünet" + "Pause": "Szünet", + "This is an experimental feature. For now, new users receiving an invite will have to open the invite on to actually join.": "Ez egy kísérleti funkció Egyenlőre az a felhasználó aki meghívót kap a meghívóban lévő linkre kattintva tud csatlakozni.", + "To join %(spaceName)s, turn on the Spaces beta": "A csatlakozáshoz ide: %(spaceName)s először kapcsolja be a béta Tereket", + "To view %(spaceName)s, turn on the Spaces beta": "A %(spaceName)s megjelenítéséhez először kapcsolja be a béta Tereket", + "Select a room below first": "Először válasszon ki szobát alulról", + "Communities are changing to Spaces": "A közösségek Terek lesznek", + "Join the beta": "Csatlakozás béta lehetőségekhez", + "Leave the beta": "Béta kikapcsolása", + "Beta": "Béta", + "Tap for more info": "Koppints további információért", + "Spaces is a beta feature": "A terek béta állapotban van", + "Want to add a new room instead?": "Inkább új szobát adna hozzá?", + "Adding rooms... (%(progress)s out of %(count)s)|one": "Szobák hozzáadása…", + "Adding rooms... (%(progress)s out of %(count)s)|other": "Szobák hozzáadása… (%(progress)s ennyiből: %(count)s)", + "Not all selected were added": "Nem az összes kijelölt lett hozzáadva", + "You can add existing spaces to a space.": "Létező tereket adhat a térhez.", + "Feeling experimental?": "Kísérletezni szeretne?", + "You are not allowed to view this server's rooms list": "Nincs joga ennek a szervernek a szobalistáját megnézni", + "Error processing voice message": "Hiba a hangüzenet feldolgozásánál", + "We didn't find a microphone on your device. Please check your settings and try again.": "Nem található mikrofon. Ellenőrizze a beállításokat és próbálja újra.", + "No microphone found": "Nem található mikrofon", + "We were unable to access your microphone. Please check your browser settings and try again.": "Nem lehet a mikrofont használni. Ellenőrizze a böngésző beállításait és próbálja újra.", + "Unable to access your microphone": "A mikrofont nem lehet használni", + "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.": "Kedve van kísérletezni? Labs az a hely ahol először hozzá lehet jutni az új dolgokhoz, kipróbálni új lehetőségeket és segíteni a fejlődésüket mielőtt mindenkihez eljut. Tudj meg többet.", + "Your access token gives full access to your account. Do not share it with anyone.": "A hozzáférési kulcs teljes elérést biztosít a fiókhoz. Soha ne ossza meg mással.", + "Access Token": "Elérési kulcs", + "Spaces are a new way to group rooms and people. To join an existing space you'll need an invite.": "A terek egy új lehetőség a szobák és emberek csoportosításához. Létező térhez meghívóval lehet csatlakozni.", + "Please enter a name for the space": "Kérem adjon meg egy nevet a térhez", + "Connecting": "Kapcsolás", + "Allow Peer-to-Peer for 1:1 calls (if you enable this, the other party might be able to see your IP address)": "Közvetlen hívás engedélyezése két fél között (ha ezt engedélyezi a másik fél láthatja az ön IP címét)", + "Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.": "Béta verzió elérhető webre, asztali kliensre és Androidra. Bizonyos funkciók lehet, hogy nem elérhetők a matrix szerverén.", + "You can leave the beta any time from settings or tapping on a beta badge, like the one above.": "Bármikor elhagyhatja a béta változatot a beállításokban vagy a béta kitűzőre koppintva, mint alább.", + "%(brand)s will reload with Spaces enabled. Communities and custom tags will be hidden.": "%(brand)s a Terekkel lesz újra betöltve. A közösségek és egyedi címkék rejtve maradnak.", + "Beta available for web, desktop and Android. Thank you for trying the beta.": "Béta verzió elérhető webre, asztali kliensre és Androidra. Köszönjük, hogy kipróbálja.", + "Spaces are a new way to group rooms and people.": "Szobák és emberek csoportosításának új lehetősége a Terek használata.", + "%(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "%(brand)s a Terek nélkül lesz újra betöltve. A közösségek és egyedi címkék újra megjelennek." } From 7be918f52744ceb8dc60353683be3fe2b6099a7a Mon Sep 17 00:00:00 2001 From: jelv Date: Wed, 12 May 2021 12:29:48 +0000 Subject: [PATCH 036/464] Translated using Weblate (Dutch) Currently translated at 100.0% (2959 of 2959 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 | 215 +++++++++++++++++++++++---------------- 1 file changed, 125 insertions(+), 90 deletions(-) diff --git a/src/i18n/strings/nl.json b/src/i18n/strings/nl.json index a30c949635..e12780ec10 100644 --- a/src/i18n/strings/nl.json +++ b/src/i18n/strings/nl.json @@ -59,7 +59,7 @@ "Close": "Sluiten", "Create new room": "Nieuw gesprek aanmaken", "Custom Server Options": "Aangepaste serverinstellingen", - "Dismiss": "Afwijzen", + "Dismiss": "Sluiten", "Error": "Fout", "Failed to forget room %(errCode)s": "Vergeten van gesprek is mislukt %(errCode)s", "Favourite": "Favoriet", @@ -177,7 +177,7 @@ "Hangup": "Ophangen", "Historical": "Historisch", "Home": "Thuis", - "Homeserver is": "Thuisserver is", + "Homeserver is": "Homeserver is", "Identity Server is": "Identiteitsserver is", "I have verified my email address": "Ik heb mijn e-mailadres geverifieerd", "Import": "Inlezen", @@ -193,7 +193,7 @@ "Invited": "Uitgenodigd", "Invites": "Uitnodigingen", "Invites user with given id to current room": "Nodigt de gebruiker met de gegeven ID uit in het huidige gesprek", - "Sign in with": "Aanmelden met", + "Sign in with": "Inloggen met", "Join as voice or video.": "Deelnemen met spraak of video.", "Join Room": "Gesprek toetreden", "%(targetName)s joined the room.": "%(targetName)s is tot het gesprek toegetreden.", @@ -240,7 +240,7 @@ "%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.": "%(senderName)s heeft %(targetDisplayName)s in het gesprek uitgenodigd.", "Server error": "Serverfout", "Server may be unavailable, overloaded, or search timed out :(": "De server is misschien onbereikbaar of overbelast, of het zoeken duurde te lang :(", - "Server may be unavailable, overloaded, or you hit a bug.": "De server is misschien onbereikbaar of overbelast, of je bent een fout tegengekomen.", + "Server may be unavailable, overloaded, or you hit a bug.": "De server is misschien onbereikbaar of overbelast, of je bent een bug tegengekomen.", "Server unavailable, overloaded, or something else went wrong.": "De server is onbereikbaar of overbelast, of er is iets anders foutgegaan.", "Session ID": "Sessie-ID", "%(senderName)s kicked %(targetName)s.": "%(senderName)s heeft %(targetName)s het gesprek uitgestuurd.", @@ -250,8 +250,8 @@ "%(senderName)s set their display name to %(displayName)s.": "%(senderName)s heeft %(displayName)s als weergavenaam aangenomen.", "Show timestamps in 12 hour format (e.g. 2:30pm)": "Tijd in 12-uursformaat tonen (bv. 2:30pm)", "Signed Out": "Afgemeld", - "Sign in": "Aanmelden", - "Sign out": "Afmelden", + "Sign in": "Inloggen", + "Sign out": "Uitloggen", "%(count)s of your messages have not been sent.|other": "Enkele van uw berichten zijn niet verstuurd.", "Someone": "Iemand", "The phone number entered looks invalid": "Het ingevoerde telefoonnummer ziet er ongeldig uit", @@ -266,7 +266,7 @@ "This room": "Dit gesprek", "This room is not accessible by remote Matrix servers": "Dit gesprek is niet toegankelijk vanaf externe Matrix-servers", "To use it, just wait for autocomplete results to load and tab through them.": "Om het te gebruiken, wacht u tot de autoaanvullen resultaten geladen zijn en tabt u erdoorheen.", - "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "U heeft gepoogd een gegeven punt in de tijdslijn van dit gesprek te laden, maar u bent niet bevoegd het desbetreffende bericht te zien.", + "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "U probeert een punt in de tijdlijn van dit gesprek te laden, maar u heeft niet voldoende rechten om het bericht te lezen.", "Tried to load a specific point in this room's timeline, but was unable to find it.": "Geprobeerd een gegeven punt in de tijdslijn van dit gesprek te laden, maar kon dit niet vinden.", "Unable to add email address": "Kan e-mailadres niet toevoegen", "Unable to remove contact information": "Kan contactinformatie niet verwijderen", @@ -329,7 +329,7 @@ "Please select the destination room for this message": "Selecteer het bestemmingsgesprek voor dit bericht", "New Password": "Nieuw wachtwoord", "Start automatically after system login": "Automatisch starten na systeemlogin", - "Analytics": "Statistische gegevens", + "Analytics": "Gebruiksgegevens", "Options": "Opties", "%(brand)s collects anonymous analytics to allow us to improve the application.": "%(brand)s verzamelt anonieme analysegegevens die het mogelijk maken de toepassing te verbeteren.", "Passphrases must match": "Wachtwoorden moeten overeenkomen", @@ -628,10 +628,10 @@ "Room Notification": "Groepsgespreksmelding", "The information being sent to us to help make %(brand)s better includes:": "De informatie die naar ons wordt verstuurd om %(brand)s te verbeteren bevat:", "Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Waar deze pagina identificeerbare informatie bevat, zoals een gespreks-, gebruikers- of groeps-ID, zullen deze gegevens verwijderd worden voordat ze naar de server gestuurd worden.", - "The platform you're on": "Het platform dat je gebruikt", + "The platform you're on": "Het platform dat u gebruikt", "The version of %(brand)s": "De versie van %(brand)s", "Your language of choice": "De door jou gekozen taal", - "Which officially provided instance you are using, if any": "Welke officieel aangeboden instantie je eventueel gebruikt", + "Which officially provided instance you are using, if any": "Welke officieel aangeboden instantie u eventueel gebruikt", "Whether or not you're using the Richtext mode of the Rich Text Editor": "Of u de tekstverwerker al dan niet in de modus voor opgemaakte tekst gebruikt", "Your homeserver's URL": "De URL van je homeserver", "In reply to ": "Als antwoord op ", @@ -658,8 +658,8 @@ "Who can join this community?": "Wie kan er tot deze gemeenschap toetreden?", "Everyone": "Iedereen", "Leave this community": "Deze gemeenschap verlaten", - "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.": "Voor het oplossen van, via GitHub, gemelde problemen helpen foutopsporingslogboeken ons enorm. Deze bevatten wel gebruiksgegevens (waaronder uw gebruikersnaam, de ID’s of bijnamen van de gesprekken en groepen die u heeft bezocht, en de namen van andere gebruikers), maar geen berichten.", - "Submit debug logs": "Foutopsporingslogboeken indienen", + "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.": "Voor het oplossen van, via GitHub, gemelde bugs helpen foutenlogboeken ons enorm. Deze bevatten wel uw gebruiksgegevens, maar geen berichten. Het bevat onder meer uw gebruikersnaam, de ID’s of bijnamen van de gesprekken en groepen die u heeft bezocht en de namen van andere gebruikers.", + "Submit debug logs": "Foutenlogboek versturen", "Opens the Developer Tools dialog": "Opent het dialoogvenster met ontwikkelaarsgereedschap", "Fetching third party location failed": "Het ophalen van de locatie van de derde partij is mislukt", "I understand the risks and wish to continue": "Ik begrijp de risico’s en wil graag verdergaan", @@ -727,11 +727,11 @@ "All messages (noisy)": "Alle berichten (luid)", "Enable them now": "Deze nu inschakelen", "Toolbox": "Gereedschap", - "Collecting logs": "Logboeken worden verzameld", + "Collecting logs": "Logs worden verzameld", "You must specify an event type!": "U dient een gebeurtenistype op te geven!", "(HTTP status %(httpStatus)s)": "(HTTP-status %(httpStatus)s)", "Invite to this room": "Uitnodigen voor dit gesprek", - "Send logs": "Logboeken versturen", + "Send logs": "Logs versturen", "All messages": "Alle berichten", "Call invitation": "Oproep-uitnodiging", "Downloading update...": "Update wordt gedownload…", @@ -778,17 +778,17 @@ "Thank you!": "Bedankt!", "With your current browser, the look and feel of the application may be completely incorrect, and some or all features may not function. If you want to try it anyway you can continue, but you are on your own in terms of any issues you may encounter!": "Met uw huidige browser kan de toepassing er volledig onjuist uitzien. Tevens is het mogelijk dat niet alle functies naar behoren werken. U kunt doorgaan als u het toch wilt proberen, maar bij problemen bent u volledig op uzelf aangewezen!", "Checking for an update...": "Bezig met controleren op updates…", - "Logs sent": "Logboeken verstuurd", - "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.": "Foutopsporingslogboeken bevatten gebruiksgegevens over de toepassing, inclusief uw gebruikersnaam, de ID’s of bijnamen van de gesprekken die u heeft bezocht, evenals de gebruikersnamen van andere gebruikers. Ze bevatten geen berichten.", - "Failed to send logs: ": "Versturen van logboeken mislukt: ", - "Preparing to send logs": "Logboeken worden voorbereid voor versturen", + "Logs sent": "Logs verstuurd", + "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.": "Foutenlogboeken bevatten gebruiksgegevens van de app inclusief uw gebruikersnaam, de ID’s of bijnamen van de gesprekken die u heeft bezocht, en de gebruikersnamen van andere gebruikers. Ze bevatten geen berichten.", + "Failed to send logs: ": "Versturen van logs mislukt: ", + "Preparing to send logs": "Logs voorbereiden voor versturen", "e.g. %(exampleValue)s": "bv. %(exampleValue)s", - "Every page you use in the app": "Iedere bladzijde die je in de toepassing gebruikt", + "Every page you use in the app": "Iedere bladzijde die u in de app gebruikt", "e.g. ": "bv. ", "Your device resolution": "De resolutie van je apparaat", "Missing roomId.": "roomId ontbreekt.", "Always show encryption icons": "Versleutelingspictogrammen altijd tonen", - "Send analytics data": "Statistische gegevens versturen", + "Send analytics data": "Gebruiksgegevens delen", "Enable widget screenshots on supported widgets": "Widget-schermafbeeldingen inschakelen op ondersteunde widgets", "Muted Users": "Gedempte gebruikers", "Popout widget": "Widget in nieuw venster openen", @@ -798,11 +798,11 @@ "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.": "De zichtbaarheid van berichten in Matrix is zoals bij e-mails. Het vergeten van uw berichten betekent dat berichten die u heeft verstuurd niet meer gedeeld worden met nieuwe of ongeregistreerde gebruikers, maar geregistreerde gebruikers die al toegang hebben tot deze berichten zullen alsnog toegang hebben tot hun eigen kopie ervan.", "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)": "Vergeet bij het sluiten van mijn account alle door mij verstuurde berichten (Let op: hierdoor zullen personen een onvolledig beeld krijgen van gesprekken)", "To continue, please enter your password:": "Voer uw wachtwoord in om verder te gaan:", - "Clear Storage and Sign Out": "Opslag wissen en afmelden", - "Send Logs": "Logboek versturen", + "Clear Storage and Sign Out": "Opslag wissen en uitloggen", + "Send Logs": "Logs versturen", "Refresh": "Herladen", "We encountered an error trying to restore your previous session.": "Het herstel van uw vorige sessie is mislukt.", - "Clearing your browser's storage may fix the problem, but will sign you out and cause any encrypted chat history to become unreadable.": "Het legen van de opslag van uw browser zal het probleem misschien verhelpen, maar zal u ook afmelden en uw gehele versleutelde gespreksgeschiedenis onleesbaar maken.", + "Clearing your browser's storage may fix the problem, but will sign you out and cause any encrypted chat history to become unreadable.": "Het legen van de opslag van uw browser zal het probleem misschien verhelpen, maar zal u ook uitloggen en uw gehele versleutelde gespreksgeschiedenis onleesbaar maken.", "Collapse Reply Thread": "Reactieketting dichtvouwen", "Can't leave Server Notices room": "Kan servermeldingsgesprek niet verlaten", "This room is used for important messages from the Homeserver, so you cannot leave it.": "Dit gesprek is bedoeld voor belangrijke berichten van de homeserver, dus u kunt het niet verlaten.", @@ -842,7 +842,7 @@ "Bulk options": "Bulkopties", "This homeserver has hit its Monthly Active User limit.": "Deze homeserver heeft zijn limiet voor maandelijks actieve gebruikers bereikt.", "This homeserver has exceeded one of its resource limits.": "Deze homeserver heeft één van zijn systeembronlimieten overschreden.", - "Whether or not you're logged in (we don't record your username)": "Of je al dan niet ingelogd bent (we slaan je gebruikersnaam niet op)", + "Whether or not you're logged in (we don't record your username)": "Of u al dan niet ingelogd bent (we slaan je gebruikersnaam niet op)", "The file '%(fileName)s' exceeds this homeserver's size limit for uploads": "Het bestand ‘%(fileName)s’ is groter dan de uploadlimiet van de homeserver", "Unable to load! Check your network connectivity and try again.": "Laden mislukt! Controleer je netwerktoegang en probeer het nogmaals.", "Failed to invite users to the room:": "Kon de volgende gebruikers hier niet uitnodigen:", @@ -1023,20 +1023,20 @@ "Profile picture": "Profielfoto", "Upgrade to your own domain": "Upgrade naar uw eigen domein", "Display Name": "Weergavenaam", - "Set a new account password...": "Stel een nieuw accountwachtwoord in…", + "Set a new account password...": "Stel een nieuw wachtwoord in…", "Email addresses": "E-mailadressen", "Phone numbers": "Telefoonnummers", "Language and region": "Taal en regio", "Theme": "Thema", "Account management": "Accountbeheer", - "Deactivating your account is a permanent action - be careful!": "Pas op! Het sluiten van uw account is onherroepelijk!", + "Deactivating your account is a permanent action - be careful!": "Pas op! Het sluiten van uw account kan niet teruggedraaid worden!", "General": "Algemeen", - "Legal": "Wettelijk", + "Legal": "Juridisch", "Credits": "Met dank aan", "For help with using %(brand)s, click here.": "Klik hier voor hulp bij het gebruiken van %(brand)s.", "For help with using %(brand)s, click here or start a chat with our bot using the button below.": "Klik hier voor hulp bij het gebruiken van %(brand)s, of begin een gesprek met onze robot met de knop hieronder.", - "Help & About": "Hulp & Info", - "Bug reporting": "Foutmeldingen", + "Help & About": "Hulp & info", + "Bug reporting": "Bug meldingen", "FAQ": "FAQ", "Versions": "Versies", "Preferences": "Instellingen", @@ -1046,10 +1046,10 @@ "Autocomplete delay (ms)": "Vertraging voor autoaanvullen (ms)", "Accept all %(invitedRooms)s invites": "Alle %(invitedRooms)s de uitnodigingen aannemen", "Key backup": "Sleutelback-up", - "Security & Privacy": "Veiligheid & Privacy", + "Security & Privacy": "Veiligheid & privacy", "Missing media permissions, click the button below to request.": "Mediatoestemmingen ontbreken, klik op de knop hieronder om deze aan te vragen.", "Request media permissions": "Mediatoestemmingen verzoeken", - "Voice & Video": "Spraak & Video", + "Voice & Video": "Spraak & video", "Room information": "Gespreksinformatie", "Internal room ID:": "Interne gespreks-ID:", "Room version": "Gespreksversie", @@ -1108,7 +1108,7 @@ "Unable to find profiles for the Matrix IDs listed below - would you like to invite them anyway?": "Kan geen profielen voor de Matrix-ID’s hieronder vinden - wilt u ze toch uitnodigen?", "Invite anyway and never warn me again": "Alsnog uitnodigen en mij nooit meer waarschuwen", "Invite anyway": "Alsnog uitnodigen", - "Before submitting logs, you must create a GitHub issue to describe your problem.": "Voor u logboeken indient, dient u uw probleem te melden op GitHub.", + "Before submitting logs, you must create a GitHub issue to describe your problem.": "Voordat u logs indient, dient u uw probleem te melden in een GitHub issue.", "Unable to load commit detail: %(msg)s": "Kan commitdetail niet laden: %(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": "Om uw gespreksgeschiedenis niet te verliezen vóór het uitloggen dient u uw veiligheidssleutel te exporteren. Dat moet vanuit de nieuwere versie van %(brand)s", "Incompatible Database": "Incompatibele database", @@ -1125,7 +1125,7 @@ "I don't want my encrypted messages": "Ik wil mijn versleutelde berichten niet", "Manually export keys": "Sleutels handmatig wegschrijven", "You'll lose access to your encrypted messages": "U zult de toegang tot uw versleutelde berichten verliezen", - "Are you sure you want to sign out?": "Weet u zeker dat u zich wilt afmelden?", + "Are you sure you want to sign out?": "Weet u zeker dat u wilt uitloggen?", "If you run into any bugs or have feedback you'd like to share, please let us know on GitHub.": "Als u fouten zou tegenkomen of voorstellen zou hebben, laat het ons dan weten op 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.": "Voorkom dubbele meldingen: doorzoek eerst de bestaande meldingen (en voeg desgewenst een +1 toe). Maak enkel een nieuwe melding aan indien u niets kunt vinden.", "Report bugs & give feedback": "Fouten melden & feedback geven", @@ -1193,7 +1193,7 @@ "Could not load user profile": "Kon gebruikersprofiel niet laden", "Your Matrix account on %(serverName)s": "Uw Matrix-account op %(serverName)s", "A verification email will be sent to your inbox to confirm setting your new password.": "Er is een verificatie-e-mail naar u gestuurd om het instellen van uw nieuwe wachtwoord te bevestigen.", - "Sign in instead": "Aanmelden", + "Sign in instead": "In plaats daarvan inloggen", "Your password has been reset.": "Uw wachtwoord is opnieuw ingesteld.", "Set a new password": "Stel een nieuw wachtwoord in", "Invalid homeserver discovery response": "Ongeldig homeserver-vindbaarheids-antwoord", @@ -1202,13 +1202,13 @@ "This homeserver does not support login using email address.": "Deze homeserver biedt geen ondersteuning voor inloggen met e-mailadres.", "Please contact your service administrator to continue using this service.": "Gelieve contact op te nemen met uw dienstbeheerder om deze dienst te blijven gebruiken.", "Failed to perform homeserver discovery": "Ontdekken van homeserver is mislukt", - "Sign in with single sign-on": "Aanmelden met eenmalige aanmelding", + "Sign in with single sign-on": "Inloggen met eenmalig inloggen", "Create account": "Account aanmaken", "Registration has been disabled on this homeserver.": "Registratie is uitgeschakeld op deze homeserver.", "Unable to query for supported registration methods.": "Kan ondersteunde registratiemethoden niet opvragen.", "Create your account": "Maak uw account aan", "Keep going...": "Doe verder…", - "For maximum security, this should be different from your account password.": "Voor maximale veiligheid zou dit moeten verschillen van uw accountwachtwoord.", + "For maximum security, this should be different from your account password.": "Voor maximale veiligheid moet dit verschillen van uw accountwachtwoord.", "That matches!": "Dat komt overeen!", "That doesn't match.": "Dat komt niet overeen.", "Go back to set it again.": "Ga terug om het opnieuw in te stellen.", @@ -1228,11 +1228,11 @@ "Don't ask again": "Niet opnieuw vragen", "New Recovery Method": "Nieuwe herstelmethode", "A new recovery passphrase and key for Secure Messages have been detected.": "Er zijn een nieuw herstelwachtwoord en een nieuwe herstelsleutel voor beveiligde berichten gedetecteerd.", - "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.": "Als u deze nieuwe herstelmethode niet heeft ingesteld, is het mogelijk dat een aanvaller toegang tot uw account probeert te krijgen. Wijzig onmiddellijk uw accountwachtwoord en stel in het instellingenmenu een nieuwe herstelmethode in.", + "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.": "Als u deze nieuwe herstelmethode niet heeft ingesteld, is het mogelijk dat een aanvaller toegang tot uw account probeert te krijgen. Wijzig onmiddellijk uw wachtwoord en stel bij instellingen een nieuwe herstelmethode in.", "Go to Settings": "Ga naar instellingen", "Set up Secure Messages": "Beveiligde berichten instellen", "Recovery Method Removed": "Herstelmethode verwijderd", - "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.": "Als u de herstelmethode niet heeft verwijderd, is het mogelijk dat er een aanvaller toegang tot uw account probeert te verkrijgen. Wijzig onmiddellijk uw accountwachtwoord en stel in het instellingenmenu een nieuwe herstelmethode in.", + "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.": "Als u de herstelmethode niet heeft verwijderd, is het mogelijk dat er een aanvaller toegang tot uw account probeert te verkrijgen. Wijzig onmiddellijk uw wachtwoord en stel bij instellingen een nieuwe herstelmethode in.", "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.": "Let op: gesprekken bijwerken voegt gespreksleden niet automatisch toe aan de nieuwe versie van het gesprek. Er komt in het oude gesprek een koppeling naar het nieuwe, waarop gespreksleden moeten klikken om aan het nieuwe gesprek deel te nemen.", "Adds a custom widget by URL to the room": "Voegt met een URL een aangepaste widget toe aan het gesprek", "Please supply a https:// or http:// widget URL": "Voer een https://- of http://-widget-URL in", @@ -1264,8 +1264,8 @@ "GitHub issue": "GitHub-melding", "Notes": "Opmerkingen", "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.": "Gelieve alle verdere informatie die zou kunnen helpen het probleem te analyseren (wat u aan het doen was, relevante gespreks-ID’s, gebruikers-ID’s, enz.) bij te voegen.", - "Sign out and remove encryption keys?": "Afmelden en versleutelingssleutels verwijderen?", - "To help us prevent this in future, please send us logs.": "Gelieve ons logboeken te sturen om dit in de toekomst te helpen voorkomen.", + "Sign out and remove encryption keys?": "Uitloggen en versleutelingssleutels verwijderen?", + "To help us prevent this in future, please send us logs.": "Stuur ons uw logs om dit in de toekomst te helpen voorkomen.", "Missing session data": "Sessiegegevens ontbreken", "Some session data, including encrypted message keys, is missing. Sign out and sign in to fix this, restoring keys from backup.": "Sommige sessiegegevens, waaronder sleutels voor versleutelde berichten, ontbreken. Herstel de sleutels uit uw back-up door u af- en weer aan te melden.", "Your browser likely removed this data when running low on disk space.": "Uw browser heeft deze gegevens wellicht verwijderd toen de beschikbare opslagruimte vol was.", @@ -1297,7 +1297,7 @@ "Rejecting invite …": "Uitnodiging wordt geweigerd…", "Join the conversation with an account": "Neem deel aan het gesprek met een account", "Sign Up": "Registreren", - "Sign In": "Aanmelden", + "Sign In": "Inloggen", "You were kicked from %(roomName)s by %(memberName)s": "U bent uit %(roomName)s gezet door %(memberName)s", "Reason: %(reason)s": "Reden: %(reason)s", "Forget this room": "Dit gesprek vergeten", @@ -1315,7 +1315,7 @@ "%(roomName)s can't be previewed. Do you want to join it?": "%(roomName)s kan niet vooraf bekeken worden. Wilt u eraan deelnemen?", "This room doesn't exist. Are you sure you're at the right place?": "Dit gesprek bestaat niet. Weet u zeker dat u zich op de juiste plaats bevindt?", "Try again later, or ask a room admin to check if you have access.": "Probeer het later opnieuw, of vraag een gespreksbeheerder om te controleren of u wel toegang heeft.", - "%(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.": "De foutcode %(errcode)s is weergegeven bij het toetreden van het gesprek. Als u meent dat u dit bericht foutief te zien krijgt, gelieve dan een foutmelding in te dienen.", + "%(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.": "De foutcode %(errcode)s is weergegeven bij het toetreden van het gesprek. Als u meent dat u dit bericht foutief te zien krijgt, gelieve dan een bugmelding indienen.", "This room has already been upgraded.": "Dit gesprek is reeds geüpgraded.", "reacted with %(shortName)s": "heeft gereageerd met %(shortName)s", "edited": "bewerkt", @@ -1386,8 +1386,8 @@ "Resend edit": "Bewerking opnieuw versturen", "Resend %(unsentCount)s reaction(s)": "%(unsentCount)s reactie(s) opnieuw versturen", "Resend removal": "Verwijdering opnieuw versturen", - "Failed to re-authenticate due to a homeserver problem": "Opnieuw aanmelden is mislukt wegens een probleem met de homeserver", - "Failed to re-authenticate": "Opnieuw aanmelden is mislukt", + "Failed to re-authenticate due to a homeserver problem": "Opnieuw inloggen is mislukt wegens een probleem met de homeserver", + "Failed to re-authenticate": "Opnieuw inloggen is mislukt", "Enter your password to sign in and regain access to your account.": "Voer uw wachtwoord in om u aan te melden en toegang tot uw account te herkrijgen.", "Forgotten your password?": "Wachtwoord vergeten?", "You're signed out": "U bent afgemeld", @@ -1401,7 +1401,7 @@ "Service": "Dienst", "Summary": "Samenvatting", "Sign in and regain access to your account.": "Meld u aan en herkrijg toegang tot uw account.", - "You cannot sign in to your account. Please contact your homeserver admin for more information.": "U kunt zich niet aanmelden met uw account. Neem voor meer informatie contact op met de beheerder van uw homeserver.", + "You cannot sign in to your account. Please contact your homeserver admin for more information.": "U kunt niet inloggen met uw account. Neem voor meer informatie contact op met de beheerder van uw homeserver.", "This account has been deactivated.": "Deze account is gesloten.", "Messages": "Berichten", "Actions": "Acties", @@ -1478,11 +1478,11 @@ "No recent messages by %(user)s found": "Geen recente berichten door %(user)s gevonden", "Try scrolling up in the timeline to see if there are any earlier ones.": "Probeer omhoog te scrollen in de tijdslijn om te kijken of er eerdere zijn.", "Remove recent messages by %(user)s": "Recente berichten door %(user)s verwijderen", - "You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?|other": "U staat op het punt %(count)s berichten door %(user)s te verwijderen. Dit is onherroepelijk. Wilt u doorgaan?", + "You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?|other": "U staat op het punt %(count)s berichten van %(user)s te verwijderen. Dit kan niet teruggedraaid worden. Wilt u doorgaan?", "For a large amount of messages, this might take some time. Please don't refresh your client in the meantime.": "Bij een groot aantal berichten kan dit even duren. Herlaad uw cliënt niet gedurende deze tijd.", "Remove %(count)s messages|other": "%(count)s berichten verwijderen", "Deactivate user?": "Gebruiker deactiveren?", - "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?": "Deze gebruiker deactiveren zal deze gebruiker uitloggen en verhinderen dat de gebruiker weer inlogt. Bovendien zal de gebruiker alle gesprekken waaraan de gebruiker deelneemt verlaten. Deze actie is onherroepelijk. Weet u zeker dat u deze gebruiker wilt deactiveren?", + "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?": "Deze gebruiker deactiveren zal deze gebruiker uitloggen en verhinderen dat de gebruiker weer inlogt. Bovendien zal de gebruiker alle gesprekken waaraan de gebruiker deelneemt verlaten. Deze actie is niet terug te draaien. Weet u zeker dat u deze gebruiker wilt deactiveren?", "Deactivate user": "Gebruiker deactiveren", "Remove recent messages": "Recente berichten verwijderen", "Bold": "Vet", @@ -1523,7 +1523,7 @@ "%(count)s unread messages.|other": "%(count)s ongelezen berichten.", "Unread mentions.": "Ongelezen vermeldingen.", "Show image": "Afbeelding tonen", - "Please create a new issue on GitHub so that we can investigate this bug.": "Maak een nieuw rapport aan op GitHub opdat we dit probleem kunnen onderzoeken.", + "Please create a new issue on GitHub so that we can investigate this bug.": "Maak een nieuwe issue aan op GitHub zodat we deze bug kunnen onderzoeken.", "e.g. my-room": "bv. mijn-gesprek", "Close dialog": "Dialoog sluiten", "Please enter a name for the room": "Geef een naam voor het gesprek op", @@ -1549,7 +1549,7 @@ "Click the link in the email you received to verify and then click continue again.": "Open de koppeling in de ontvangen verificatie-e-mail, en klik dan op ‘Doorgaan’.", "%(creator)s created and configured the room.": "Gesprek gestart en ingesteld door %(creator)s.", "Setting up keys": "Sleutelconfiguratie", - "Verify this session": "Deze sessie verifiëren", + "Verify this session": "Verifieer deze sessie", "Encryption upgrade available": "Versleutelingsupgrade beschikbaar", "You can use /help to list available commands. Did you mean to send this as a message?": "Typ /help om alle opdrachten te zien. Was het uw bedoeling dit als bericht te sturen?", "Help": "Hulp", @@ -1621,7 +1621,7 @@ "Upgrade": "Upgraden", "Verify": "Verifiëren", "Later": "Later", - "Review": "Controle", + "Review": "Controleer", "Decline (%(counter)s)": "Afwijzen (%(counter)s)", "This bridge was provisioned by .": "Dank aan voor de brug.", "This bridge is managed by .": "Brug onderhouden door .", @@ -1636,14 +1636,14 @@ "Unable to load session list": "Kan sessielijst niet laden", "Delete %(count)s sessions|other": "%(count)s sessies verwijderen", "Delete %(count)s sessions|one": "%(count)s sessie verwijderen", - "Whether you're using %(brand)s on a device where touch is the primary input mechanism": "Of je %(brand)s op een apparaat gebruikt waarop een aanraakscherm de voornaamste invoermethode is", - "Whether you're using %(brand)s as an installed Progressive Web App": "Of je %(brand)s gebruikt als een geïnstalleerde Progressive-Web-App", + "Whether you're using %(brand)s on a device where touch is the primary input mechanism": "Of u %(brand)s op een apparaat gebruikt waarop een aanraakscherm de voornaamste invoermethode is", + "Whether you're using %(brand)s as an installed Progressive Web App": "Of u %(brand)s gebruikt als een geïnstalleerde Progressieve Web-App", "Your user agent": "Jouw gebruikersagent", "If you cancel now, you won't complete verifying the other user.": "Als u nu annuleert zult u de andere gebruiker niet verifiëren.", "If you cancel now, you won't complete verifying your other session.": "Als u nu annuleert zult u uw andere sessie niet verifiëren.", "Cancel entering passphrase?": "Wachtwoord annuleren?", "Show typing notifications": "Typmeldingen weergeven", - "Verify this session by completing one of the following:": "Verifieer deze sessie door een van het volgende te doen:", + "Verify this session by completing one of the following:": "Verifieer deze sessie door een van het volgende handelingen te doen:", "Scan this unique code": "Scan deze unieke code", "or": "of", "Compare unique emoji": "Vergelijk unieke emoji", @@ -1746,7 +1746,7 @@ "Clear notifications": "Meldingen wissen", "You should remove your personal data from identity server before disconnecting. Unfortunately, identity server is currently offline or cannot be reached.": "U moet uw persoonlijke informatie van de identiteitsserver verwijderen voordat u zich ontkoppelt. Helaas kan de identiteitsserver op dit moment niet worden bereikt. Mogelijk is hij offline.", "Your homeserver does not support cross-signing.": "Uw homeserver biedt geen ondersteuning voor kruiselings ondertekenen.", - "Homeserver feature support:": "Homeserver ondersteund deze functies:", + "Homeserver feature support:": "Homeserver functie ondersteuning:", "exists": "aanwezig", "Sign In or Create Account": "Meld u aan of maak een account aan", "Use your account or create a new one to continue.": "Gebruik uw bestaande account of maak een nieuwe aan om verder te gaan.", @@ -1872,10 +1872,10 @@ "More options": "Meer opties", "Language Dropdown": "Taalselectie", "Destroy cross-signing keys?": "Sleutels voor kruiselings ondertekenen verwijderen?", - "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.": "Het verwijderen van sleutels voor kruiselings ondertekenen is onherroepelijk. Iedereen waarmee u geverifieerd heeft zal beveiligingswaarschuwingen te zien krijgen. U wilt dit hoogstwaarschijnlijk niet doen, tenzij u alle apparaten heeft verloren waarmee u kruiselings kon ondertekenen.", + "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.": "Het verwijderen van sleutels voor kruiselings ondertekenen is niet terug te draaien. Iedereen waarmee u geverifieerd heeft zal beveiligingswaarschuwingen te zien krijgen. U wilt dit hoogstwaarschijnlijk niet doen, tenzij u alle apparaten heeft verloren waarmee u kruiselings kon ondertekenen.", "Clear cross-signing keys": "Sleutels voor kruiselings ondertekenen wissen", "Clear all data in this session?": "Alle gegevens in deze sessie verwijderen?", - "Clearing all data from this session is permanent. Encrypted messages will be lost unless their keys have been backed up.": "Het verwijderen van alle gegevens in deze sessie is onherroepelijk. Versleutelde berichten zullen verloren gaan, tenzij u een back-up van de sleutels heeft.", + "Clearing all data from this session is permanent. Encrypted messages will be lost unless their keys have been backed up.": "Het verwijderen van alle gegevens in deze sessie is niet terug te draaien. Versleutelde berichten zullen verloren gaan, tenzij u een back-up van de sleutels heeft.", "Verify session": "Sessie verifiëren", "Session name": "Sessienaam", "Session key": "Sessiesleutel", @@ -1909,7 +1909,7 @@ "Automatically invite users": "Gebruikers automatisch uitnodigen", "Upgrade private room": "Privégesprek upgraden", "Upgrade public room": "Openbaar gesprek upgraden", - "Upgrading a room is an advanced action and is usually recommended when a room is unstable due to bugs, missing features or security vulnerabilities.": "Het bijwerken van een gesprek is een gevorderde actie en wordt meestal aanbevolen wanneer een gesprek onstabiel is door fouten, ontbrekende functies of problemen met de beveiliging.", + "Upgrading a room is an advanced action and is usually recommended when a room is unstable due to bugs, missing features or security vulnerabilities.": "Het bijwerken van een gesprek is een gevorderde actie en wordt meestal aanbevolen wanneer een gesprek onstabiel is door bugs, ontbrekende functies of problemen met de beveiliging.", "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.": "Dit heeft meestal enkel een invloed op de manier waarop het gesprek door de server verwerkt wordt. Als u problemen met uw %(brand)s ondervindt, dien dan een foutmelding in.", "You'll upgrade this room from to .": "U upgrade dit gesprek van naar .", "This will allow you to return to your account after signing out, and sign in on other sessions.": "Daardoor kunt u na afmelding terugkeren tot uw account, en u bij andere sessies aanmelden.", @@ -1927,7 +1927,7 @@ "Remove for me": "Verwijderen voor mezelf", "User Status": "Gebruikersstatus", "Country Dropdown": "Landselectie", - "Confirm your identity by entering your account password below.": "Bevestig uw identiteit door hieronder uw accountwachtwoord in te voeren.", + "Confirm your identity by entering your account password below.": "Bevestig uw identiteit door hieronder uw wachtwoord in te voeren.", "No identity server is configured so you cannot add an email address in order to reset your password in the future.": "Er is geen identiteitsserver geconfigureerd, dus u kunt geen e-mailadres toevoegen om in de toekomst een nieuw wachtwoord in te stellen.", "Jump to first unread room.": "Ga naar het eerste ongelezen gesprek.", "Jump to first invite.": "Ga naar de eerste uitnodiging.", @@ -1939,13 +1939,13 @@ "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.": "Door uw wachtwoord te wijzigen stelt u alle eind-tot-eind-versleutelingssleutels op al uw sessies opnieuw in, waardoor uw versleutelde gespreksgeschiedenis onleesbaar wordt. Stel uw sleutelback-up in of sla uw gesprekssleutels van een andere sessie op voor u een nieuw wachtwoord instelt.", "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.": "U bent uitgelogd bij al uw sessies en zult geen pushberichten meer ontvangen. Meld u op elk apparaat opnieuw aan om meldingen opnieuw in te schakelen.", "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.": "Ontvang toegang tot uw account en herstel de tijdens deze sessie opgeslagen versleutelingssleutels, zonder deze sleutels zijn sommige van uw versleutelde berichten in uw sessies onleesbaar.", - "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.": "Let op: uw persoonlijke gegevens (waaronder versleutelingssleutels) zijn nog steeds opgeslagen in deze sessie. Wis ze wanneer u klaar bent met deze sessie, of wanneer u zich wilt aanmelden met een andere 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.": "Let op: uw persoonlijke gegevens (waaronder versleutelingssleutels) zijn nog steeds opgeslagen in deze sessie. Wis ze wanneer u klaar bent met deze sessie, of wanneer u wilt inloggen met een andere account.", "Command Autocomplete": "Opdrachten autoaanvullen", "DuckDuckGo Results": "DuckDuckGo-resultaten", - "Enter your account password to confirm the upgrade:": "Voer uw accountwachtwoord in om het upgraden te bevestigen:", + "Enter your account password to confirm the upgrade:": "Voer uw wachtwoord in om het upgraden te bevestigen:", "Restore your key backup to upgrade your encryption": "Herstel uw sleutelback-up om uw versleuteling te upgraden", "Restore": "Herstellen", - "You'll need to authenticate with the server to confirm the upgrade.": "U zult zich moeten aanmelden bij de server om het upgraden te bevestigen.", + "You'll need to authenticate with the server to confirm the upgrade.": "U zult moeten inloggen bij de server om het upgraden te bevestigen.", "Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.": "Upgrade deze sessie om er andere sessies mee te verifiëren, waardoor deze ook de toegang verkrijgen tot uw versleutelde berichten en deze voor andere gebruikers als vertrouwd gemarkeerd worden.", "Set up with a recovery key": "Instellen met een herstelsleutel", "Keep a copy of it somewhere secure, like a password manager or even a safe.": "Bewaar een kopie op een veilige plaats, zoals in een wachtwoordbeheerder of een kluis.", @@ -1970,7 +1970,7 @@ "To report a Matrix-related security issue, please read the Matrix.org Security Disclosure Policy.": "Bekijk eerst het beveiligingsopenbaarmakingsbeleid van Matrix.org als u een probleem met de beveiliging van Matrix wilt melden.", "Not currently indexing messages for any room.": "Er worden momenteel voor geen enkel gesprek berichten geïndexeerd.", "%(doneRooms)s out of %(totalRooms)s": "%(doneRooms)s van %(totalRooms)s", - "Where you’re logged in": "Waar u ingelogd bent", + "Where you’re logged in": "Waar u bent ingelogd", "Manage the names of and sign out of your sessions below or verify them in your User Profile.": "Beheer hieronder de namen van uw sessies en meld ze af. Of verifieer ze in uw gebruikersprofiel.", "Use Single Sign On to continue": "Ga verder met eenmalige aanmelding", "Confirm adding this email address by using Single Sign On to prove your identity.": "Bevestig je identiteit met je eenmalige aanmelding om dit e-mailadres toe te voegen.", @@ -1989,7 +1989,7 @@ "Command failed": "Opdracht mislukt", "Could not find user in room": "Kon die deelnemer aan het gesprek niet vinden", "Please supply a widget URL or embed code": "Gelieve een widgetURL of in te bedden code te geven", - "Send a bug report with logs": "Rapporteer een fout, met foutopsporingslogboek bijgesloten", + "Send a bug report with logs": "Stuur een bugrapport met logs", "%(senderDisplayName)s changed the room name from %(oldRoomName)s to %(newRoomName)s.": "%(senderDisplayName)s heeft het gesprek %(oldRoomName)s hernoemd tot %(newRoomName)s.", "%(senderName)s added the alternative addresses %(addresses)s for this room.|other": "%(senderName)s heeft dit gesprek de nevenadressen %(addresses)s toegekend.", "%(senderName)s added the alternative addresses %(addresses)s for this room.|one": "%(senderName)s heeft dit gesprek het nevenadres %(addresses)s toegekend.", @@ -2315,7 +2315,7 @@ "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.": "Uw homeserver wees uw inlogpoging af. Dit kan zijn doordat het te lang heeft geduurd. Probeer het opnieuw. Als dit probleem zich blijft voordoen, neem contact op met de beheerder van uw homeserver.", "Your homeserver was unreachable and was not able to log you in. Please try again. If this continues, please contact your homeserver administrator.": "Uw homeserver was onbereikbaar en kon u niet inloggen, probeer het opnieuw. Wanneer dit probleem zich blijft voordoen, neem contact op met de beheerder van uw homeserver.", "Try again": "Probeer opnieuw", - "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.": "De browser is verzocht uw homeserver te onthouden die u gebruikt om zich aan te melden, maar is deze vergeten. Ga naar de aanmeldpagina en probeer het opnieuw.", + "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.": "De browser is verzocht uw homeserver te onthouden die u gebruikt om in te loggen, maar helaas heeft de browser deze vergeten. Ga naar de inlog-pagina en probeer het opnieuw.", "We couldn't log you in": "We konden u niet inloggen", "Room Info": "Gespreksinfo", "Matrix.org is the biggest public homeserver in the world, so it’s a good place for many.": "Matrix.org is de grootste openbare homeserver van de wereld, dus het is een goede plek voor vele.", @@ -2416,8 +2416,8 @@ "sends snowfall": "Stuur sneeuwvlokken", "sends confetti": "verstuurt confetti", "sends fireworks": "Stuur vuurwerk", - "Downloading logs": "Logboeken downloaden", - "Uploading logs": "Logboeken versturen", + "Downloading logs": "Logs downloaden", + "Uploading logs": "Logs uploaden", "Use Ctrl + Enter to send a message": "Gebruik Ctrl + Enter om een bericht te sturen", "Use Command + Enter to send a message": "Gebruik Command (⌘) + Enter om een bericht te sturen", "Use Ctrl + F to search": "Ctrl + F om te zoeken gebruiken", @@ -2425,7 +2425,7 @@ "Use a more compact ‘Modern’ layout": "Compacte 'Modern'-layout inschakelen", "Use custom size": "Aangepaste lettergrootte gebruiken", "Font size": "Lettergrootte", - "Enable advanced debugging for the room list": "Geavanceerde foutopsporing voor de gesprekkenlijst inschakelen", + "Enable advanced debugging for the room list": "Geavanceerde bugopsporing voor de gesprekkenlijst inschakelen", "Render LaTeX maths in messages": "Weergeef LaTeX-wiskundenotatie in berichten", "Change notification settings": "Meldingsinstellingen wijzigen", "%(senderName)s: %(stickerName)s": "%(senderName)s: %(stickerName)s", @@ -2449,7 +2449,7 @@ "%(senderName)s has updated the widget layout": "%(senderName)s heeft de widget-indeling bijgewerkt", "%(senderName)s declined the call.": "%(senderName)s heeft de oproep afgewezen.", "(an error occurred)": "(een fout is opgetreden)", - "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.": "U heeft eerder een nieuwere versie van %(brand)s in deze sessie gebruikt. Om deze versie opnieuw met eind-tot-eind-versleuteling te gebruiken, zult u zich moeten afmelden en opnieuw aanmelden.", + "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.": "U heeft eerder een nieuwere versie van %(brand)s in deze sessie gebruikt. Om deze versie opnieuw met eind-tot-eind-versleuteling te gebruiken, zult u moeten uitloggen en opnieuw inloggen.", "Block anyone not part of %(serverName)s from ever joining this room.": "Weiger iedereen die geen deel uitmaakt van %(serverName)s aan dit gesprek deel te nemen.", "Create a room in %(communityName)s": "Een gesprek aanmaken in %(communityName)s", "Enable end-to-end encryption": "Eind-tot-eind-versleuteling inschakelen", @@ -2467,7 +2467,7 @@ "Show": "Toon", "People you know on %(brand)s": "Personen die u kent van %(brand)s", "Add another email": "Nog een e-mailadres toevoegen", - "Download logs": "Download logboeken", + "Download logs": "Logs downloaden", "Add a new server...": "Een nieuwe server toevoegen…", "Server name": "Servernaam", "Add a new server": "Een nieuwe server toevoegen", @@ -2479,8 +2479,8 @@ "Enter a server name": "Geef een servernaam", "Continue with %(provider)s": "Doorgaan met %(provider)s", "Homeserver": "Homeserver", - "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.": "U kunt de aangepaste serverinstellingen gebruiken om u aan te melden bij andere Matrix-servers, door een andere homeserver-URL in te voeren. Dit laat u toe Element te gebruiken met een bestaande Matrix-account bij een andere homeserver.", - "Server Options": "Serverinstellingen", + "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.": "U kunt de server opties wijzigen om in te loggen bij andere Matrix-servers, wijzig hiervoor de homeserver-URL. Hiermee kunt u Element gebruiken met een al bestaand Matrix-account van een andere homeserver.", + "Server Options": "Server opties", "This address is already in use": "Dit adres is al in gebruik", "This address is available to use": "Dit adres kan worden gebruikt", "Please provide a room address": "Geef een gespreksadres", @@ -2539,7 +2539,7 @@ "Invite by email": "Via e-mail uitnodigen", "Click the button below to confirm your identity.": "Druk op de knop hieronder om uw identiteit te bevestigen.", "Confirm to continue": "Bevestig om door te gaan", - "Report a bug": "Een fout rapporteren", + "Report a bug": "Een bug rapporteren", "Comment": "Opmerking", "Add comment": "Opmerking toevoegen", "Tell us below how you feel about %(brand)s so far.": "Vertel ons hoe %(brand)s u tot dusver bevalt.", @@ -2640,7 +2640,7 @@ "Use this when referencing your community to others. The community ID cannot be changed.": "Gebruik dit om anderen naar uw gemeenschap te verwijzen. De gemeenschaps-ID kan later niet meer veranderd worden.", "Please go into as much detail as you like, so we can track down the problem.": "Gebruik a.u.b. zoveel mogelijk details, zodat wij uw probleem kunnen vinden.", "There are two ways you can provide feedback and help us improve %(brand)s.": "U kunt op twee manieren feedback geven en ons helpen %(brand)s te verbeteren.", - "Please view existing bugs on Github first. No match? Start a new one.": "Bekijk eerst de bestaande problemen op Github. Maak een nieuwe aan wanneer u uw probleem niet heeft gevonden.", + "Please view existing bugs on Github first. No match? Start a new one.": "Bekijk eerst de bestaande bugs op GitHub. Maak een nieuwe aan wanneer u uw bugs niet heeft gevonden.", "Invite someone using their name, email address, username (like ) or share this room.": "Nodig iemand uit door gebruik te maken van hun naam, e-mailadres, gebruikersnaam (zoals ) of deel dit gesprek.", "Invite someone using their name, username (like ) or share this room.": "Nodig iemand uit door gebruik te maken van hun naam, gebruikersnaam (zoals ) of deel dit gesprek.", "Send feedback": "Feedback versturen", @@ -2738,13 +2738,13 @@ "Use Security Key or Phrase": "Gebruik veiligheidssleutel of -wachtwoord", "Decide where your account is hosted": "Kies waar uw account wordt gehost", "Host account on": "Host uw account op", - "Already have an account? Sign in here": "Heeft u al een account? Aanmelden", + "Already have an account? Sign in here": "Heeft u al een account? Inloggen", "%(ssoButtons)s Or %(usernamePassword)s": "%(ssoButtons)s of %(usernamePassword)s", "Continue with %(ssoButtons)s": "Ga verder met %(ssoButtons)s", "That username already exists, please try another.": "Die gebruikersnaam bestaat al, probeer een andere.", "New? Create account": "Nieuw? Maak een account aan", "If you've joined lots of rooms, this might take a while": "Als u zich bij veel gesprekken heeft aangesloten, kan dit een tijdje duren", - "Signing In...": "Aanmelden...", + "Signing In...": "Inloggen...", "Syncing...": "Synchroniseren...", "There was a problem communicating with the homeserver, please try again later.": "Er was een communicatieprobleem met de homeserver, probeer het later opnieuw.", "Community and user menu": "Gemeenschaps- en gebruikersmenu", @@ -2754,7 +2754,7 @@ "User settings": "Gebruikersinstellingen", "Security & privacy": "Veiligheid & privacy", "New here? Create an account": "Nieuw hier? Maak een account", - "Got an account? Sign in": "Heeft u een account? Aanmelden", + "Got an account? Sign in": "Heeft u een account? Inloggen", "Failed to find the general chat for this community": "De algemene chat voor deze gemeenschap werd niet gevonden", "Filter rooms and people": "Gespreken en personen filteren", "Explore rooms in %(communityName)s": "Ontdek de gesprekken van %(communityName)s", @@ -2774,8 +2774,8 @@ "Create community": "Gemeenschap aanmaken", "Attach files from chat or just drag and drop them anywhere in a room.": "Voeg bestanden toe vanuit het gesprek of sleep ze in een gesprek.", "No files visible in this room": "Geen bestanden zichtbaar in dit gesprek", - "Sign in with SSO": "Aanmelden met SSO", - "Use email to optionally be discoverable by existing contacts.": "Gebruik e-mail om optioneel ontdekt te worden door bestaande contacten.", + "Sign in with SSO": "Inloggen met SSO", + "Use email to optionally be discoverable by existing contacts.": "Gebruik e-mail ook om optioneel ontdekt te worden door bestaande contacten.", "Use email or phone to optionally be discoverable by existing contacts.": "Gebruik e-mail of telefoon om optioneel ontdekt te kunnen worden door bestaande contacten.", "Add an email to be able to reset your password.": "Voeg een e-mail toe om uw wachtwoord te kunnen resetten.", "Forgot password?": "Wachtwoord vergeten?", @@ -2852,7 +2852,7 @@ "Continuing temporarily allows the %(hostSignupBrand)s setup process to access your account to fetch verified email addresses. This data is not stored.": "Door tijdelijk door te gaan, krijgt het installatieproces van %(hostSignupBrand)s toegang tot uw account om geverifieerde e-mailadressen op te halen. Deze gegevens worden niet opgeslagen.", "Failed to connect to your homeserver. Please close this dialog and try again.": "Kan geen verbinding maken met uw homeserver. Sluit dit dialoogvenster en probeer het opnieuw.", "Are you sure you wish to abort creation of the host? The process cannot be continued.": "Weet u zeker dat u het aanmaken van de host wilt afbreken? Het proces kan niet worden voortgezet.", - "PRO TIP: If you start a bug, please submit debug logs to help us track down the problem.": "PRO TIP: Als u een bug start, stuur ons dan debug logs om ons te helpen het probleem op te sporen.", + "PRO TIP: If you start a bug, please submit debug logs to help us track down the problem.": "PRO TIP: Als u een nieuwe bug maakt, stuur ons dan uw foutenlogboek om ons te helpen het probleem op te sporen.", "There was an error updating your community. The server is unable to process your request.": "Er is een fout opgetreden bij het updaten van uw gemeenschap. De server is niet in staat om uw verzoek te verwerken.", "There was an error finding this widget.": "Er is een fout opgetreden bij het vinden van deze widget.", "Server did not return valid authentication information.": "Server heeft geen geldige verificatiegegevens teruggestuurd.", @@ -2889,7 +2889,7 @@ "Submit logs": "Logs versturen", "Messages in this room are end-to-end encrypted. When people join, you can verify them in their profile, just tap on their avatar.": "Berichten in dit gesprek zijn eind-tot-eind-versleuteld. Als personen deelnemen, kan u ze verifiëren in hun profiel, tik hiervoor op hun avatar.", "In encrypted rooms, verify all users to ensure it’s secure.": "Controleer alle gebruikers in versleutelde gesprekken om er zeker van te zijn dat het veilig is.", - "Verify all users in a room to ensure it's secure.": "Controleer alle gebruikers in een gesprek om er zeker van te zijn dat hij veilig is.", + "Verify all users in a room to ensure it's secure.": "Controleer alle gebruikers in een gesprek om er zeker van te zijn dat het veilig is.", "%(count)s people|one": "%(count)s persoon", "Add widgets, bridges & bots": "Widgets, bruggen & bots toevoegen", "Edit widgets, bridges & bots": "Widgets, bruggen & bots bewerken", @@ -2921,10 +2921,10 @@ "%(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 kan versleutelde berichten niet veilig lokaal opslaan in een webbrowser. Gebruik %(brand)s Desktop om versleutelde berichten in zoekresultaten te laten verschijnen.", "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|one": "Veilig lokaal opslaan van versleutelde berichten zodat ze in de zoekresultaten verschijnen, gebruik %(size)s voor het opslaan van berichten uit %(rooms)s gesprek.", "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|other": "Veilig lokaal opslaan van versleutelde berichten zodat ze in de zoekresultaten verschijnen, gebruik %(size)s voor het opslaan van berichten uit %(rooms)s gesprekken.", - "Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.": "Elke sessie die door een gebruiker wordt gebruikt, afzonderlijk verifiëren om deze als vertrouwd aan te merken, waarbij geen vertrouwen wordt gesteld in kruiselings ondertekende apparaten.", - "User signing private key:": "Gebruiker ondertekening privésleutel:", - "Master private key:": "Hoofd privésleutel:", - "Self signing private key:": "Zelfondertekenende privésleutel:", + "Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.": "Verifieer elke sessie die door een gebruiker wordt gebruikt afzonderlijk om deze te markeren als vertrouwd, niet vertrouwend op kruislings ondertekende apparaten.", + "User signing private key:": "Gebruikerondertekening-privésleutel:", + "Master private key:": "Hoofdprivésleutel:", + "Self signing private key:": "Zelfondertekening-privésleutel:", "Cross-signing is not set up.": "Kruiselings ondertekenen is niet ingesteld.", "Cross-signing is ready for use.": "Kruiselings ondertekenen is klaar voor gebruik.", "Your server isn't responding to some requests.": "Uw server reageert niet op sommige verzoeken.", @@ -2941,13 +2941,13 @@ "Minimize dialog": "Dialoog minimaliseren", "If you cancel now, you may lose encrypted messages & data if you lose access to your logins.": "Als u nu annuleert, kunt u versleutelde berichten en gegevens verliezen als u geen toegang meer heeft tot uw login.", "Confirm your identity by verifying this login from one of your other sessions, granting it access to encrypted messages.": "Bevestig uw identiteit door deze login te verifiëren vanuit een van uw andere sessies, waardoor u toegang krijgt tot uw versleutelde berichten.", - "Verify this login": "Controleer deze login", + "Verify this login": "Deze inlog verifiëren", "To continue, use Single Sign On to prove your identity.": "Om verder te gaan, gebruik uw eenmalige aanmelding om uw identiteit te bewijzen.", "Prepends ( ͡° ͜ʖ ͡°) to a plain-text message": "Plakt ( ͡° ͜ʖ ͡°) vóór een bericht zonder opmaak", "Prepends ┬──┬ ノ( ゜-゜ノ) to a plain-text message": "Plakt ┬──┬ ノ( ゜-゜ノ) vóór een bericht zonder opmaak", "Prepends (╯°□°)╯︵ ┻━┻ to a plain-text message": "Plakt (╯°□°)╯︵ ┻━┻ vóór een bericht zonder opmaak", "Liberate your communication": "Bevrijd uw communicatie", - "Create a Group Chat": "Maak een groepsgesprek aan", + "Create a Group Chat": "Maak een groepsgesprek", "Send a Direct Message": "Start een direct gesprek", "Welcome to %(appName)s": "Welkom bij %(appName)s", "Add a topic to help people know what it is about.": "Stel een gespreksonderwerp in zodat de personen weten waar het over gaat.", @@ -3024,7 +3024,7 @@ "Start audio stream": "Audio-stream starten", "Failed to start livestream": "Starten van livestream is mislukt", "Unable to start audio streaming.": "Kan audio-streaming niet starten.", - "Save Changes": "Wijzigingen Opslaan", + "Save Changes": "Wijzigingen opslaan", "Saving...": "Opslaan...", "View dev tools": "Bekijk dev tools", "Leave Space": "Space verlaten", @@ -3136,7 +3136,7 @@ "A new login is accessing your account: %(name)s (%(deviceID)s) at %(ip)s": "Een nieuwe login heeft toegang tot uw account: %(name)s (%(deviceID)s) op %(ip)s", "You have unverified logins": "U heeft ongeverifieerde logins", "Without verifying, you won’t have access to all your messages and may appear as untrusted to others.": "Zonder verifiëren heeft u geen toegang tot al uw berichten en kan u als onvertrouwd aangemerkt staan bij anderen.", - "Verify your identity to access encrypted messages and prove your identity to others.": "Verifeer uw identiteit om toegang te krijgen tot uw versleutelde berichten en uw identiteit te bewijzen voor anderen.", + "Verify your identity to access encrypted messages and prove your identity to others.": "Verifeer uw identiteit om toegang te krijgen tot uw versleutelde berichten en om uw identiteit te bewijzen voor anderen.", "Use another login": "Gebruik andere login", "Please choose a strong password": "Kies een sterk wachtwoord", "You can add more later too, including already existing ones.": "U kunt er later nog meer toevoegen, inclusief al bestaande gesprekken.", @@ -3170,7 +3170,7 @@ "Share decryption keys for room history when inviting users": "Deel ontsleutelsleutels voor de gespreksgeschiedenis wanneer u personen uitnodigd", "Send and receive voice messages (in development)": "Verstuur en ontvang audioberichten (in ontwikkeling)", "%(deviceId)s from %(ip)s": "%(deviceId)s van %(ip)s", - "Review to ensure your account is safe": "Controleer ze voor de zekerheid dat uw account veilig is", + "Review to ensure your account is safe": "Controleer ze zodat uw account veilig is", "Sends the given message as a spoiler": "Verstuurt het bericht als een spoiler", "You are the only person here. If you leave, no one will be able to join in the future, including you.": "U bent de enige persoon hier. Als u weggaat, zal niemand in de toekomst kunnen toetreden, u ook niet.", "If you reset everything, you will restart with no trusted sessions, no trusted users, and might not be able to see past messages.": "Als u alles reset, zult u opnieuw opstarten zonder vertrouwde sessies, zonder vertrouwde gebruikers, en zult u misschien geen vroegere berichten meer kunnen zien.", @@ -3205,5 +3205,40 @@ "%(count)s results in all spaces|other": "%(count)s resultaten in alle spaces", "You have no ignored users.": "U heeft geen gebruiker genegeerd.", "Play": "Afspelen", - "Pause": "Pauze" + "Pause": "Pauze", + "This is an experimental feature. For now, new users receiving an invite will have to open the invite on to actually join.": "Dit is een experimentele functie. Voorlopig moeten nieuwe personen die een uitnodiging krijgen de gebruiken om daadwerkelijk deel te nemen.", + "To join %(spaceName)s, turn on the Spaces beta": "Om aan %(spaceName)s deel te nemen moet u de Spaces beta inschakelen", + "To view %(spaceName)s, turn on the Spaces beta": "Om %(spaceName)s te bekijken moet u de Spaces beta inschakelen", + "Select a room below first": "Start met selecteren van een gesprek hieronder", + "Communities are changing to Spaces": "Gemeenschappen worden vervangen door Spaces", + "Join the beta": "Aan beta deelnemen", + "Leave the beta": "Beta verlaten", + "Beta": "Beta", + "Tap for more info": "Klik voor meer info", + "Spaces is a beta feature": "Spaces zijn in beta", + "Want to add a new room instead?": "Wilt u anders een nieuw gesprek toevoegen?", + "Adding rooms... (%(progress)s out of %(count)s)|one": "Gesprek toevoegen...", + "Adding rooms... (%(progress)s out of %(count)s)|other": "Gesprekken toevoegen... (%(progress)s van %(count)s)", + "Not all selected were added": "Niet alle geselecteerden zijn toegevoegd", + "You can add existing spaces to a space.": "U kunt bestaande spaces toevoegen aan een space.", + "Feeling experimental?": "Zin in een experiment?", + "You are not allowed to view this server's rooms list": "U heeft geen toegang tot deze server zijn gesprekkenlijst", + "Error processing voice message": "Fout bij verwerking spraakbericht", + "We didn't find a microphone on your device. Please check your settings and try again.": "We hebben geen microfoon gevonden op uw apparaat. Controleer uw instellingen en probeer het opnieuw.", + "No microphone found": "Geen microfoon gevonden", + "We were unable to access your microphone. Please check your browser settings and try again.": "We hebben geen toegang tot uw microfoon. Controleer uw browserinstellingen en probeer het opnieuw.", + "Unable to access your microphone": "Geen toegang tot uw microfoon", + "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.": "Zin in een experiment? Labs is de beste manier om dingen vroeg te krijgen, nieuwe functies uit te testen en ze te helpen vormen voordat ze daadwerkelijk worden gelanceerd. Lees meer.", + "Your access token gives full access to your account. Do not share it with anyone.": "Uw toegangstoken geeft u toegang to uw account. Deel hem niet met anderen.", + "Access Token": "Toegangstoken", + "Spaces are a new way to group rooms and people. To join an existing space you'll need an invite.": "Spaces zijn de nieuwe manier om gesprekken en personen te groeperen. Om aan een bestaande space deel te nemen heeft u een uitnodiging nodig.", + "Please enter a name for the space": "Vul een naam in voor deze space", + "Connecting": "Verbinden", + "Allow Peer-to-Peer for 1:1 calls (if you enable this, the other party might be able to see your IP address)": "Peer-to-peer voor 1op1 oproepen toestaan (als u dit inschakelt kunnen andere personen mogelijk uw ipadres zien)", + "Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.": "Beta beschikbaar voor web, desktop en Android. Sommige functies zijn nog niet beschikbaar op uw homeserver.", + "You can leave the beta any time from settings or tapping on a beta badge, like the one above.": "U kunt de beta elk moment verlaten via instellingen of door op de beta badge hierboven te klikken.", + "%(brand)s will reload with Spaces enabled. Communities and custom tags will be hidden.": "%(brand)s zal herladen met Spaces ingeschakeld. Gemeenschappen en labels worden verborgen.", + "Beta available for web, desktop and Android. Thank you for trying the beta.": "Beta beschikbaar voor web, desktop en Android. Bedankt dat u de beta wilt proberen.", + "%(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "%(brand)s zal herladen met Spaces uitgeschakeld. Gemeenschappen en labels zullen weer zichtbaar worden.", + "Spaces are a new way to group rooms and people.": "Spaces zijn de nieuwe manier om gesprekken en personen te groeperen." } From 9e981ae1189b6649fa643abed9cdde6894f9d19b Mon Sep 17 00:00:00 2001 From: Jeff Huang Date: Wed, 12 May 2021 02:44:24 +0000 Subject: [PATCH 037/464] Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (2959 of 2959 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 | 37 ++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json index 2dff300c18..51ebb36dc0 100644 --- a/src/i18n/strings/zh_Hant.json +++ b/src/i18n/strings/zh_Hant.json @@ -3322,5 +3322,40 @@ "%(count)s results in all spaces|other": "所有空間中有 %(count)s 個結果", "You have no ignored users.": "您沒有忽略的使用者。", "Play": "播放", - "Pause": "暫停" + "Pause": "暫停", + "This is an experimental feature. For now, new users receiving an invite will have to open the invite on to actually join.": "這是實驗性功能。目前,收到邀請的新使用者必須在 上開啟邀請才能真的加入。", + "To join %(spaceName)s, turn on the Spaces beta": "要加入 %(spaceName)s,請開啟空間測試版", + "To view %(spaceName)s, turn on the Spaces beta": "要檢視 %(spaceName)s,開啟空間測試版", + "Select a room below first": "首先選取一個聊天室", + "Communities are changing to Spaces": "社群正在變更為空間", + "Join the beta": "加入測試版", + "Leave the beta": "離開測試版", + "Beta": "測試", + "Tap for more info": "點擊以取得更多資訊", + "Spaces is a beta feature": "空間為測試功能", + "Want to add a new room instead?": "想要新增新聊天室嗎?", + "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 can add existing spaces to a space.": "您可以新增既有的空間至空間中。", + "Feeling experimental?": "想要來點實驗嗎?", + "You are not allowed to view this server's rooms list": "您不被允許檢視此伺服器的聊天室清單", + "Error processing 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": "無法存取您的麥克風", + "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": "存取權杖", + "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": "請輸入空間名稱", + "Connecting": "正在連線", + "Allow Peer-to-Peer for 1:1 calls (if you enable this, the other party might be able to see your IP address)": "允許在 1:1 通話中使用點對點通訊(若您啟用此功能,對方就能看到您的 IP 位置)", + "Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.": "供網頁、桌面與 Android 使用的測試版。部份功能可能在您的家伺服器上不可用。", + "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.": "測試版可用於網路、桌面與 Android。感謝您試用測試版。", + "%(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.": "空間是將聊天室與人們分組的一種新方式。" } From 3169f75e909ceb4d5d2d7c677efb07b9a1515d5d Mon Sep 17 00:00:00 2001 From: waclaw66 Date: Wed, 12 May 2021 08:21:58 +0000 Subject: [PATCH 038/464] Translated using Weblate (Czech) Currently translated at 100.0% (2959 of 2959 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 | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/cs.json b/src/i18n/strings/cs.json index c9dd63f637..6d4cb953cc 100644 --- a/src/i18n/strings/cs.json +++ b/src/i18n/strings/cs.json @@ -3236,5 +3236,40 @@ "Pause": "Pozastavit", "Enter your Security Phrase a second time to confirm it.": "Zadejte bezpečnostní frázi podruhé a potvrďte ji.", "Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.": "Vyberte místnosti nebo konverzace, které chcete přidat. Toto je prostor pouze pro vás, nikdo nebude informován. Později můžete přidat další.", - "You have no ignored users.": "Nemáte žádné ignorované uživatele." + "You have no ignored users.": "Nemáte žádné ignorované uživatele.", + "This is an experimental feature. For now, new users receiving an invite will have to open the invite on to actually join.": "Jedná se o experimentální funkci. Noví uživatelé, kteří obdrží pozvánku, ji budou muset otevřít na , aby se mohli připojit.", + "To join %(spaceName)s, turn on the Spaces beta": "Pro připojení k %(spaceName)s, zapněte Prostory beta", + "To view %(spaceName)s, turn on the Spaces beta": "Pro zobrazení %(spaceName)s, zapněte Prostory beta", + "Select a room below first": "Nejprve si vyberte místnost níže", + "Communities are changing to Spaces": "Skupiny se mění na Prostory", + "Join the beta": "Připojit se k beta verzi", + "Leave the beta": "Opustit beta verzi", + "Beta": "Beta", + "Tap for more info": "Klepněte pro více informací", + "Spaces is a beta feature": "Prostory jsou beta verze", + "Want to add a new room instead?": "Chcete místo toho přidat novou místnost?", + "Adding rooms... (%(progress)s out of %(count)s)|one": "Přidávání místnosti...", + "Adding rooms... (%(progress)s out of %(count)s)|other": "Přidávání místností... (%(progress)s z %(count)s)", + "Not all selected were added": "Ne všechny vybrané byly přidány", + "You can add existing spaces to a space.": "Do prostoru můžete přidat existující prostory.", + "Feeling experimental?": "Chcete experimentovat?", + "You are not allowed to view this server's rooms list": "Namáte oprávnění zobrazit seznam místností tohoto serveru", + "Error processing voice message": "Chyba při zpracování hlasové zprávy", + "We didn't find a microphone on your device. Please check your settings and try again.": "Ve vašem zařízení nebyl nalezen žádný mikrofon. Zkontrolujte prosím nastavení a zkuste to znovu.", + "No microphone found": "Nebyl nalezen žádný mikrofon", + "We were unable to access your microphone. Please check your browser settings and try again.": "Nepodařilo se získat přístup k vašemu mikrofonu . Zkontrolujte prosím nastavení prohlížeče a zkuste to znovu.", + "Unable to access your microphone": "Nelze získat přístup k mikrofonu", + "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.": "Chcete experimentovat? Laboratoře jsou nejlepším způsobem, jak získat novinky v raném stádiu, vyzkoušet nové funkce a pomoci je formovat ještě před jejich spuštěním. Zjistěte více.", + "Your access token gives full access to your account. Do not share it with anyone.": "Přístupový token vám umožní plný přístup k účtu. Nikomu ho nesdělujte.", + "Access Token": "Přístupový token", + "Spaces are a new way to group rooms and people. To join an existing space you'll need an invite.": "Prostory představují nový způsob seskupování místností a osob. Chcete-li se připojit k existujícímu prostoru, potřebujete pozvánku.", + "Please enter a name for the space": "Zadejte prosím název prostoru", + "Connecting": "Spojování", + "Allow Peer-to-Peer for 1:1 calls (if you enable this, the other party might be able to see your IP address)": "Povolit Peer-to-Peer pro hovory 1:1 (pokud tuto funkci povolíte, druhá strana může vidět vaši IP adresu)", + "Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.": "Beta verze je k dispozici pro web, desktop a Android. Některé funkce mohou být na vašem domovském serveru nedostupné.", + "You can leave the beta any time from settings or tapping on a beta badge, like the one above.": "Beta verzi můžete kdykoli opustit v nastavení nebo klepnutím na štítek beta verze, jako je ten výše.", + "%(brand)s will reload with Spaces enabled. Communities and custom tags will be hidden.": "%(brand)s se znovu načte s povolenými Prostory. Skupiny a vlastní značky budou skryty.", + "Beta available for web, desktop and Android. Thank you for trying the beta.": "Beta verze je k dispozici pro web, desktop a Android. Děkujeme vám za vyzkoušení beta verze.", + "%(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "%(brand)s se znovu načte s vypnutými Prostory. Skupiny a vlastní značky budou opět viditelné.", + "Spaces are a new way to group rooms and people.": "Prostory představují nový způsob seskupování místností a osob." } From 68fcc259d0fbe6c44ef768c8811ee88bc782cb83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Tue, 11 May 2021 17:04:44 +0000 Subject: [PATCH 039/464] Translated using Weblate (Estonian) Currently translated at 100.0% (2959 of 2959 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 | 43 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/src/i18n/strings/et.json b/src/i18n/strings/et.json index fc42ad73dd..88480ceb6e 100644 --- a/src/i18n/strings/et.json +++ b/src/i18n/strings/et.json @@ -652,7 +652,7 @@ "A session's public name is visible to people you communicate with": "Sessiooni avalik nimi on nähtav neile, kellega sa suhtled", "%(brand)s collects anonymous analytics to allow us to improve the application.": "Võimaldamaks meil rakendust parandada kogub %(brand)s anonüümset teavet rakenduse kasutuse kohta.", "Privacy is important to us, so we don't collect any personal or identifiable data for our analytics.": "Privaatsus on meile oluline ning seega me ei kogu ei isiklikke ega isikustatavaid andmeid.", - "Learn more about how we use analytics.": "Loe lisaks kuidas me kasutama analüütikat.", + "Learn more about how we use analytics.": "Loe lisaks selles kohta, kuidas me kasutame analüütikat.", "No media permissions": "Meediaõigused puuduvad", "You may need to manually permit %(brand)s to access your microphone/webcam": "Sa võib-olla pead andma %(brand)s'ile loa mikrofoni ja veebikaamera kasutamiseks", "Missing media permissions, click the button below to request.": "Meediaga seotud õigused puuduvad. Nende nõutamiseks klõpsi järgnevat nuppu.", @@ -1202,8 +1202,8 @@ "eg: @bot:* or example.org": "näiteks: @bot:* või example.org", "Subscribed lists": "Tellitud loendid", "Subscribe": "Telli", - "Start automatically after system login": "Käivita automaatselt peale arvutisse sisselogimist", - "Always show the window menu bar": "Näita alati aknas menüüriba", + "Start automatically after system login": "Käivita Element automaatselt peale arvutisse sisselogimist", + "Always show the window menu bar": "Näita aknas alati menüüriba", "Preferences": "Eelistused", "Room list": "Jututubade loend", "Timeline": "Ajajoon", @@ -3297,5 +3297,40 @@ "%(count)s results in all spaces|other": "%(count)s tulemust kõikides kogukonnakeskustes", "You have no ignored users.": "Sa ei ole veel kedagi eiranud.", "Play": "Esita", - "Pause": "Peata" + "Pause": "Peata", + "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.": "Kas sa tahaksid katsetada? Sa tutvud meie rakenduse uuendustega teistest varem ja võib-olla isegi saad mõjutada arenduse lõpptulemust. Lisateavet liad siit.", + "This is an experimental feature. For now, new users receiving an invite will have to open the invite on to actually join.": "See on katseline funktsionaalsus. Seetõttu uued kutse saanud kasutajad peavad tegelikuks liitumiseks avama kutse siin .", + "To join %(spaceName)s, turn on the Spaces beta": "%(spaceName)s kogukonnakeskusega liitumiseks lülita sisse vastav katseline funktsionaalsus", + "To view %(spaceName)s, turn on the Spaces beta": "%(spaceName)s kogukonnakeskuse vaatamiseks lülita sisse vastav katseline funktsionaalsus", + "Select a room below first": "Esmalt vali alljärgnevast üks jututuba", + "Communities are changing to Spaces": "Seniste kogukondade asemele tulevad kogukonnakeskused", + "Join the beta": "Hakka kasutama beetaversiooni", + "Leave the beta": "Lõpeta beetaversiooni kasutamine", + "Beta": "Beetaversioon", + "Tap for more info": "Lisateabe jaoks klõpsi", + "Spaces is a beta feature": "Kogukonnakeskused on veel katsetamisjärgus funktsionaalsus", + "Want to add a new room instead?": "Kas sa selle asemel soovid lisada jututuba?", + "Adding rooms... (%(progress)s out of %(count)s)|one": "Lisan jututuba...", + "Adding rooms... (%(progress)s out of %(count)s)|other": "Lisan jututubasid... (%(progress)s/%(count)s)", + "Not all selected were added": "Kõiki valituid me ei lisanud", + "You can add existing spaces to a space.": "Sa võid kogukonnakeskusele lisada ka teisi kogukonnakeskuseid.", + "Feeling experimental?": "Kas sa tahaksid natukene katsetada?", + "You are not allowed to view this server's rooms list": "Sul puuduvad õigused selle serveri jututubade loendi vaatamiseks", + "Error processing voice message": "Viga häälsõnumi töötlemisel", + "We didn't find a microphone on your device. Please check your settings and try again.": "Me ei suutnud sinu seadmest leida mikrofoni. Palun kontrolli seadistusi ja proovi siis uuesti.", + "No microphone found": "Mikrofoni ei leidu", + "We were unable to access your microphone. Please check your browser settings and try again.": "Meil puudub ligipääs sinu mikrofonile. Palun kontrolli oma veebibrauseri seadistusi ja proovi uuesti.", + "Unable to access your microphone": "Puudub ligipääs mikrofonile", + "Your access token gives full access to your account. Do not share it with anyone.": "Sinu pääsuluba annab täismahulise ligipääsu sinu kasutajakontole. Palun ära jaga seda teistega.", + "Access Token": "Pääsuluba", + "Spaces are a new way to group rooms and people. To join an existing space you'll need an invite.": "Kogukonnakeskused on uus viis inimeste ja jututubade ühendamiseks. Kogukonnakeskusega liitumiseks vajad sa kutset.", + "Please enter a name for the space": "Palun sisesta kogukonnakeskuse nimi", + "Connecting": "Kõne on ühendamisel", + "Allow Peer-to-Peer for 1:1 calls (if you enable this, the other party might be able to see your IP address)": "Kasuta võrdõigusvõrku 1:1 kõnede jaoks (kui sa P2P-võrgu sisse lülitad, siis teine osapool ilmselt näeb sinu IP-aadressi)", + "Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.": "Rakenduse beetaversioon on saadaval veebirakendusena, töölauarakendusena ja Androidi jaoks. Kõik funtsionaalsused ei pruugi sinu koduserveri poolt olla toetatud.", + "You can leave the beta any time from settings or tapping on a beta badge, like the one above.": "Sa võid beetaversiooni kasutamise lõpetada niipea, kui tahad. Selleks klõpsi beeta-silti, mida näed siin samas ülal.", + "%(brand)s will reload with Spaces enabled. Communities and custom tags will be hidden.": "Käivitame %(brand)s uuesti nii, et kogukonnakeskused on kasutusel. Vana tüüpi kogukonnad ja kohandatud sildid on siis välja lülitatud.", + "%(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "Käivitame %(brand)s uuesti nii, et kogukonnakeskused ei ole kasutusel. Vana tüüpi kogukonnad ja kohandatud sildid saavad jälle olema kasutusel.", + "Beta available for web, desktop and Android. Thank you for trying the beta.": "Rakenduse beetaversioon on saadaval veebirakendusena, töölauarakendusena ja Androidi jaoks. Tänud, et oled huviline katsetama meie rakendust.", + "Spaces are a new way to group rooms and people.": "Kogukonnakeskused on uus viis jututubade ja inimeste ühendamiseks." } From f595813e21af32fba9331f2cf527e33d77e55deb Mon Sep 17 00:00:00 2001 From: libexus Date: Thu, 13 May 2021 18:00:02 +0000 Subject: [PATCH 040/464] Translated using Weblate (German) Currently translated at 99.1% (2935 of 2959 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 | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index 23f6071efc..dbd2df28e8 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -338,7 +338,7 @@ "Uploading %(filename)s and %(count)s others|one": "%(filename)s und %(count)s weitere Dateien werden hochgeladen", "Uploading %(filename)s and %(count)s others|other": "%(filename)s und %(count)s weitere Dateien werden hochgeladen", "You must register to use this functionality": "Du musst dich registrieren, um diese Funktionalität nutzen zu können", - "Create new room": "Neuen Raum erstellen", + "Create new room": "Neuer Raum", "Room directory": "Raum-Verzeichnis", "Start chat": "Chat starten", "New Password": "Neues Passwort", @@ -2597,7 +2597,7 @@ "See videos posted to your active room": "In deinen aktiven Raum gesendete Videos anzeigen", "See videos posted to this room": "In diesen Raum gesendete Videos anzeigen", "Send images as you in this room": "Bilder als du in diesen Raum senden", - "Send images as you in your active room": "Sende Bilder als deine Person in den aktiven Raum.", + "Send images as you in your active room": "Sende Bilder in den aktuellen Raum", "See images posted to this room": "In diesen Raum gesendete Bilder anzeigen", "See images posted to your active room": "In deinen aktiven Raum gesendete Bilder anzeigen", "Send videos as you in this room": "Videos als du in diesen Raum senden", @@ -2945,7 +2945,7 @@ "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.": "Du kannst in den benutzerdefinierten Serveroptionen eine andere Heimserver-URL angeben, um dich bei anderen Matrixservern anzumelden.", "Server Options": "Servereinstellungen", "No other application is using the webcam": "keine andere Anwendung auf die Webcam zugreift", - "Permission is granted to use the webcam": "Zugriff auf die Webcam ist gestattet.", + "Permission is granted to use the webcam": "Zugriff auf Webcam gestattet", "A microphone and webcam are plugged in and set up correctly": "Mikrofon und Webcam eingesteckt und richtig eingerichtet sind", "Call failed because no microphone could not be accessed. Check that a microphone is plugged in and set up correctly.": "Der Anruf ist fehlgeschlagen weil nicht auf das Mikrofon zugegriffen werden konnte. Stelle sicher, dass das Mikrofon richtig eingesteckt und eingerichtet ist.", "Call failed because no webcam or microphone could not be accessed. Check that:": "Der Anruf ist fehlgeschlagen weil nicht auf das Mikrofon oder die Webcam zugegriffen werden konnte. Stelle sicher, dass:", @@ -3084,12 +3084,12 @@ "Accept Invite": "Einladung akzeptieren", "Save changes": "Änderungen speichern", "Undo": "Rückgängig", - "Save Changes": "Änderungen Speichern", + "Save Changes": "Speichern", "View dev tools": "Entwicklerwerkzeuge", "Apply": "Anwenden", "Create a new room": "Neuen Raum erstellen", "Suggested Rooms": "Vorgeschlagene Räume", - "Add existing room": "Existierenden Raum hinzufügen", + "Add existing room": "Existierenden Raum", "Send message": "Nachricht senden", "New room": "Neuer Raum", "Share invite link": "Einladungslink teilen", @@ -3241,7 +3241,7 @@ "Values at explicit levels in this room": "Werte für explizite Stufen in diesem Raum", "Confirm abort of host creation": "Bestätige das Abbrechen der Host-Erstellung", "Are you sure you wish to abort creation of the host? The process cannot be continued.": "Soll die Host-Erstellung wirklich abgebrochen werden? Dieser Prozess kann nicht wieder fortgesetzt werden.", - "Invite to just this room": "Nur für diesen Raum einladen", + "Invite to just this room": "Nur in diesen Raum einladen", "Consult first": "Konsultiere zuerst", "Reset event store?": "Ereignisspeicher zurück setzen?", "You most likely do not want to reset your event index store": "Es ist wahrscheinlich, dass du den Ereignis-Indexspeicher nicht zurück setzen möchtest", @@ -3253,7 +3253,7 @@ "What are some things you want to discuss in %(spaceName)s?": "Welche Themen willst du in %(spaceName)s besprechen?", "Inviting...": "Einladen...", "Failed to create initial space rooms": "Fehler beim Initialisieren des Space", - "You are the only person here. If you leave, no one will be able to join in the future, including you.": "Du bist hier noch alleine. Wenn du den Space verlässt, ist er für immer verloren (eine lange Zeit).", + "You are the only person here. If you leave, no one will be able to join in the future, including you.": "Du bist die einzige Person hier. Wenn du den Space verlässt, ist er für immer verloren (eine lange Zeit).", "Edit settings relating to your space.": "Einstellungen vom Space bearbeiten.", "Please choose a strong password": "Bitte gib ein sicheres Passwort ein", "If you reset everything, you will restart with no trusted sessions, no trusted users, and might not be able to see past messages.": "Wenn du alles zurücksetzt, gehen alle verifizierten Anmeldungen, Benutzer und verschlüsselte Nachrichten verloren.", @@ -3288,11 +3288,11 @@ "What do you want to organise?": "Was willst du organisieren?", "Enter your Security Phrase a second time to confirm it.": "Gib dein Kennwort ein zweites Mal zur Bestätigung ein.", "Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.": "Wähle Räume oder Konversationen die Du hinzufügen möchtest. Dieser Bereich ist nur für Dich, niemand wird informiert. Du kannst später mehr hinzufügen.", - "Filter all spaces": "Alle Bereiche filtern", + "Filter all spaces": "Alle Spaces durchsuchen", "Delete recording": "Aufnahme löschen", "Stop the recording": "Aufnahme stoppen", - "%(count)s results in all spaces|one": "%(count)s Ergebnis in allen Bereichen", - "%(count)s results in all spaces|other": "%(count)s Ergebnisse in allen Bereichen", + "%(count)s results in all spaces|one": "%(count)s Ergebnis", + "%(count)s results in all spaces|other": "%(count)s Ergebnisse", "You have no ignored users.": "Du ignorierst keine Benutzer.", "Error processing voice message": "Fehler beim Verarbeiten der Sprachnachricht", "To join %(spaceName)s, turn on the Spaces beta": "Um %(spaceName)s beizutreten, aktiviere die Spaces Betaversion", From 7248f28278f261d44abf2e9b9a76737d8ba769a6 Mon Sep 17 00:00:00 2001 From: random Date: Thu, 13 May 2021 10:26:49 +0000 Subject: [PATCH 041/464] Translated using Weblate (Italian) Currently translated at 100.0% (2959 of 2959 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 | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/it.json b/src/i18n/strings/it.json index a393a83409..00fce6dcb2 100644 --- a/src/i18n/strings/it.json +++ b/src/i18n/strings/it.json @@ -3319,5 +3319,40 @@ "%(count)s results in all spaces|other": "%(count)s risultati in tutti gli spazi", "You have no ignored users.": "Non hai utenti ignorati.", "Play": "Riproduci", - "Pause": "Pausa" + "Pause": "Pausa", + "This is an experimental feature. For now, new users receiving an invite will have to open the invite on to actually join.": "Questa è una funzione sperimentale. Per ora, i nuovi utenti che ricevono un invito dovranno aprirlo su per entrare.", + "To join %(spaceName)s, turn on the Spaces beta": "Per entrare in %(spaceName)s, attiva la beta degli spazi", + "To view %(spaceName)s, turn on the Spaces beta": "Per vedere %(spaceName)s, attiva la beta degli spazi", + "Select a room below first": "Prima seleziona una stanza sotto", + "Communities are changing to Spaces": "Le comunità stanno diventando spazi", + "Join the beta": "Unisciti alla beta", + "Leave the beta": "Abbandona la beta", + "Beta": "Beta", + "Tap for more info": "Tocca per maggiori info", + "Spaces is a beta feature": "Gli spazi sono una funzionalità beta", + "Want to add a new room instead?": "Vuoi invece aggiungere una nuova stanza?", + "Adding rooms... (%(progress)s out of %(count)s)|one": "Aggiunta stanza...", + "Adding rooms... (%(progress)s out of %(count)s)|other": "Aggiunta stanze... (%(progress)s di %(count)s)", + "Not all selected were added": "Non tutti i selezionati sono stati aggiunti", + "You can add existing spaces to a space.": "Puoi aggiungere spazi esistenti ad uno spazio.", + "Feeling experimental?": "Ti va di sperimentare?", + "You are not allowed to view this server's rooms list": "Non hai i permessi per vedere l'elenco di stanze del server", + "Error processing voice message": "Errore di elaborazione del vocale", + "We didn't find a microphone on your device. Please check your settings and try again.": "Non abbiamo trovato un microfono nel tuo dispositivo. Controlla le impostazioni e riprova.", + "No microphone found": "Nessun microfono trovato", + "We were unable to access your microphone. Please check your browser settings and try again.": "Non abbiamo potuto accedere al tuo microfono. Controlla le impostazioni del browser e riprova.", + "Unable to access your microphone": "Impossibile accedere al microfono", + "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.": "Ti va di sperimentare? I laboratori sono il miglior modo di ottenere anteprime, testare nuove funzioni ed aiutare a modellarle prima che vengano pubblicate. Maggiori informazioni.", + "Your access token gives full access to your account. Do not share it with anyone.": "Il tuo token di accesso ti dà l'accesso al tuo account. Non condividerlo con nessuno.", + "Access Token": "Token di accesso", + "Spaces are a new way to group rooms and people. To join an existing space you'll need an invite.": "Gli spazi sono un nuovo modo di raggruppare stanze e persone. Per entrare in uno spazio esistente ti serve un invito.", + "Please enter a name for the space": "Inserisci un nome per lo spazio", + "Connecting": "In connessione", + "Allow Peer-to-Peer for 1:1 calls (if you enable this, the other party might be able to see your IP address)": "Permetti Peer-to-Peer per chiamate 1:1 (se lo attivi, l'altra parte potrebbe essere in grado di vedere il tuo indirizzo IP)", + "Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.": "Beta disponibile per web, desktop e Android. Alcune funzioni potrebbero non essere disponibili nel tuo homeserver.", + "You can leave the beta any time from settings or tapping on a beta badge, like the one above.": "Puoi abbandonare la beta quando vuoi dalle impostazioni o toccando un'etichetta beta, come quella sopra.", + "%(brand)s will reload with Spaces enabled. Communities and custom tags will be hidden.": "%(brand)s si ricaricherà con gli spazi attivati. Le comunità e le etichette personalizzate saranno nascoste.", + "Beta available for web, desktop and Android. Thank you for trying the beta.": "Beta disponibile per web, desktop e Android. Grazie per la partecipazione alla beta.", + "%(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "%(brand)s si ricaricherà con gli spazi disattivati. Le comunità e le etichette personalizzate saranno di nuovo visibili.", + "Spaces are a new way to group rooms and people.": "Gli spazi sono un nuovo modo di raggruppare stanze e persone." } From c5093cafad1542c39d4124b573fb60e7af410ca6 Mon Sep 17 00:00:00 2001 From: XoseM Date: Thu, 13 May 2021 12:43:51 +0000 Subject: [PATCH 042/464] Translated using Weblate (Galician) Currently translated at 100.0% (2959 of 2959 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 | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/gl.json b/src/i18n/strings/gl.json index 451be0fb7c..15226882f2 100644 --- a/src/i18n/strings/gl.json +++ b/src/i18n/strings/gl.json @@ -3319,5 +3319,40 @@ "%(count)s results in all spaces|other": "%(count)s resultados en tódolos espazos", "You have no ignored users.": "Non tes usuarias ignoradas.", "Play": "Reproducir", - "Pause": "Deter" + "Pause": "Deter", + "This is an experimental feature. For now, new users receiving an invite will have to open the invite on to actually join.": "Esta é unha característica experimental. Por agora as novas usuarias convidadas deberán abrir o convite en para poder unirse.", + "To join %(spaceName)s, turn on the Spaces beta": "Para unirte a %(spaceName)s, activa a beta de Espazos", + "To view %(spaceName)s, turn on the Spaces beta": "Para ver %(spaceName)s, cambia á beta de Espazos", + "Select a room below first": "Primeiro elixe embaixo unha sala", + "Communities are changing to Spaces": "Comunidades cambia a Espazos", + "Join the beta": "Unirse á beta", + "Leave the beta": "Saír da beta", + "Beta": "Beta", + "Tap for more info": "Toca para ter máis información", + "Spaces is a beta feature": "Espazos é unha característica en beta", + "Want to add a new room instead?": "Queres engadir unha nova sala?", + "Adding rooms... (%(progress)s out of %(count)s)|one": "Engadindo sala...", + "Adding rooms... (%(progress)s out of %(count)s)|other": "Engadindo salas... (%(progress)s de %(count)s)", + "Not all selected were added": "Non se engadiron tódolos seleccionados", + "You can add existing spaces to a space.": "Podes engadir espazos existentes a un espazo.", + "Feeling experimental?": "Sínteste aventureira?", + "You are not allowed to view this server's rooms list": "Non tes permiso para ver a lista de salas deste servidor", + "Error processing voice message": "Erro ao procesar a mensaxe de voz", + "We didn't find a microphone on your device. Please check your settings and try again.": "Non atopamos ningún micrófono no teu dispositivo. Comproba os axustes e proba outra vez.", + "No microphone found": "Non atopamos ningún micrófono", + "We were unable to access your microphone. Please check your browser settings and try again.": "Non puidemos acceder ao teu micrófono. Comproba os axustes do navegador e proba outra vez.", + "Unable to access your microphone": "Non se puido acceder ao micrófono", + "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.": "Gañas de experimentar? Labs é o mellor xeito para un acceso temperá e probar novas funcións e axudar a melloralas antes de ser publicadas. Coñece máis.", + "Your access token gives full access to your account. Do not share it with anyone.": "O teu token de acceso da acceso completo á túa conta. Non o compartas con ninguén.", + "Access Token": "Token de acceso", + "Spaces are a new way to group rooms and people. To join an existing space you'll need an invite.": "Espazos é un novo xeito de agrupar salas e persoas. Precisas un convite para unirte a un espazo existente.", + "Please enter a name for the space": "Escribe un nome para o espazo", + "Connecting": "Conectando", + "Allow Peer-to-Peer for 1:1 calls (if you enable this, the other party might be able to see your IP address)": "Permitir Peer-to-Peer en chamadas 1:1 (se activas isto a outra parte podería coñecer o teu enderezo IP)", + "Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.": "Beta dispoñible para web, escritorio e Android. Algunhas características poderían non estar dispoñibles no teu servidor de inicio.", + "You can leave the beta any time from settings or tapping on a beta badge, like the one above.": "Podes saír da beta desde os axustes cando queiras ou tocando na insignia beta, como a superior.", + "%(brand)s will reload with Spaces enabled. Communities and custom tags will be hidden.": "%(brand)s cargará con Espazos activado. Comunidades e etiquetas personais estarán agochadas.", + "Beta available for web, desktop and Android. Thank you for trying the beta.": "Beta dispoñible para web, escritorio e Android. Grazas por probar a beta.", + "%(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "%(brand)s volverá a cargar con Espazos desactivado. Comunidade e etiquetas personalizadas estarán visibles de volta.", + "Spaces are a new way to group rooms and people.": "Espazos é un novo xeito de agrupar salas e persoas." } From 3f9ef015c73e0fd6b299fd545f9f8ca65f52a86b Mon Sep 17 00:00:00 2001 From: Trendyne Date: Thu, 13 May 2021 22:12:00 +0000 Subject: [PATCH 043/464] Translated using Weblate (Icelandic) Currently translated at 13.7% (407 of 2959 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 | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/is.json b/src/i18n/strings/is.json index ddb3bbc66d..90b8bc8d13 100644 --- a/src/i18n/strings/is.json +++ b/src/i18n/strings/is.json @@ -460,5 +460,11 @@ "Create Account": "Stofna Reikning", "Please install Chrome, Firefox, or Safari for the best experience.": "vinsamlegast setja upp Chrome, Firefox, eða Safari fyrir besta reynsluna.", "Explore rooms": "Kanna herbergi", - "Sign In": "Skrá inn" + "Sign In": "Skrá inn", + "The user's homeserver does not support the version of the room.": "Heimaþjónn notandans styður ekki útgáfu herbergis.", + "The user must be unbanned before they can be invited.": "Notandinn þarf að vera afbannaður áður en að hægt er að bjóða þeim.", + "User %(user_id)s may or may not exist": "Notandi %(user_id)s gæti verið til", + "User %(user_id)s does not exist": "Notandi %(user_id)s er ekki til", + "User %(userId)s is already in the room": "Notandi %(userId)s er nú þegar í herberginu", + "You do not have permission to invite people to this room.": "Þú hefur ekki heimild til að bjóða fólk í þessa spjallrás." } From d6a25d493abd23d24ba2225f500762cb36b12c17 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Fri, 14 May 2021 10:11:59 +0100 Subject: [PATCH 044/464] 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 045/464] 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 046/464] 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 047/464] 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 08d855c8b993cb8fee966c1975f467142b536b64 Mon Sep 17 00:00:00 2001 From: libexus Date: Fri, 14 May 2021 17:13:28 +0000 Subject: [PATCH 048/464] Translated using Weblate (German) Currently translated at 98.8% (2935 of 2968 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, 7 insertions(+), 4 deletions(-) diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index dbd2df28e8..5742517d6f 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -2488,7 +2488,7 @@ "Secret storage:": "Sicherer Speicher:", "ready": "bereit", "not ready": "nicht bereit", - "Secure Backup": "Sichere Aufbewahrungskopie", + "Secure Backup": "Sicheres Backup", "End Call": "Anruf beenden", "Remove the group call from the room?": "Konferenzgespräch aus diesem Raum entfernen?", "You don't have permission to remove the call from the room": "Du hast keine Berechtigung um den Konferenzanruf aus dem Raum zu entfernen", @@ -3318,9 +3318,12 @@ "Please enter a name for the space": "Gib den Namen des Spaces ein", "Connecting": "Verbinden", "Allow Peer-to-Peer for 1:1 calls (if you enable this, the other party might be able to see your IP address)": "Direktverbindung für Direktanrufe aktivieren. Dadurch sieht dein Gegenüber möglicherweise deine IP-Adresse.", - "Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.": "Die Betaversion ist verfügbar für Browser, Desktop und Android verfügbar. Je nach Homeserver sind einige Funktionen möglicherweise nicht verfügbar.", - "You can leave the beta any time from settings or tapping on a beta badge, like the one above.": "Du kannst die Betaversion jederzeit verlassen. Mache dies entweder in den Einstellungen oder klicke auf Beta-Icons wie dieses hier.", + "Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.": "Die Betaversion ist verfügbar für Browser, Desktop und Android. Je nach Homeserver sind einige Funktionen möglicherweise nicht verfügbar.", + "You can leave the beta any time from settings or tapping on a beta badge, like the one above.": "Du kannst die Betaversion jederzeit verlassen. Mache dies entweder in den Einstellungen oder klicke auf eines der \"Beta\"-Icons wie das hier oben.", "%(brand)s will reload with Spaces enabled. Communities and custom tags will be hidden.": "%(brand)s wird mit aktivierten Spaces neuladen. Danach kannst Communities und Custom Tags nicht verwenden.", "Beta available for web, desktop and Android. Thank you for trying the beta.": "Die Betaversion ist für Browser, Desktop und Android verfügbar. Danke, dass Du die Betaversion testest.", - "%(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "%(brand)s wird mit deaktivierten Spaces neuladen und du kannst Communities und Custom Tags wieder verwenden können." + "%(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "%(brand)s wird mit deaktivierten Spaces neuladen und du kannst Communities und Custom Tags wieder verwenden können.", + "Spaces are a beta feature.": "Spaces sind in der Beta.", + "Spaces are a new way to group rooms and people. To join an existing space you'll need an invite.": "Wir haben Spaces entwickelt, damit ihr eure vielen Räume besser organisieren könnt. Um einen existierenden Space beitreten zu können musst du (noch) von jemandem eingeladen werden.", + "Spaces are a new way to group rooms and people.": "Wir haben Spaces entwickelt, damit ihr eure vielen Räume besser organisieren könnt." } From 0642952d5f235afabe25a2fe2729766bf6f0942b Mon Sep 17 00:00:00 2001 From: Aaron Raimist Date: Fri, 14 May 2021 04:43:51 +0000 Subject: [PATCH 049/464] Translated using Weblate (German) Currently translated at 98.8% (2935 of 2968 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 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index 5742517d6f..7a16e994cf 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -3325,5 +3325,6 @@ "%(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "%(brand)s wird mit deaktivierten Spaces neuladen und du kannst Communities und Custom Tags wieder verwenden können.", "Spaces are a beta feature.": "Spaces sind in der Beta.", "Spaces are a new way to group rooms and people. To join an existing space you'll need an invite.": "Wir haben Spaces entwickelt, damit ihr eure vielen Räume besser organisieren könnt. Um einen existierenden Space beitreten zu können musst du (noch) von jemandem eingeladen werden.", - "Spaces are a new way to group rooms and people.": "Wir haben Spaces entwickelt, damit ihr eure vielen Räume besser organisieren könnt." + "Spaces are a new way to group rooms and people.": "Wir haben Spaces entwickelt, damit ihr eure vielen Räume besser organisieren könnt.", + "Message search initialisation failed": "Initialisierung der Nachrichtensuche fehlgeschlagen" } From 2d74ee722aad09e3b89ac47d9f016217f01d99be Mon Sep 17 00:00:00 2001 From: Aaron Raimist Date: Fri, 14 May 2021 04:41:12 +0000 Subject: [PATCH 050/464] Translated using Weblate (English (United States)) Currently translated at 19.7% (585 of 2968 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/en_US/ --- src/i18n/strings/en_US.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/en_US.json b/src/i18n/strings/en_US.json index 4a97b60a2e..a5d7756de8 100644 --- a/src/i18n/strings/en_US.json +++ b/src/i18n/strings/en_US.json @@ -665,5 +665,6 @@ "Unrecognised command: %(commandText)s": "Unrecognized command: %(commandText)s", "Add some details to help people recognise it.": "Add some details to help people recognize it.", "Unrecognised room address:": "Unrecognized room address:", - "A private space to organise your rooms": "A private space to organize your rooms" + "A private space to organise your rooms": "A private space to organize your rooms", + "Message search initialisation failed": "Message search initialization failed" } From 796306fa8fc00b3afd4b6eaba7497f6b344d928b Mon Sep 17 00:00:00 2001 From: Aaron Raimist Date: Fri, 14 May 2021 04:44:38 +0000 Subject: [PATCH 051/464] Translated using Weblate (Spanish) Currently translated at 99.4% (2953 of 2968 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 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/es.json b/src/i18n/strings/es.json index 86749e7b3f..ec5e4c1bfe 100644 --- a/src/i18n/strings/es.json +++ b/src/i18n/strings/es.json @@ -3291,5 +3291,6 @@ "%(brand)s will reload with Spaces enabled. Communities and custom tags will be hidden.": "%(brand)s se volverá a cargar con los espacios activados. Las comunidades y etiquetas personalizadas se ocultarán.", "Beta available for web, desktop and Android. Thank you for trying the beta.": "Versión beta disponible para web, escritorio y Android. Gracias por usar la beta.", "%(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "%(brand)s volverá a cargarse con los espacios desactivados. Las comunidades y etiquetas personalizadas serán visibles de nuevo.", - "Spaces are a new way to group rooms and people.": "Los espacios son una nueva manera de agrupar salas y gente." + "Spaces are a new way to group rooms and people.": "Los espacios son una nueva manera de agrupar salas y gente.", + "Message search initialisation failed": "Ha fallado la inicialización de la búsqueda de mensajes" } From d74ab2a4f9bea3d113626b4625c8d652440ac4d1 Mon Sep 17 00:00:00 2001 From: Aaron Raimist Date: Fri, 14 May 2021 04:48:00 +0000 Subject: [PATCH 052/464] Translated using Weblate (French) Currently translated at 99.5% (2956 of 2968 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 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index 4b36d5b7ed..1d03786f71 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -3331,5 +3331,6 @@ "%(brand)s will reload with Spaces enabled. Communities and custom tags will be hidden.": "%(brand)s va être redémarré avec les espaces activés. Les communautés et les étiquettes personnalisées seront cachés.", "Beta available for web, desktop and Android. Thank you for trying the beta.": "Bêta disponible pour l’application web, de bureau et Android. Merci d’essayer la bêta.", "%(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "%(brand)s va être redémarré avec les espaces désactivés. Les communautés et les étiquettes personnalisées seront de nouveau visibles.", - "Spaces are a new way to group rooms and people.": "Les espaces sont un nouveau moyen de regrouper les salons et les personnes." + "Spaces are a new way to group rooms and people.": "Les espaces sont un nouveau moyen de regrouper les salons et les personnes.", + "Message search initialisation failed": "Échec de l’initialisation de la recherche de message" } From 59144ac9352cccf6d86e54235ba5dafd73f70474 Mon Sep 17 00:00:00 2001 From: Szimszon Date: Fri, 14 May 2021 06:10:46 +0000 Subject: [PATCH 053/464] Translated using Weblate (Hungarian) Currently translated at 99.8% (2965 of 2968 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 | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json index 0230ecf52a..3daa0b2f16 100644 --- a/src/i18n/strings/hu.json +++ b/src/i18n/strings/hu.json @@ -3349,5 +3349,14 @@ "%(brand)s will reload with Spaces enabled. Communities and custom tags will be hidden.": "%(brand)s a Terekkel lesz újra betöltve. A közösségek és egyedi címkék rejtve maradnak.", "Beta available for web, desktop and Android. Thank you for trying the beta.": "Béta verzió elérhető webre, asztali kliensre és Androidra. Köszönjük, hogy kipróbálja.", "Spaces are a new way to group rooms and people.": "Szobák és emberek csoportosításának új lehetősége a Terek használata.", - "%(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "%(brand)s a Terek nélkül lesz újra betöltve. A közösségek és egyedi címkék újra megjelennek." + "%(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "%(brand)s a Terek nélkül lesz újra betöltve. A közösségek és egyedi címkék újra megjelennek.", + "To leave the beta, visit your settings.": "A beállításokban tudja elhagyni a bétát.", + "Your platform and username will be noted to help us use your feedback as much as we can.": "A platform és a felhasználói neve felhasználásra kerül ami segít nekünk a visszajelzést minél jobban felhasználni.", + "%(featureName)s beta feedback": "%(featureName)s béta visszajelzés", + "Thank you for your feedback, we really appreciate it.": "Köszönjük a visszajelzését, ezt nagyra értékeljük.", + "Beta feedback": "Béta visszajelzés", + "Add reaction": "Reakció hozzáadása", + "Send and receive voice messages": "Hangüzenet küldése, fogadása", + "Your feedback will help make spaces better. The more detail you can go into, the better.": "A visszajelzése segítség a terek javításához. Minél részletesebb annál jobb.", + "If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "Távozás után %(brand)s Terek nélkül lesz újra betöltve. A közösségek és egyedi címkék újra megjelennek." } From 6def74a418b1b54c93a1f387b5b088ff9389cc32 Mon Sep 17 00:00:00 2001 From: Aaron Raimist Date: Fri, 14 May 2021 04:49:36 +0000 Subject: [PATCH 054/464] Translated using Weblate (Hungarian) Currently translated at 99.8% (2965 of 2968 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 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json index 3daa0b2f16..bc38d20716 100644 --- a/src/i18n/strings/hu.json +++ b/src/i18n/strings/hu.json @@ -3358,5 +3358,6 @@ "Add reaction": "Reakció hozzáadása", "Send and receive voice messages": "Hangüzenet küldése, fogadása", "Your feedback will help make spaces better. The more detail you can go into, the better.": "A visszajelzése segítség a terek javításához. Minél részletesebb annál jobb.", - "If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "Távozás után %(brand)s Terek nélkül lesz újra betöltve. A közösségek és egyedi címkék újra megjelennek." + "If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "Távozás után %(brand)s Terek nélkül lesz újra betöltve. A közösségek és egyedi címkék újra megjelennek.", + "Message search initialisation failed": "Üzenet keresés beállítása sikertelen" } From 57d3c525e2c2fb2540de3a8955134be893c83c22 Mon Sep 17 00:00:00 2001 From: Aaron Raimist Date: Fri, 14 May 2021 04:51:02 +0000 Subject: [PATCH 055/464] Translated using Weblate (Dutch) Currently translated at 99.5% (2956 of 2968 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 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/nl.json b/src/i18n/strings/nl.json index e12780ec10..69a73ab76b 100644 --- a/src/i18n/strings/nl.json +++ b/src/i18n/strings/nl.json @@ -3240,5 +3240,6 @@ "%(brand)s will reload with Spaces enabled. Communities and custom tags will be hidden.": "%(brand)s zal herladen met Spaces ingeschakeld. Gemeenschappen en labels worden verborgen.", "Beta available for web, desktop and Android. Thank you for trying the beta.": "Beta beschikbaar voor web, desktop en Android. Bedankt dat u de beta wilt proberen.", "%(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "%(brand)s zal herladen met Spaces uitgeschakeld. Gemeenschappen en labels zullen weer zichtbaar worden.", - "Spaces are a new way to group rooms and people.": "Spaces zijn de nieuwe manier om gesprekken en personen te groeperen." + "Spaces are a new way to group rooms and people.": "Spaces zijn de nieuwe manier om gesprekken en personen te groeperen.", + "Message search initialisation failed": "Zoeken in berichten opstarten is mislukt" } From 6e47f968fab987061030e4ee92b3af3648cb5b0d Mon Sep 17 00:00:00 2001 From: Aaron Raimist Date: Fri, 14 May 2021 04:51:59 +0000 Subject: [PATCH 056/464] Translated using Weblate (Swedish) Currently translated at 98.4% (2922 of 2968 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 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/sv.json b/src/i18n/strings/sv.json index 5824224f3c..2ed4fa03d1 100644 --- a/src/i18n/strings/sv.json +++ b/src/i18n/strings/sv.json @@ -3250,5 +3250,6 @@ "%(count)s results in all spaces|other": "%(count)s resultat i alla utrymmen", "You have no ignored users.": "Du har inga ignorerade användare.", "Play": "Spela", - "Pause": "Pausa" + "Pause": "Pausa", + "Message search initialisation failed": "Initialisering av meddelandesökning misslyckades" } From fc3d56cdc34481f76c03990b59a432d3733bb868 Mon Sep 17 00:00:00 2001 From: Aaron Raimist Date: Fri, 14 May 2021 04:53:38 +0000 Subject: [PATCH 057/464] Translated using Weblate (Chinese (Traditional)) Currently translated at 99.5% (2956 of 2968 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 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json index 51ebb36dc0..a3854e59dd 100644 --- a/src/i18n/strings/zh_Hant.json +++ b/src/i18n/strings/zh_Hant.json @@ -3357,5 +3357,6 @@ "%(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.": "測試版可用於網路、桌面與 Android。感謝您試用測試版。", "%(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.": "空間是將聊天室與人們分組的一種新方式。" + "Spaces are a new way to group rooms and people.": "空間是將聊天室與人們分組的一種新方式。", + "Message search initialisation failed": "訊息搜尋初始化失敗" } From 00508faf9d0c046cbd7b23883b8a10c82e9932cd Mon Sep 17 00:00:00 2001 From: random Date: Fri, 14 May 2021 14:53:50 +0000 Subject: [PATCH 058/464] Translated using Weblate (Italian) Currently translated at 100.0% (2968 of 2968 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 | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/it.json b/src/i18n/strings/it.json index 00fce6dcb2..2d6a6ec3e9 100644 --- a/src/i18n/strings/it.json +++ b/src/i18n/strings/it.json @@ -3354,5 +3354,17 @@ "%(brand)s will reload with Spaces enabled. Communities and custom tags will be hidden.": "%(brand)s si ricaricherà con gli spazi attivati. Le comunità e le etichette personalizzate saranno nascoste.", "Beta available for web, desktop and Android. Thank you for trying the beta.": "Beta disponibile per web, desktop e Android. Grazie per la partecipazione alla beta.", "%(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "%(brand)s si ricaricherà con gli spazi disattivati. Le comunità e le etichette personalizzate saranno di nuovo visibili.", - "Spaces are a new way to group rooms and people.": "Gli spazi sono un nuovo modo di raggruppare stanze e persone." + "Spaces are a new way to group rooms and people.": "Gli spazi sono un nuovo modo di raggruppare stanze e persone.", + "Spaces are a beta feature.": "Gli spazi sono una funzionalità beta.", + "Search names and descriptions": "Cerca nomi e descrizioni", + "You may contact me if you have any follow up questions": "Potete contattarmi se avete altre domande", + "To leave the beta, visit your settings.": "Per abbandonare la beta, vai nelle impostazioni.", + "Your platform and username will be noted to help us use your feedback as much as we can.": "Verranno annotate la tua piattaforma e il nome utente per aiutarci ad usare la tua opinione al meglio.", + "%(featureName)s beta feedback": "Feedback %(featureName)s beta", + "Thank you for your feedback, we really appreciate it.": "Grazie per la tua opinione, lo appreziamo molto.", + "Beta feedback": "Feedback beta", + "Add reaction": "Aggiungi reazione", + "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." } From 1b67e680616f62b94f3bd291a0b4eb0e919b3395 Mon Sep 17 00:00:00 2001 From: Aaron Raimist Date: Fri, 14 May 2021 04:50:01 +0000 Subject: [PATCH 059/464] Translated using Weblate (Italian) Currently translated at 100.0% (2968 of 2968 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 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/it.json b/src/i18n/strings/it.json index 2d6a6ec3e9..d109837bb1 100644 --- a/src/i18n/strings/it.json +++ b/src/i18n/strings/it.json @@ -3366,5 +3366,6 @@ "Add reaction": "Aggiungi reazione", "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." + "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" } From d862df9f05b734fe7878b99c2af3875dded07bc6 Mon Sep 17 00:00:00 2001 From: Aaron Raimist Date: Fri, 14 May 2021 04:43:25 +0000 Subject: [PATCH 060/464] Translated using Weblate (Czech) Currently translated at 99.5% (2956 of 2968 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 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/cs.json b/src/i18n/strings/cs.json index 6d4cb953cc..9f3e48dd61 100644 --- a/src/i18n/strings/cs.json +++ b/src/i18n/strings/cs.json @@ -3271,5 +3271,6 @@ "%(brand)s will reload with Spaces enabled. Communities and custom tags will be hidden.": "%(brand)s se znovu načte s povolenými Prostory. Skupiny a vlastní značky budou skryty.", "Beta available for web, desktop and Android. Thank you for trying the beta.": "Beta verze je k dispozici pro web, desktop a Android. Děkujeme vám za vyzkoušení beta verze.", "%(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "%(brand)s se znovu načte s vypnutými Prostory. Skupiny a vlastní značky budou opět viditelné.", - "Spaces are a new way to group rooms and people.": "Prostory představují nový způsob seskupování místností a osob." + "Spaces are a new way to group rooms and people.": "Prostory představují nový způsob seskupování místností a osob.", + "Message search initialisation failed": "Inicializace vyhledávání zpráv se nezdařila" } From 126077a67817366a946be8a676f17d5331d0400d Mon Sep 17 00:00:00 2001 From: XoseM Date: Fri, 14 May 2021 12:42:01 +0000 Subject: [PATCH 061/464] Translated using Weblate (Galician) Currently translated at 100.0% (2968 of 2968 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 | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/gl.json b/src/i18n/strings/gl.json index 15226882f2..110f95c8b2 100644 --- a/src/i18n/strings/gl.json +++ b/src/i18n/strings/gl.json @@ -3354,5 +3354,17 @@ "%(brand)s will reload with Spaces enabled. Communities and custom tags will be hidden.": "%(brand)s cargará con Espazos activado. Comunidades e etiquetas personais estarán agochadas.", "Beta available for web, desktop and Android. Thank you for trying the beta.": "Beta dispoñible para web, escritorio e Android. Grazas por probar a beta.", "%(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "%(brand)s volverá a cargar con Espazos desactivado. Comunidade e etiquetas personalizadas estarán visibles de volta.", - "Spaces are a new way to group rooms and people.": "Espazos é un novo xeito de agrupar salas e persoas." + "Spaces are a new way to group rooms and people.": "Espazos é un novo xeito de agrupar salas e persoas.", + "Spaces are a beta feature.": "Espazos é unha ferramenta en beta.", + "Search names and descriptions": "Buscar nome e descricións", + "You may contact me if you have any follow up questions": "Podes contactar conmigo se tes algunha outra suxestión", + "To leave the beta, visit your settings.": "Para saír da beta, vai aos axustes.", + "Your platform and username will be noted to help us use your feedback as much as we can.": "A túa plataforma e nome de usuaria serán notificados para axudarnos a utilizar a túa opinión do mellor xeito posible.", + "%(featureName)s beta feedback": "Opinión acerca de %(featureName)s beta", + "Thank you for your feedback, we really appreciate it.": "Grazas pola túa opinión, realmente apreciámola.", + "Beta feedback": "Opinión sobre a beta", + "Add reaction": "Engadir reacción", + "Send and receive voice messages": "Enviar e recibir mensaxes de voz", + "Your feedback will help make spaces better. The more detail you can go into, the better.": "A túa opinión axudaranos a mellorar os espazos. Canto máis detallada sexa moito mellor para nós.", + "If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "Se saes, %(brand)s volverá a cargar con Espazos desactivados. Comunidades e etiquetas personais serán visibles outra vez." } From 9cc24657878ca58371ded2479234c2b048486dd6 Mon Sep 17 00:00:00 2001 From: Aaron Raimist Date: Fri, 14 May 2021 04:48:54 +0000 Subject: [PATCH 062/464] Translated using Weblate (Galician) Currently translated at 100.0% (2968 of 2968 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 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/gl.json b/src/i18n/strings/gl.json index 110f95c8b2..0de93753f7 100644 --- a/src/i18n/strings/gl.json +++ b/src/i18n/strings/gl.json @@ -3366,5 +3366,6 @@ "Add reaction": "Engadir reacción", "Send and receive voice messages": "Enviar e recibir mensaxes de voz", "Your feedback will help make spaces better. The more detail you can go into, the better.": "A túa opinión axudaranos a mellorar os espazos. Canto máis detallada sexa moito mellor para nós.", - "If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "Se saes, %(brand)s volverá a cargar con Espazos desactivados. Comunidades e etiquetas personais serán visibles outra vez." + "If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "Se saes, %(brand)s volverá a cargar con Espazos desactivados. Comunidades e etiquetas personais serán visibles outra vez.", + "Message search initialisation failed": "Fallo a inicialización da busca de mensaxes" } From 7e1f5625fa39b3af0099cdf095141e03967f1c33 Mon Sep 17 00:00:00 2001 From: Besnik Bleta Date: Fri, 14 May 2021 07:32:02 +0000 Subject: [PATCH 063/464] Translated using Weblate (Albanian) Currently translated at 99.7% (2961 of 2968 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 | 52 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/sq.json b/src/i18n/strings/sq.json index ab4d9862c8..505b98bcc8 100644 --- a/src/i18n/strings/sq.json +++ b/src/i18n/strings/sq.json @@ -3305,5 +3305,55 @@ "%(count)s results in all spaces|other": "%(count)s përfundime në krejt hapësirat", "You have no ignored users.": "S’keni përdorues të shpërfillur.", "Play": "Luaje", - "Pause": "Ndalesë" + "Pause": "Ndalesë", + "This is an experimental feature. For now, new users receiving an invite will have to open the invite on to actually join.": "Kjo është një veçori eksperimentale. Hëpërhë, përdoruesve të rinj që marrin një ftesë, do t’u duhet ta hapin ftesën në , që të marrin pjesë.", + "To join %(spaceName)s, turn on the Spaces beta": "Për të hyrë në %(spaceName)s, aktivizoni beta-n për Hapësira", + "To view %(spaceName)s, turn on the Spaces beta": "Për të parë %(spaceName)s, aktivizoni beta-n për Hapësira", + "Spaces are a beta feature.": "Hapësirat janë një veçori në version beta.", + "Search names and descriptions": "Kërko te emra dhe përshkrime", + "Select a room below first": "Së pari, përzgjidhni më poshtë një dhomë", + "Communities are changing to Spaces": "Bashkësitë po ndryshojnë në Hapësira", + "Join the beta": "Merrni pjesë te beta", + "Leave the beta": "Braktiseni beta-n", + "Beta": "Beta", + "Tap for more info": "Për më tepër hollësi, prekeni", + "Spaces is a beta feature": "Hapësirat janë një veçori në version beta", + "You may contact me if you have any follow up questions": "Mund të lidheni me mua, nëse keni pyetje të mëtejshme", + "To leave the beta, visit your settings.": "Që të braktisni beta-n, vizitoni rregullimet tuaja.", + "Your platform and username will be noted to help us use your feedback as much as we can.": "Platforma dhe emri juaj i përdoruesit do të mbahen shënim, për të na ndihmuar t’i përdorim përshtypjet tuaja sa më shumë që të mundemi.", + "%(featureName)s beta feedback": "Përshtypje për beta %(featureName)s", + "Thank you for your feedback, we really appreciate it.": "Faleminderit për përshtypjet tuaja, vërtet e çmojmë.", + "Beta feedback": "Përshtypje për versionin Beta", + "Want to add a new room instead?": "Doni të shtohet një dhomë e re, në vend të kësaj?", + "Adding rooms... (%(progress)s out of %(count)s)|one": "Po shtohet dhomë…", + "Adding rooms... (%(progress)s out of %(count)s)|other": "Po shtohen dhoma… (%(progress)s nga %(count)s)", + "Not all selected were added": "S’u shtuan të gjithë të përzgjedhurit", + "You can add existing spaces to a space.": "Mund të shtoni hapësira ekzistuese te një hapësirë.", + "Feeling experimental?": "Ndiheni eksperimentues?", + "You are not allowed to view this server's rooms list": "S’keni leje të shihni listën e dhomave të këtij shërbyesi", + "Zoom in": "Zmadhoje", + "Zoom out": "Zvogëloje", + "Add reaction": "Shtoni reagim", + "Error processing voice message": "Gabim në përpunimin e mesazhit zanor", + "Accept on your other login…": "Pranojeni te hyrja tjetër e juaja…", + "We didn't find a microphone on your device. Please check your settings and try again.": "S’gjetëm mikrofon në pajisjen tuaj. Ju lutemi, kontrolloni rregullimet tuaja dhe riprovoni.", + "No microphone found": "S’u gjet mikrofon", + "We were unable to access your microphone. Please check your browser settings and try again.": "S’qemë në gjendje të përdorim mikrofonin tuaj. Ju lutemi, kontrolloni rregullimet e shfletuesit tuaj dhe riprovoni.", + "Unable to access your microphone": "S’arrihet të përdoret mikrofoni juaj", + "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.": "Ndiheni eksperimentues? Laboratorët janë rruga më e mirë për t’u marrë herët me gjërat, për të provuar veçori të reja dhe për të ndihmuar t’u jepet formë atyre, përpara se të hidhen faktikisht në qarkullim. Mësoni më tepër.", + "Your access token gives full access to your account. Do not share it with anyone.": "Tokeni-i juaj i hyrjeve jep hyrje të plotë në llogarinë tuaj. Mos ia jepni kujt.", + "Access Token": "Token Hyrjesh", + "Spaces are a new way to group rooms and people. To join an existing space you'll need an invite.": "Hapësirat janë rrugë e re për të grupuar dhoma dhe njerëz. Për t’u bërë pjesë e një hapësire ekzistuese, do t’ju duhet një ftesë.", + "Please enter a name for the space": "Ju lutemi, jepni një emër për hapësirën", + "Connecting": "Po lidhet", + "Allow Peer-to-Peer for 1:1 calls (if you enable this, the other party might be able to see your IP address)": "Lejo Tek-për-Tek për thirrje 1:1 (nëse e aktivizoni këtë, pala tjetër mund të jetë në gjendje të shohë adresën tuaj IP)", + "New spinner design": "Rrotullues i ri", + "Send and receive voice messages": "Dërgoni dhe merrni mesazhe zanorë", + "Your feedback will help make spaces better. The more detail you can go into, the better.": "Përshtypjet tuaja do t’i bëjnë hapësirat më të mira. Sa më shumë hollësi që të jepni, aq më mirë.", + "Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.": "Beta e gatshme për web, desktop dhe Android. Disa veçori mund të mos jenë të përdorshme në shërbyesin tuaj Home.", + "You can leave the beta any time from settings or tapping on a beta badge, like the one above.": "Beta-n mund ta braktisni në çfarëdo kohe, që nga rregullimet, ose duke prekur një stemë beta, si ajo më sipër.", + "%(brand)s will reload with Spaces enabled. Communities and custom tags will be hidden.": "%(brand)s do të ringarkohet me Hapësirat të aktivizuara. Bashkësitë dhe etiketat vetjake do të jenë të fshehura.", + "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." } From bf5e07db88ed30f128c53ec34741aa2f5d3baba9 Mon Sep 17 00:00:00 2001 From: Aaron Raimist Date: Fri, 14 May 2021 04:51:36 +0000 Subject: [PATCH 064/464] Translated using Weblate (Albanian) Currently translated at 99.7% (2961 of 2968 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 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/sq.json b/src/i18n/strings/sq.json index 505b98bcc8..8935b5e348 100644 --- a/src/i18n/strings/sq.json +++ b/src/i18n/strings/sq.json @@ -3355,5 +3355,6 @@ "%(brand)s will reload with Spaces enabled. Communities and custom tags will be hidden.": "%(brand)s do të ringarkohet me Hapësirat të aktivizuara. Bashkësitë dhe etiketat vetjake do të jenë të fshehura.", "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." + "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" } From 779972ea51a3aec221c06f48a16258fc379f0704 Mon Sep 17 00:00:00 2001 From: Trendyne Date: Fri, 14 May 2021 15:25:39 +0000 Subject: [PATCH 065/464] Translated using Weblate (Icelandic) Currently translated at 16.2% (481 of 2968 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 | 77 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 76 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/is.json b/src/i18n/strings/is.json index 90b8bc8d13..ae337d56bf 100644 --- a/src/i18n/strings/is.json +++ b/src/i18n/strings/is.json @@ -466,5 +466,80 @@ "User %(user_id)s may or may not exist": "Notandi %(user_id)s gæti verið til", "User %(user_id)s does not exist": "Notandi %(user_id)s er ekki til", "User %(userId)s is already in the room": "Notandi %(userId)s er nú þegar í herberginu", - "You do not have permission to invite people to this room.": "Þú hefur ekki heimild til að bjóða fólk í þessa spjallrás." + "You do not have permission to invite people to this room.": "Þú hefur ekki heimild til að bjóða fólk í þessa spjallrás.", + "Leave Room": "Fara af Spjallrás", + "Add room": "Bæta við herbergi", + "Use a more compact ‘Modern’ layout": "Nota þéttara ‘nútímalegt’ skipulag", + "Switch to dark mode": "Skiptu yfir í dökkstillingu", + "Switch to light mode": "Skiptu yfir í ljósstillingu", + "Modify widgets": "Breyta viðmótshluta", + "Room Info": "Herbergis upplýsingar", + "Room information": "Upplýsingar um herbergi", + "Room options": "Herbergisvalkostir", + "Invite People": "Bjóða Fólki", + "Invite people": "Bjóða fólki", + "%(count)s people|other": "%(count)s manns", + "%(count)s people|one": "%(count)s manneskja", + "People": "Fólk", + "Finland": "Finnland", + "Norway": "Noreg", + "Denmark": "Danmörk", + "Iceland": "Ísland", + "Mentions & Keywords": "Nefnir og stikkorð", + "If you cancel now, you may lose encrypted messages & data if you lose access to your logins.": "Ef þú hættir við núna geturðu tapað dulkóðuðum skilaboðum og gögnum ef þú missir aðgang að innskráningum þínum.", + "Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.": "Heimaþjónastjórnandi þinn hefur lokað á sjálfkrafa dulkóðun í einkaherbergjum og beinskilaboðum.", + "You can’t disable this later. Bridges & most bots won’t work yet.": "Þú getur ekki gert þetta óvirkt síðar. Brýr og flest vélmenni virka ekki ennþá.", + "Travel & Places": "Ferðalög og staðir", + "Food & Drink": "Mat og drykkur", + "Animals & Nature": "Dýr og náttúra", + "Smileys & People": "Broskarlar og fólk", + "Voice & Video": "Rödd og myndband", + "Roles & Permissions": "Hlutverk og heimildir", + "Help & About": "Hjálp og um", + "Reject & Ignore user": "Hafna og hunsa notanda", + "Security & privacy": "Öryggi og einkalíf", + "Security & Privacy": "Öryggi & Einkalíf", + "Feedback sent": "Endurgjöf sent", + "Send feedback": "Senda endurgjöf", + "Feedback": "Endurgjöf", + "%(featureName)s beta feedback": "%(featureName)s beta endurgjöf", + "Thank you for your feedback, we really appreciate it.": "Þakka þér fyrir athugasemdir þínar.", + "Beta feedback": "Beta endurgjöf", + "All settings": "Allar stillingar", + "Notification settings": "Tilkynningarstillingar", + "Change notification settings": "Breytta tilkynningastillingum", + "You can't send any messages until you review and agree to our terms and conditions.": "Þú getur ekki sent nein skilaboð fyrr en þú hefur farið yfir og samþykkir skilmála okkar.", + "Send a Direct Message": "Senda beinskilaboð", + "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.": "Tilkynning um þessi skilaboð mun senda einstakt 'atburðarauðkenni' til stjórnanda heimaþjónns. Ef skilaboð í þessu herbergi eru dulkóðuð getur stjórnandi heimaþjónns ekki lesið skilaboðatextann eða skoðað skrár eða myndir.", + "Send a message…": "Senda skilaboð…", + "Send message": "Senda skilaboð", + "Sending your message...": "Er að senda skilaboð þitt...", + "Send as message": "Senda sem skilaboð", + "You can use /help to list available commands. Did you mean to send this as a message?": "Þú getur notað /help til að lista tilteknar skipanir. Ætlaðir þú að senda þetta sem skilaboð?", + "Send messages": "Senda skilaboð", + "Sends the given message with snowfall": "Sendir skilaboðið með snjókomu", + "Sends the given message with fireworks": "Sendir skilaboðið með flugeldum", + "Sends the given message with confetti": "Sendir skilaboðið með skrauti", + "Never send encrypted messages to unverified sessions in this room from this session": "Aldrei senda dulrituð skilaboð af þessu tæki til ósannvottaðra tækja í þessu herbergi", + "Never send encrypted messages to unverified sessions from this session": "Aldrei senda dulrituð skilaboð af þessu tæki til ósannvottaðra tækja", + "Use Ctrl + Enter to send a message": "Notaðu Ctrl + Enter til að senda skilaboð", + "Use Command + Enter to send a message": "Notaðu Command + Enter til að senda skilaboð", + "Jump to the bottom of the timeline when you send a message": "Hoppaðu neðst á tímalínunni þegar þú sendir skilaboð", + "Send and receive voice messages": "Senda og taka á móti talskilaboðum", + "%(senderName)s: %(message)s": "%(senderName)s: %(message)s", + "Send %(msgtype)s messages as you in your active room": "Senda %(msgtype)s skilaboð sem þú í virka herbergi þínu", + "Send %(msgtype)s messages as you in this room": "Senda %(msgtype)s skilaboð sem þú í þessu herbergi", + "Send text messages as you in your active room": "Senda texta skilaboð sem þú í virku herbergi þínu", + "Send text messages as you in this room": "Senda texta skilaboð sem þú í þessu herbergi", + "Send messages as you in your active room": "Senda skilaboð sem þú í virku herbergi þínu", + "Send messages as you in this room": "Senda skilaboð sem þú í þessu herbergi", + "%(senderName)s changed the pinned messages for the room.": "%(senderName)s breytti föstum skilaboðum fyrir herbergið.", + "Sends a message to the given user": "Sendir skilaboð til viðkomandi notanda", + "Sends the given message coloured as a rainbow": "Sendir gefið skilaboð litað sem regnbogi", + "Sends a message as html, without interpreting it as markdown": "Sendir skilaboð sem html, án þess að túlka það sem markdown", + "Sends a message as plain text, without interpreting it as markdown": "Sendir skilaboð sem óbreyttur texti án þess að túlka það sem markdown", + "Sends the given message as a spoiler": "Sendir skilaboðið sem spoiler", + "No need for symbols, digits, or uppercase letters": "Engin þörf á táknum, tölustöfum, eða hástöfum", + "Use a few words, avoid common phrases": "Notaðu nokkur orð. Forðastu algengar setningar", + "Unknown server error": "Óþekkt villa á þjóni" } From 159693fc4711f7a8bb969cef9a6b833a1f671418 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Fri, 14 May 2021 06:40:16 +0000 Subject: [PATCH 066/464] Translated using Weblate (Estonian) Currently translated at 100.0% (2968 of 2968 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 | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/et.json b/src/i18n/strings/et.json index 88480ceb6e..47d57e908a 100644 --- a/src/i18n/strings/et.json +++ b/src/i18n/strings/et.json @@ -3332,5 +3332,17 @@ "%(brand)s will reload with Spaces enabled. Communities and custom tags will be hidden.": "Käivitame %(brand)s uuesti nii, et kogukonnakeskused on kasutusel. Vana tüüpi kogukonnad ja kohandatud sildid on siis välja lülitatud.", "%(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "Käivitame %(brand)s uuesti nii, et kogukonnakeskused ei ole kasutusel. Vana tüüpi kogukonnad ja kohandatud sildid saavad jälle olema kasutusel.", "Beta available for web, desktop and Android. Thank you for trying the beta.": "Rakenduse beetaversioon on saadaval veebirakendusena, töölauarakendusena ja Androidi jaoks. Tänud, et oled huviline katsetama meie rakendust.", - "Spaces are a new way to group rooms and people.": "Kogukonnakeskused on uus viis jututubade ja inimeste ühendamiseks." + "Spaces are a new way to group rooms and people.": "Kogukonnakeskused on uus viis jututubade ja inimeste ühendamiseks.", + "Spaces are a beta feature.": "Kogukonnakeskused on veel katsetamisjärgus funktsionaalsus.", + "Search names and descriptions": "Otsi nimede ja kirjelduste seast", + "You may contact me if you have any follow up questions": "Kui sul on lisaküsimusi, siis vastan neile hea meelega", + "To leave the beta, visit your settings.": "Beetaversiooni saad välja lülitada rakenduse seadistustest.", + "Your platform and username will be noted to help us use your feedback as much as we can.": "Lisame sinu kommentaaridele ka kasutajanime ja operatsioonisüsteemi.", + "%(featureName)s beta feedback": "%(featureName)s testversiooni tagasiside", + "Thank you for your feedback, we really appreciate it.": "Täname sind nende kommentaaride eest.", + "Beta feedback": "Tagasiside testversioonile", + "Add reaction": "Lisa reaktsioon", + "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." } From b834ac8a080d6f31a6d2a40b9c78120a2fe5eee3 Mon Sep 17 00:00:00 2001 From: Aaron Raimist Date: Fri, 14 May 2021 04:45:22 +0000 Subject: [PATCH 067/464] Translated using Weblate (Estonian) Currently translated at 100.0% (2968 of 2968 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 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/et.json b/src/i18n/strings/et.json index 47d57e908a..5e8d744cca 100644 --- a/src/i18n/strings/et.json +++ b/src/i18n/strings/et.json @@ -3344,5 +3344,6 @@ "Add reaction": "Lisa reaktsioon", "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." + "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" } From 555b2e3266e542e20d02f7c81984d4af33534402 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Sun, 16 May 2021 08:00:54 -0400 Subject: [PATCH 068/464] Reuse Spinner styles for InlineSpinner Signed-off-by: Robin Townsend --- res/css/views/elements/_InlineSpinner.scss | 13 ------------- src/components/views/elements/InlineSpinner.js | 2 +- 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/res/css/views/elements/_InlineSpinner.scss b/res/css/views/elements/_InlineSpinner.scss index c850191b93..ca5cb5d3a8 100644 --- a/res/css/views/elements/_InlineSpinner.scss +++ b/res/css/views/elements/_InlineSpinner.scss @@ -23,19 +23,6 @@ limitations under the License. vertical-align: -3px; } -@keyframes spin { - from { - transform: rotateZ(0deg); - } - to { - transform: rotateZ(360deg); - } -} - .mx_InlineSpinner_icon { display: inline-block; - background-color: $primary-fg-color; - mask: url('$(res)/img/spinner.svg'); - mask-size: contain; - animation: 1.1s steps(12, end) infinite spin; } diff --git a/src/components/views/elements/InlineSpinner.js b/src/components/views/elements/InlineSpinner.js index feb0adb7a1..757e809564 100644 --- a/src/components/views/elements/InlineSpinner.js +++ b/src/components/views/elements/InlineSpinner.js @@ -40,7 +40,7 @@ export default class InlineSpinner extends React.Component { } else { icon = (
    From e798b36f1db8ea73c81fcc1626e1af31014c0800 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Sun, 16 May 2021 08:39:22 -0400 Subject: [PATCH 069/464] 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 070/464] 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 071/464] 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 072/464] 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 fa77bd91bb719ff7f51034281ed566b9a7da2458 Mon Sep 17 00:00:00 2001 From: libexus Date: Sat, 15 May 2021 09:22:28 +0000 Subject: [PATCH 073/464] Translated using Weblate (German) Currently translated at 98.9% (2937 of 2968 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 | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index 7a16e994cf..0113f1d07f 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -451,7 +451,7 @@ "Pinned Messages": "Angeheftete Nachrichten", "%(senderName)s changed the pinned messages for the room.": "%(senderName)s hat die angehefteten Nachrichten für diesen Raum geändert.", "Jump to read receipt": "Zur Lesebestätigung springen", - "Message Pinning": "Nachrichtenanheftung", + "Message Pinning": "Nachrichten anheften", "Long Description (HTML)": "Lange Beschreibung (HTML)", "Jump to message": "Zur Nachricht springen", "No pinned messages.": "Keine angehefteten Nachrichten vorhanden.", @@ -795,7 +795,7 @@ "We encountered an error trying to restore your previous session.": "Wir haben ein Problem beim Wiederherstellen deiner vorherigen Sitzung festgestellt.", "Clearing your browser's storage may fix the problem, but will sign you out and cause any encrypted chat history to become unreadable.": "Den Browser-Speicher zu löschen kann das Problem lösen, wird dich aber abmelden und verschlüsselte Chats unlesbar machen.", "Collapse Reply Thread": "Antwort-Thread zusammenklappen", - "Enable widget screenshots on supported widgets": "Bildschirmfotos bei unterstützten Widgets aktivieren", + "Enable widget screenshots on supported widgets": "Bildschirmfotos für unterstützte Widgets", "Send analytics data": "Analysedaten senden", "e.g. %(exampleValue)s": "z.B. %(exampleValue)s", "Muted Users": "Stummgeschaltete Benutzer", @@ -1336,7 +1336,7 @@ "Sends a message as plain text, without interpreting it as markdown": "Verschickt eine Nachricht in Rohtext, ohne sie als Markdown darzustellen", "Use an identity server to invite by email. Manage in Settings.": "Mit einem Identitätsserver kannst du über E-Mail Einladungen zu verschicken. Verwalte ihn in den Einstellungen.", "%(name)s (%(userId)s)": "%(name)s (%(userId)s)", - "Try out new ways to ignore people (experimental)": "Verwende neue Möglichkeiten, Menschen zu blockieren (experimentell)", + "Try out new ways to ignore people (experimental)": "Verwende neue Möglichkeiten, Menschen zu blockieren", "Send read receipts for messages (requires compatible homeserver to disable)": "Lesebestätigungen für Nachrichten senden (Deaktivieren erfordert einen kompatiblen Heimserver)", "My Ban List": "Meine Bannliste", "This is your list of users/servers you have blocked - don't leave the room!": "Dies ist die Liste von Benutzer und Servern, die du blockiert hast - verlasse diesen Raum nicht!", @@ -1368,7 +1368,7 @@ "%(num)s hours from now": "in %(num)s Stunden", "about a day from now": "in etwa einem Tag", "%(num)s days from now": "in %(num)s Tagen", - "Show info about bridges in room settings": "Information über Brücken in den Raumeinstellungen anzeigen", + "Show info about bridges in room settings": "Information über Brücken in Raumeinstellungen", "Enable message search in encrypted rooms": "Nachrichtensuche in verschlüsselten Räumen aktivieren", "Lock": "Schloss", "Later": "Später", @@ -1651,7 +1651,7 @@ "Not Trusted": "Nicht vertraut", "Manually Verify by Text": "Verifiziere manuell mit einem Text", "Interactively verify by Emoji": "Verifiziere interaktiv mit Emojis", - "Support adding custom themes": "Unterstütze das Hinzufügen von benutzerdefinierten Designs", + "Support adding custom themes": "Benutzerdefinierte Designs", "Ask this user to verify their session, or manually verify it below.": "Bitte diesen Nutzer, seine Sitzung zu verifizieren, oder verifiziere diese unten manuell.", "a few seconds from now": "in ein paar Sekunden", "Manually verify all remote sessions": "Remotesitzungen manuell verifizieren", @@ -1700,7 +1700,7 @@ "This room is end-to-end encrypted": "Dieser Raum ist Ende-zu-Ende verschlüsselt", "You are not subscribed to any lists": "Du hast keine Listen abonniert", "Error adding ignored user/server": "Fehler beim Blockieren eines Nutzers/Servers", - "None": "Keine", + "None": "Nichts", "Ban list rules - %(roomName)s": "Verbotslistenregeln - %(roomName)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.": "Füge hier die Benutzer und Server hinzu, die du blockieren willst. Verwende Sternchen, damit %(brand)s mit beliebigen Zeichen übereinstimmt. Bspw. würde @bot: * alle Benutzer blockieren, die auf einem Server den Namen 'bot' haben.", "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.": "Das Ignorieren von Personen erfolgt über Sperrlisten. Wenn eine Sperrliste abonniert wird, werden die von dieser Liste blockierten Benutzer und Server ausgeblendet.", @@ -1764,7 +1764,7 @@ "Backup has a valid signature from unverified session ": "Die Sicherung hat eine gültige Signatur von einer nicht verifizierten Sitzung ", "Backup has an invalid signature from verified session ": "Die Sicherung hat eine ungültige Signatur von einer verifizierten Sitzung ", "Backup has an invalid signature from unverified session ": "Die Sicherung hat eine ungültige Signatur von einer nicht verifizierten Sitzung ", - "Your keys are not being backed up from this session.": "Deine Schlüssel werden nicht von dieser Sitzung gesichert.", + "Your keys are not being backed up from this session.": "Deine Schlüssel werden von dieser Sitzung nicht gesichert.", "You are currently using to discover and be discoverable by existing contacts you know. You can change your identity server below.": "Zur Zeit verwendest du , um Kontakte zu finden und von anderen gefunden zu werden. Du kannst deinen Identitätsserver weiter unten ändern.", "Invalid theme schema.": "Ungültiges Designschema.", "Error downloading theme information.": "Fehler beim herunterladen des Themas.", @@ -2321,7 +2321,7 @@ "%(senderName)s invited %(targetName)s": "%(senderName)s hat %(targetName)s eingeladen", "You changed the room topic": "Du hast das Raumthema geändert", "%(senderName)s changed the room topic": "%(senderName)s hat das Raumthema geändert", - "New spinner design": "Neue Warteanimation", + "New spinner design": "Neue Ladeanimation", "Use a more compact ‘Modern’ layout": "Modernes kompaktes Layout", "Message deleted on %(date)s": "Nachricht am %(date)s gelöscht", "Wrong file type": "Falscher Dateityp", @@ -2337,7 +2337,7 @@ "Are you sure you want to cancel entering passphrase?": "Bist du sicher, dass du die Eingabe der Passphrase abbrechen möchtest?", "Use your account to sign in to the latest version": "Melde dich mit deinem Account in der neuesten Version an", "* %(senderName)s %(emote)s": "* %(senderName)s %(emote)s", - "Enable advanced debugging for the room list": "Erweiterte Fehlersuche für die Raumliste aktivieren", + "Enable advanced debugging for the room list": "Erweiterte Fehlersuche für die Raumliste", "Enable experimental, compact IRC style layout": "Kompaktes Layout im IRC-Stil (experimentell)", "User menu": "Benutzermenü", "%(brand)s Web": "%(brand)s Web", @@ -2345,10 +2345,10 @@ "%(brand)s iOS": "%(brand)s iOS", "%(brand)s X for Android": "%(brand)s X für Android", "We’re excited to announce Riot is now Element": "Wir freuen uns zu verkünden, dass Riot jetzt Element ist", - "%(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 kann verschlüsselte Nachrichten nicht sicher während der Ausführung im Browser durchsuchen. Benutze %(brand)s Desktop, um verschlüsselte Nachrichten in den Suchergebnissen angezeigt zu bekommen.", + "%(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.": "Das Durchsuchen von verschlüsselten Nachrichten wird aus Sicherheitsgründen nur von %(brand)s Desktop unterstützt. Hier geht's zum Download.", "Show rooms with unread messages first": "Räume mit ungelesenen Nachrichten zuerst zeigen", "Show previews of messages": "Nachrichtenvorschau anzeigen", - "Use default": "Standardeinstellungen benutzen", + "Use default": "Standardeinstellungen", "Mentions & Keywords": "Erwähnungen und Schlüsselwörter", "Notification options": "Benachrichtigungsoptionen", "Forget Room": "Raum vergessen", @@ -2916,7 +2916,7 @@ "United Kingdom": "Großbritannien", "We call the places you where you can host your account ‘homeservers’.": "Orte, an denen du dein Benutzerkonto hosten kannst, nennen wir \"Homeserver\".", "Specify a homeserver": "Gib einen Homeserver an", - "Render LaTeX maths in messages": "LaTeX-Matheformeln in Nachrichten anzeigen", + "Render LaTeX maths in messages": "LaTeX-Matheformeln", "Decide where your account is hosted": "Gib an wo dein Benutzerkonto gehostet werden soll", "Already have an account? Sign in here": "Hast du schon ein Benutzerkonto? Melde dich hier an", "%(ssoButtons)s Or %(usernamePassword)s": "%(ssoButtons)s oder %(usernamePassword)s", @@ -3314,7 +3314,7 @@ "No microphone found": "Kein Mikrofon gefunden", "We were unable to access your microphone. Please check your browser settings and try again.": "Fehler beim Zugriff auf dein Mikrofon. Überprüfe deine Browsereinstellungen und versuche es nochmal.", "Unable to access your microphone": "Fehler beim Zugriff auf Mikrofon", - "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.": "Lust auf ein paar Experimente? In den Laboreinstellungen kannst du zukünftige Features vor der Veröffentlichung testen und uns mit Feedback beim Verbessern helfen. Mehr Infos.", + "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.": "Hier kannst du zukünftige Features noch vor der Veröffentlichung testen und uns mit Feedback beim Verbessern helfen. Mehr Infos.", "Please enter a name for the space": "Gib den Namen des Spaces ein", "Connecting": "Verbinden", "Allow Peer-to-Peer for 1:1 calls (if you enable this, the other party might be able to see your IP address)": "Direktverbindung für Direktanrufe aktivieren. Dadurch sieht dein Gegenüber möglicherweise deine IP-Adresse.", @@ -3326,5 +3326,6 @@ "Spaces are a beta feature.": "Spaces sind in der Beta.", "Spaces are a new way to group rooms and people. To join an existing space you'll need an invite.": "Wir haben Spaces entwickelt, damit ihr eure vielen Räume besser organisieren könnt. Um einen existierenden Space beitreten zu können musst du (noch) von jemandem eingeladen werden.", "Spaces are a new way to group rooms and people.": "Wir haben Spaces entwickelt, damit ihr eure vielen Räume besser organisieren könnt.", - "Message search initialisation failed": "Initialisierung der Nachrichtensuche fehlgeschlagen" + "Message search initialisation failed": "Initialisierung der Nachrichtensuche fehlgeschlagen", + "Send and receive voice messages": "Sprachnachrichten" } From ba5a769a609822b1bdb5491f80e90626b070dc16 Mon Sep 17 00:00:00 2001 From: iaiz Date: Mon, 17 May 2021 11:13:39 +0000 Subject: [PATCH 074/464] Translated using Weblate (Spanish) Currently translated at 99.9% (2966 of 2968 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 | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/es.json b/src/i18n/strings/es.json index ec5e4c1bfe..0e4d50210a 100644 --- a/src/i18n/strings/es.json +++ b/src/i18n/strings/es.json @@ -2102,7 +2102,7 @@ "Communities v2 prototypes. Requires compatible homeserver. Highly experimental - use with caution.": "Prototipo de comunidades v2. Requiere un servidor compatible. Altamente experimental - usar con precuación.", "Font size": "Tamaño del texto", "Use custom size": "Usar un tamaño personalizado", - "Use a more compact ‘Modern’ layout": "Usar un diseño más «moderno y compacto", + "Use a more compact ‘Modern’ layout": "Usar un diseño más «moderno y compacto»", "Use a system font": "Usar una fuente del sistema", "System font name": "Nombre de la fuente", "Enable experimental, compact IRC style layout": "Activar el diseño experimental de IRC compacto", @@ -3292,5 +3292,18 @@ "Beta available for web, desktop and Android. Thank you for trying the beta.": "Versión beta disponible para web, escritorio y Android. Gracias por usar la beta.", "%(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "%(brand)s volverá a cargarse con los espacios desactivados. Las comunidades y etiquetas personalizadas serán visibles de nuevo.", "Spaces are a new way to group rooms and people.": "Los espacios son una nueva manera de agrupar salas y gente.", - "Message search initialisation failed": "Ha fallado la inicialización de la búsqueda de mensajes" + "Message search initialisation failed": "Ha fallado la inicialización de la búsqueda de mensajes", + "Spaces are a beta feature.": "Los espacios son una funcionalidad en beta.", + "Search names and descriptions": "Buscar por nombre y descripción", + "You may contact me if you have any follow up questions": "Os podéis poner en contacto conmigo si tenéis alguna pregunta", + "To leave the beta, visit your settings.": "Para salir de la beta, ve a tus ajustes.", + "Your platform and username will be noted to help us use your feedback as much as we can.": "Tu nombre de usuario y plataforma serán adjuntados, para que podamos interpretar tus comentarios lo mejor posible.", + "%(featureName)s beta feedback": "Comentarios sobre la funcionalidad beta %(featureName)s", + "Thank you for your feedback, we really appreciate it.": "Muchas gracias por tus comentarios.", + "Beta feedback": "Danos tu opinión sobre la beta", + "Add reaction": "Reaccionar", + "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.": "¿Te apetece probar cosas nuevas? Los experimentos son la mejor manera de conseguir acceso anticipado a nuevas funcionalidades, probarlas y ayudar a mejorarlas antes de su lanzamiento. Más información.", + "Send and receive voice messages": "Enviar y recibir mensajes de voz", + "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." } From 9d538614c4aaa4feca5635666a6ecf727b860e9d Mon Sep 17 00:00:00 2001 From: Thomas Citharel Date: Mon, 17 May 2021 14:15:40 +0000 Subject: [PATCH 075/464] Translated using Weblate (French) Currently translated at 100.0% (2968 of 2968 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 | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index 1d03786f71..b4bf9de59f 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -3332,5 +3332,10 @@ "Beta available for web, desktop and Android. Thank you for trying the beta.": "Bêta disponible pour l’application web, de bureau et Android. Merci d’essayer la bêta.", "%(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "%(brand)s va être redémarré avec les espaces désactivés. Les communautés et les étiquettes personnalisées seront de nouveau visibles.", "Spaces are a new way to group rooms and people.": "Les espaces sont un nouveau moyen de regrouper les salons et les personnes.", - "Message search initialisation failed": "Échec de l’initialisation de la recherche de message" + "Message search initialisation failed": "Échec de l’initialisation de la recherche de message", + "Your feedback will help make spaces better. The more detail you can go into, the better.": "Vos commentaires aideront à rendre les espaces meilleurs. N'hésitez pas à entrer dans les détails.", + "%(featureName)s beta feedback": "Commentaires sur la bêta de %(featureName)s", + "Thank you for your feedback, we really appreciate it.": "Merci pour vos commentaires, nous en sommes vraiment reconnaissants.", + "Beta feedback": "Commentaires sur la bêta", + "If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "Si vous quittez, %(brand)s sera rechargé avec les espaces désactivés. Les communautés et les étiquettes personnalisées seront à nouveau visibles." } From b48be43a2fa82d67608fb8539b23849b5216640c Mon Sep 17 00:00:00 2001 From: Thibault Martin Date: Sat, 15 May 2021 07:59:35 +0000 Subject: [PATCH 076/464] Translated using Weblate (French) Currently translated at 100.0% (2968 of 2968 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 | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index b4bf9de59f..36446ebba3 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -3337,5 +3337,12 @@ "%(featureName)s beta feedback": "Commentaires sur la bêta de %(featureName)s", "Thank you for your feedback, we really appreciate it.": "Merci pour vos commentaires, nous en sommes vraiment reconnaissants.", "Beta feedback": "Commentaires sur la bêta", - "If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "Si vous quittez, %(brand)s sera rechargé avec les espaces désactivés. Les communautés et les étiquettes personnalisées seront à nouveau visibles." + "If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "Si vous quittez, %(brand)s sera rechargé avec les espaces désactivés. Les communautés et les étiquettes personnalisées seront à nouveau visibles.", + "Spaces are a beta feature.": "Les espaces sont une fonctionnalité en bêta.", + "Search names and descriptions": "Rechercher par nom et description", + "You may contact me if you have any follow up questions": "Vous pouvez me contacter si vous avez des questions par la suite", + "To leave the beta, visit your settings.": "Pour quitter la bêta, consultez les paramètres.", + "Your platform and username will be noted to help us use your feedback as much as we can.": "Votre plateforme et nom d’utilisateur seront consignés pour nous aider à tirer le maximum de vos retours.", + "Add reaction": "Ajouter une réaction", + "Send and receive voice messages": "Envoyer et recevoir des messages vocaux" } From 5e0db72582510616aefbd307ee9498d4bcd1d971 Mon Sep 17 00:00:00 2001 From: jelv Date: Sun, 16 May 2021 06:41:15 +0000 Subject: [PATCH 077/464] Translated using Weblate (Dutch) Currently translated at 100.0% (2968 of 2968 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 | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/nl.json b/src/i18n/strings/nl.json index 69a73ab76b..94da2fccc1 100644 --- a/src/i18n/strings/nl.json +++ b/src/i18n/strings/nl.json @@ -3241,5 +3241,17 @@ "Beta available for web, desktop and Android. Thank you for trying the beta.": "Beta beschikbaar voor web, desktop en Android. Bedankt dat u de beta wilt proberen.", "%(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "%(brand)s zal herladen met Spaces uitgeschakeld. Gemeenschappen en labels zullen weer zichtbaar worden.", "Spaces are a new way to group rooms and people.": "Spaces zijn de nieuwe manier om gesprekken en personen te groeperen.", - "Message search initialisation failed": "Zoeken in berichten opstarten is mislukt" + "Message search initialisation failed": "Zoeken in berichten opstarten is mislukt", + "Spaces are a beta feature.": "Spaces zijn een beta functie.", + "Search names and descriptions": "Namen en beschrijvingen zoeken", + "You may contact me if you have any follow up questions": "U mag contact met mij opnemen als u nog vervolg vragen heeft", + "To leave the beta, visit your settings.": "Om de beta te verlaten, ga naar uw instellingen.", + "Your platform and username will be noted to help us use your feedback as much as we can.": "Uw platform en gebruikersnaam zullen worden opgeslagen om onze te helpen uw feedback zo goed mogelijk te gebruiken.", + "%(featureName)s beta feedback": "%(featureName)s beta feedback", + "Thank you for your feedback, we really appreciate it.": "Bedankt voor uw feedback, we waarderen het enorm.", + "Beta feedback": "Beta feedback", + "Add reaction": "Reactie toevoegen", + "Send and receive voice messages": "Stuur en ontvang spraakberichten", + "Your feedback will help make spaces better. The more detail you can go into, the better.": "Uw feedback maakt spaces beter. Hoe meer details u kan geven, des te beter.", + "If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "Als u de pagina nu verlaat zal %(brand)s herladen met Spaces uitgeschakeld. Gemeenschappen en labels zullen weer zichtbaar worden." } From fc4ee0df4b53cb2ca406557de23d8eec8b86e4e5 Mon Sep 17 00:00:00 2001 From: LinAGKar Date: Mon, 17 May 2021 19:49:21 +0000 Subject: [PATCH 078/464] Translated using Weblate (Swedish) Currently translated at 99.9% (2966 of 2968 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 | 46 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/sv.json b/src/i18n/strings/sv.json index 2ed4fa03d1..74e7a30032 100644 --- a/src/i18n/strings/sv.json +++ b/src/i18n/strings/sv.json @@ -3251,5 +3251,49 @@ "You have no ignored users.": "Du har inga ignorerade användare.", "Play": "Spela", "Pause": "Pausa", - "Message search initialisation failed": "Initialisering av meddelandesökning misslyckades" + "Message search initialisation failed": "Initialisering av meddelandesökning misslyckades", + "To view %(spaceName)s, turn on the Spaces beta": "För att se %(spaceName)s, aktivera utrymmesbetan", + "Spaces are a beta feature.": "Utrymmen är en betafunktion.", + "Search names and descriptions": "Sök namn och beskrivningar", + "Select a room below first": "Välj ett rum nedan först", + "Communities are changing to Spaces": "Gemenskaper byts ut mot utrymmen", + "Join the beta": "Gå med i betan", + "Leave the beta": "Lämna betan", + "Beta": "Beta", + "Tap for more info": "Klicka för mer info", + "Spaces is a beta feature": "Utrymmen är en betafunktion", + "You may contact me if you have any follow up questions": "Ni kan kontakta mig om ni har vidare frågor", + "To leave the beta, visit your settings.": "För att lämna betan, besök dina inställningar.", + "Your platform and username will be noted to help us use your feedback as much as we can.": "Din plattform och ditt användarnamn kommer att noteras för att hjälpa oss att använda din återkoppling så mycket vi kan.", + "%(featureName)s beta feedback": "%(featureName)s betaåterkoppling", + "Thank you for your feedback, we really appreciate it.": "Tack för din återkoppling, vi uppskattar det verkligen.", + "Beta feedback": "Betaåterkoppling", + "Want to add a new room instead?": "Vill du lägga till ett nytt rum istället?", + "Adding rooms... (%(progress)s out of %(count)s)|one": "Lägger till rum…", + "Adding rooms... (%(progress)s out of %(count)s)|other": "Lägger till rum… (%(progress)s av %(count)s)", + "Not all selected were added": "Inte alla valda tillades", + "You can add existing spaces to a space.": "Du kan lägga till existerande utrymmen till ett utrymme.", + "Feeling experimental?": "Känner du dig äventyrlig?", + "You are not allowed to view this server's rooms list": "Du tillåts inte att se den här serverns rumslista", + "Add reaction": "Lägg till reaktion", + "Error processing voice message": "Fel vid hantering av röstmeddelande", + "We didn't find a microphone on your device. Please check your settings and try again.": "Vi kunde inte hitta en mikrofon på din enhet. Vänligen kolla dina inställningar och försök igen.", + "No microphone found": "Ingen mikrofon hittad", + "We were unable to access your microphone. Please check your browser settings and try again.": "Vi kunde inte komma åt din mikrofon. Vänligen kolla dina webbläsarinställningar och försök igen.", + "Unable to access your microphone": "Kan inte komma åt din mikrofon", + "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.": "Känner du dig äventyrlig? Experiment är det bästa sättet att få saker tidigt, testa nya funktioner och hjälpa till att forma dem innan de egentligen släpps Läs mer.", + "Your access token gives full access to your account. Do not share it with anyone.": "Din åtkomsttoken ger full åtkomst till ditt konto. Dela den inte med någon.", + "Access Token": "Åtkomsttoken", + "Spaces are a new way to group rooms and people. To join an existing space you'll need an invite.": "Utrymmen är ett nytt sätt att gruppera rum och personer. För att gå med i existerande utrymme så behöver du en inbjudan.", + "Please enter a name for the space": "Vänligen ange ett namn för utrymmet", + "Connecting": "Ansluter", + "Allow Peer-to-Peer for 1:1 calls (if you enable this, the other party might be able to see your IP address)": "Tillåt peer-to-peer för 1:1-samtal (om du aktiverar det hör så kan den andra parten kanske se din IP-adress)", + "Send and receive voice messages": "Skicka och ta emot röstmeddelanden", + "Your feedback will help make spaces better. The more detail you can go into, the better.": "Din återkoppling kommer att hjälpa till att göra utrymmen bättre. Ju fler detaljer du kan ge desto bättre.", + "Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.": "Beta tillgänglig för webben, skrivbord och Android. Vissa funktioner kan vara otillgängliga på din hemserver.", + "You can leave the beta any time from settings or tapping on a beta badge, like the one above.": "Du kan lämna betan när som helst från inställningarna eller genom att trycka en betabricka, som den ovan.", + "%(brand)s will reload with Spaces enabled. Communities and custom tags will be hidden.": "%(brand)s kommer att ladda om med utrymmen aktiverade. Gemenskaper och anpassade taggar kommer att döljas.", + "Beta available for web, desktop and Android. Thank you for trying the beta.": "Beta tillgänglig för webben, skrivbord och Android. Tack för att du provar betan.", + "If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "Om du lämnar så kommer %(brand)s att ladda om med utrymmen inaktiverade. Gemenskaper och anpassade taggar kommer att synas igen.", + "Spaces are a new way to group rooms and people.": "Utrymmen är nya sätt att gruppera rum och personer." } From 0aa666693f0bad9be0b735d4271deeed564ac1ad Mon Sep 17 00:00:00 2001 From: Jeff Huang Date: Sun, 16 May 2021 01:02:41 +0000 Subject: [PATCH 079/464] Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (2968 of 2968 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 | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json index a3854e59dd..1ab5a59911 100644 --- a/src/i18n/strings/zh_Hant.json +++ b/src/i18n/strings/zh_Hant.json @@ -3358,5 +3358,17 @@ "Beta available for web, desktop and Android. Thank you for trying the beta.": "測試版可用於網路、桌面與 Android。感謝您試用測試版。", "%(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.": "空間是將聊天室與人們分組的一種新方式。", - "Message search initialisation failed": "訊息搜尋初始化失敗" + "Message search initialisation failed": "訊息搜尋初始化失敗", + "Spaces are a beta feature.": "空間為測試版功能。", + "Search names and descriptions": "搜尋名稱與描述", + "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": "測試版回饋", + "Add reaction": "新增反應", + "Send and receive voice messages": "傳送與接收語音訊息", + "Your feedback will help make spaces better. The more detail you can go into, the better.": "您的回饋意見將會讓空間變得更好。您可以輸入愈多細節愈好。", + "If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "若您離開,%(brand)s 將在停用空間的情況下重新載入。社群與自訂標籤將再次可見。" } From 3c526be5de94d3ec08682b1383f777887181b422 Mon Sep 17 00:00:00 2001 From: Andrejs Date: Sat, 15 May 2021 07:11:43 +0000 Subject: [PATCH 080/464] Translated using Weblate (Latvian) Currently translated at 49.5% (1470 of 2968 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/lv/ --- src/i18n/strings/lv.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/lv.json b/src/i18n/strings/lv.json index ed7da7dc6b..b56599f26e 100644 --- a/src/i18n/strings/lv.json +++ b/src/i18n/strings/lv.json @@ -1581,5 +1581,6 @@ "Failed to set topic": "Neizdevās iestatīt tematu", "Upload files": "Failu augšupielāde", "These files are too large to upload. The file size limit is %(limit)s.": "Šie faili pārsniedz augšupielādes izmēra limitu %(limit)s.", - "Upload files (%(current)s of %(total)s)": "Failu augšupielāde (%(current)s no %(total)s)" + "Upload files (%(current)s of %(total)s)": "Failu augšupielāde (%(current)s no %(total)s)", + "Check your devices": "Pārskatiet savas ierīces" } From af5bc71602308a6aa4d05ae5cf6838756524bbf1 Mon Sep 17 00:00:00 2001 From: Kaede Date: Mon, 17 May 2021 15:55:19 +0000 Subject: [PATCH 081/464] Translated using Weblate (Japanese) Currently translated at 77.4% (2300 of 2968 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 | 37 +++++++++++++++++++++++++++++++++---- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/src/i18n/strings/ja.json b/src/i18n/strings/ja.json index 83d8961147..4eb49e45e2 100644 --- a/src/i18n/strings/ja.json +++ b/src/i18n/strings/ja.json @@ -629,7 +629,7 @@ "ex. @bob:example.com": "例 @bob:example.com", "Add User": "ユーザーを追加", "Matrix ID": "Matirx ID", - "Matrix Room ID": "Matrix 部屋ID", + "Matrix Room ID": "Matrix 部屋 ID", "email address": "メールアドレス", "You have entered an invalid address.": "無効なアドレスを入力しました。", "Try using one of the following valid address types: %(validTypesList)s.": "次の有効なアドレスタイプのいずれかを使用してください:%(validTypesList)s", @@ -1212,7 +1212,7 @@ "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!": "警告: 鍵の検証に失敗しました!提供された鍵「%(fingerprint)s」は、%(userId)s およびセッション %(deviceId)s の署名鍵「%(fprint)s」と一致しません。これはつまり、あなたの会話が傍受・盗聴されようとしている恐れがあるということです!", "Show typing notifications": "入力中通知を表示する", - "Allow fallback call assist server turn.matrix.org when your homeserver does not offer one (your IP address would be shared during a call)": "あなたのホームサーバーが対応していない場合は (通話中に自己の IP アドレスが相手に共有されるのを防ぐために) 代替通話支援サーバー turn.matrix.org の使用を許可する", + "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 アドレスが通話相手に漏洩するのを防ぎます)", "Your homeserver does not support cross-signing.": "あなたのホームサーバーはクロス署名に対応していません。", "Cross-signing and secret storage are enabled.": "クロス署名および機密ストレージは有効です。", "Reset cross-signing and secret storage": "クロス署名および機密ストレージをリセット", @@ -2440,7 +2440,7 @@ "Create a space": "スペースを作成する", "Delete": "削除", "Jump to the bottom of the timeline when you send a message": "メッセージを送信する際にタイムライン最下部に移動します", - "Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags. Requires compatible homeserver for some features.": "Spacesはプロトタイプです。 コミュニティ、コミュニティv2、カスタムタグとは互換性がありません。 一部の機能には互換性のあるホームサーバーが必要です。", + "Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags. Requires compatible homeserver for some features.": "スペースはプロトタイプです。 コミュニティ、コミュニティv2、カスタムタグとは互換性がありません。 一部の機能には互換性のあるホームサーバーが必要です。", "This homeserver has been blocked by it's administrator.": "このホームサーバーは管理者によりブロックされています。", "This homeserver has been blocked by its administrator.": "このホームサーバーは管理者によりブロックされています。", "You're already in a call with this person.": "あなたは既にこの人と通話中です。", @@ -2448,5 +2448,34 @@ "Invite People": "ユーザーを招待", "Edit devices": "デバイスを編集", "%(count)s messages deleted.|one": "%(count)s 件のメッセージが削除されました。", - "%(count)s messages deleted.|other": "%(count)s 件のメッセージが削除されました。" + "%(count)s messages deleted.|other": "%(count)s 件のメッセージが削除されました。", + "To view %(spaceName)s, turn on the Spaces beta": "スペース Beta を有効にすると %(spaceName)s を表示できます", + "Allow Peer-to-Peer for 1:1 calls (if you enable this, the other party might be able to see your IP address)": "1 対 1 の通話で P2P の使用を許可 (有効にするとあなたの IP アドレスが通話相手に漏洩する可能性があります)", + "You have no ignored users.": "無視しているユーザーはいません。", + "Join the beta": "Beta に参加", + "Leave the beta": "Beta を終了", + "Beta available for web, desktop and Android. Thank you for trying the beta.": "Beta は、ウェブ、デスクトップ、Android で利用可能です。Beta をお試しいただきありがとうございます。", + "Your access token gives full access to your account. Do not share it with anyone.": "アクセストークンを用いるとあなたのアカウントの全てにアクセスできます。外部に公開しないでください。", + "Access Token": "アクセストークン", + "Filter all spaces": "全スペースを検索", + "Save Changes": "変更を保存", + "Edit settings relating to your space.": "スペースの設定を変更します。", + "Space settings": "スペースの設定", + "Spaces are a beta feature.": "スペースは Beta 機能です。", + "Spaces is a beta feature": "スペースは Beta 機能です", + "Spaces are a new way to group rooms and people.": "スペースは、部屋や人をグループ化する新しい方法です。", + "Spaces": "スペース", + "Welcome to ": "ようこそ ", + "Invite to just this room": "この部屋に招待", + "Invite to %(spaceName)s": "%(spaceName)s に招待", + "Quick actions": "クイックアクション", + "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 にアクセスできるようにします", + "Who are you working with?": "誰が使いますか?", + "Beta": "Beta", + "Tap for more info": "タップして詳細を表示", + "Spaces are a new way to group rooms and people. To join an existing space you'll need an invite.": "スペースは、部屋や人をグループ化する新しい方法です。既存のスペースに参加するには、招待が必要です。", + "Check your devices": "デバイスを確認" } From 0e2ddec7a31c2e5fb827eaa0540f5158fd422699 Mon Sep 17 00:00:00 2001 From: waclaw66 Date: Mon, 17 May 2021 07:22:08 +0000 Subject: [PATCH 082/464] Translated using Weblate (Czech) Currently translated at 100.0% (2968 of 2968 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 | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/cs.json b/src/i18n/strings/cs.json index 9f3e48dd61..9701afa0e5 100644 --- a/src/i18n/strings/cs.json +++ b/src/i18n/strings/cs.json @@ -3272,5 +3272,17 @@ "Beta available for web, desktop and Android. Thank you for trying the beta.": "Beta verze je k dispozici pro web, desktop a Android. Děkujeme vám za vyzkoušení beta verze.", "%(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "%(brand)s se znovu načte s vypnutými Prostory. Skupiny a vlastní značky budou opět viditelné.", "Spaces are a new way to group rooms and people.": "Prostory představují nový způsob seskupování místností a osob.", - "Message search initialisation failed": "Inicializace vyhledávání zpráv se nezdařila" + "Message search initialisation failed": "Inicializace vyhledávání zpráv se nezdařila", + "Spaces are a beta feature.": "Prostory jsou funkcí beta verze.", + "Search names and descriptions": "Hledat názvy a popisy", + "You may contact me if you have any follow up questions": "V případě dalších dotazů se na mě můžete obrátit", + "To leave the beta, visit your settings.": "Chcete-li opustit beta verzi, jděte do nastavení.", + "Your platform and username will be noted to help us use your feedback as much as we can.": "Vaše platforma a uživatelské jméno budou zaznamenány, abychom mohli co nejlépe využít vaši zpětnou vazbu.", + "%(featureName)s beta feedback": "%(featureName)s zpětná vazba beta verze", + "Thank you for your feedback, we really appreciate it.": "Děkujeme za vaši zpětnou vazbu, velmi si jí vážíme.", + "Beta feedback": "Zpětná vazba na betaverzi", + "Add reaction": "Přidat reakci", + "Send and receive voice messages": "Odeslat a přijmout hlasové zprávy", + "Your feedback will help make spaces better. The more detail you can go into, the better.": "Vaše zpětná vazba pomůže zlepšit prostory. Čím podrobnější bude, tím lépe.", + "If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "Pokud odejdete, %(brand)s se znovu načte s vypnutými Prostory. Skupiny a vlastní značky budou opět viditelné." } From f3e8d137dea3499fb97b0397b923d78984b44f6d Mon Sep 17 00:00:00 2001 From: XoseM Date: Sat, 15 May 2021 04:03:50 +0000 Subject: [PATCH 083/464] Translated using Weblate (Galician) Currently translated at 100.0% (2968 of 2968 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 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/strings/gl.json b/src/i18n/strings/gl.json index 0de93753f7..019929b081 100644 --- a/src/i18n/strings/gl.json +++ b/src/i18n/strings/gl.json @@ -3367,5 +3367,5 @@ "Send and receive voice messages": "Enviar e recibir mensaxes de voz", "Your feedback will help make spaces better. The more detail you can go into, the better.": "A túa opinión axudaranos a mellorar os espazos. Canto máis detallada sexa moito mellor para nós.", "If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "Se saes, %(brand)s volverá a cargar con Espazos desactivados. Comunidades e etiquetas personais serán visibles outra vez.", - "Message search initialisation failed": "Fallo a inicialización da busca de mensaxes" + "Message search initialisation failed": "Fallou a inicialización da busca de mensaxes" } From 34f902229e219d10662fea0dcbbd2592a2ee9d00 Mon Sep 17 00:00:00 2001 From: Trendyne Date: Sat, 15 May 2021 12:16:12 +0000 Subject: [PATCH 084/464] Translated using Weblate (Icelandic) Currently translated at 22.1% (656 of 2968 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 | 181 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 178 insertions(+), 3 deletions(-) diff --git a/src/i18n/strings/is.json b/src/i18n/strings/is.json index ae337d56bf..03fa871f29 100644 --- a/src/i18n/strings/is.json +++ b/src/i18n/strings/is.json @@ -136,7 +136,7 @@ "Attachment": "Viðhengi", "Hangup": "Leggja á", "Voice call": "Raddsamtal", - "Video call": "_Myndsímtal", + "Video call": "Myndsímtal", "Upload file": "Hlaða inn skrá", "Send an encrypted message…": "Senda dulrituð skilaboð…", "You do not have permission to post to this room": "Þú hefur ekki heimild til að senda skilaboð á þessa spjallrás", @@ -198,7 +198,7 @@ "Today": "Í dag", "Yesterday": "Í gær", "Error decrypting attachment": "Villa við afkóðun viðhengis", - "Copied!": "Afritað", + "Copied!": "Afritað!", "Custom Server Options": "Sérsniðnir valkostir vefþjóns", "Dismiss": "Hunsa", "Please check your email to continue registration.": "Skoðaðu tölvupóstinn þinn til að geta haldið áfram með skráningu.", @@ -541,5 +541,180 @@ "Sends the given message as a spoiler": "Sendir skilaboðið sem spoiler", "No need for symbols, digits, or uppercase letters": "Engin þörf á táknum, tölustöfum, eða hástöfum", "Use a few words, avoid common phrases": "Notaðu nokkur orð. Forðastu algengar setningar", - "Unknown server error": "Óþekkt villa á þjóni" + "Unknown server error": "Óþekkt villa á þjóni", + "Message deleted by %(name)s": "Skilaboð eytt af %(name)s", + "Message deleted": "Skilaboð eytt", + "Room list": "Herbergislisti", + "Subscribed lists": "Skráðir listar", + "eg: @bot:* or example.org": "t.d.: @vélmenni:* eða dæmi.is", + "Personal ban list": "Persónulegur bann listi", + "⚠ These settings are meant for advanced users.": "⚠ Þessar stillingar eru ætlaðar fyrir háþróaða notendur.", + "Ignored users": "Hunsaðir notendur", + "You are currently subscribed to:": "Þú ert skráður til:", + "View rules": "Skoða reglur", + "You are not subscribed to any lists": "Þú ert ekki skráður fyrir neina lista", + "You are currently ignoring:": "Þú ert að hunsa:", + "You have not ignored anyone.": "Þú hefur ekki hunsað nein.", + "User rules": "Reglur notanda", + "Server rules": "Reglur netþjóns", + "Please try again or view your console for hints.": "Vinsamlegast reyndu aftur eða skoðaðu framkvæmdaraðilaatvikuskrá þína fyrir vísbendingar.", + "Error unsubscribing from list": "Galli við að afskrá frá lista", + "Error removing ignored user/server": "Villa við að fjarlægja hunsaða notanda/netþjón", + "Use the Desktop app to search encrypted messages": "Notaðu tölvuforritið til að sía dulkóðuð skilaboð", + "Use the Desktop app to see all encrypted files": "Notaðu tölvuforritið til að sjá öll dulkóðuð gögn", + "Not encrypted": "Ekki dulkóðað", + "Encrypted by a deleted session": "Dulkóðað af eyddu tæki", + "Encrypted by an unverified session": "Dulkóðað af ósannreynu tæki", + "Enable message search in encrypted rooms": "Virka skilaboðleit í dulkóðuð herbergjum", + "This room is end-to-end encrypted": "Þetta herbergi er enda-til-enda dulkóðað", + "Unencrypted": "Ódulkóðað", + "Messages in this room are end-to-end encrypted.": "Skilaboð í þessu herbergi eru enda-til-enda dulkóðuð.", + "Clearing your browser's storage may fix the problem, but will sign you out and cause any encrypted chat history to become unreadable.": "Hreinsun geymslu vafrans gæti lagað vandamálið en mun skrá þig út og valda því að dulkóðaður spjallferil sé ólæsilegur.", + "Send an encrypted reply…": "Senda dulritað svar…", + "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.": "Þegar hún er gerð virk er ekki hægt að óvirka dulkóðun. Skilaboð í dulkóðuðu herbergi geta ekki verið séð af netþjóni en bara af þátttakendum í herberginu. Virkun dulkóðuns gæti komið í veg fyrir að vélmenni og brúr virki rétt. Lærðu meira um dulkóðun.", + "Once enabled, encryption cannot be disabled.": "Þegar kveikt er á dulkóðun er ekki hægt að slökkva á henni.", + "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.": "Í dulkóðuðum herbergjum eins og þetta er slökkt á forskoðun vefslóða sjálfgefið til að tryggja að heimaþjónn þinn (þar sem forsýningin myndast) geti ekki safnað upplýsingum um tengla sem þú sérð í þessu herbergi.", + "URL Previews": "Forskoðun Vefslóða", + "URL previews are disabled by default for participants in this room.": "Forskoðun vefslóða er ekki sjálfgefið fyrir þátttakendur í þessu herbergi.", + "URL previews are enabled by default for participants in this room.": "Forskoðun vefslóða er sjálfgefið fyrir þátttakendur í þessu herbergi.", + "You have disabled URL previews by default.": "Þú hefur óvirkt forskoðun vefslóða sjálfgefið.", + "You have enabled URL previews by default.": "Þú hefur virkt forskoðun vefslóða sjálfgefið.", + "Enable URL previews by default for participants in this room": "Virkja forskoðun vefslóða sjálfgefið fyrir þátttakendur í þessu herbergi", + "Enable URL previews for this room (only affects you)": "Virkja forskoðun vefslóða fyrir þetta herbergi (einungis fyrir þig)", + "Room settings": "Herbergisstillingar", + "Room Settings - %(roomName)s": "Herbergisstillingar - %(roomName)s", + "Pinned Messages": "Föst Skilaboð", + "No pinned messages.": "Engin föst skilaboð.", + "This is the beginning of your direct message history with .": "Þetta er upphaf beinna skilaboðasögu með .", + "Recently Direct Messaged": "Nýlega Fékk Bein Skilaboð", + "Direct Messages": "Bein skilaboð", + "Direct message": "Beint skilaboð", + "Frequently Used": "Oft notað", + "Filter all spaces": "Sía öll rými", + "Filter your rooms and spaces": "Sía rými og herbergin þín", + "Filter rooms and people": "Sía fólk og herbergi", + "Filter": "Sía", + "Your Security Key is in your Downloads folder.": "Öryggislykillinn þinn er Niðurhals möppu þinni.", + "Download logs": "Niðurhal atvikaskrá", + "Preparing to download logs": "Undirbý niðurhal atvikaskráa", + "Downloading logs": "Er að niðurhala atvikaskrá", + "Error downloading theme information.": "Villa við að niðurhala þemaupplýsingum.", + "Message downloading sleep time(ms)": "Skilaboða niðurhal svefn tími(ms)", + "How fast should messages be downloaded.": "Hve hratt ætti að hlaða niður skilaboðum.", + "Download %(text)s": "Niðurhala %(text)s", + "Share Link to User": "Deila Hlekk að Notanda", + "You have verified this user. This user has verified all of their sessions.": "Þú hefur sannreynt þennan notanda. Þessi notandi hefur sannreynt öll tæki þeirra.", + "This user has not verified all of their sessions.": "Þessi notandi hefur ekki sannreynt öll tæki þeirra.", + "%(count)s verified sessions|one": "1 sannreynt tæki", + "%(count)s verified sessions|other": "%(count)s sannreyn tæki", + "Hide verified sessions": "Fela sannreyn tæki", + "Remove recent messages": "Fjarlægja nýleg skilaboð", + "Remove recent messages by %(user)s": "Fjarlægja nýleg skilaboð af %(user)s", + "Messages in this room are not end-to-end encrypted.": "Skilaboð í þessu herbergi eru ekki enda-til-enda dulkóðuð.", + "Who would you like to add to this community?": "Hvern viltu bæta við í þetta samfélagi?", + "You cannot place a call with yourself.": "Þú getur ekki byrjað símtal með sjálfum þér.", + "You cannot place VoIP calls in this browser.": "Þú getur ekki byrjað netsímtal (VoIP) köll í þessum vafra.", + "Call Failed": "Símtal Mistókst", + "Every page you use in the app": "Sérhver síða sem þú notar í forritinu", + "Which officially provided instance you are using, if any": "Hvaða opinberlega veittan heimaþjón sem þú notar, ef einhvern", + "Whether or not you're logged in (we don't record your username)": "Hvort sem þú ert skráð(ur) inn (við skráum ekki notendanafnið þitt)", + "Add Phone Number": "Bæta Við Símanúmeri", + "Click the button below to confirm adding this phone number.": "Smelltu á hnappinn hér að neðan til að staðfesta að bæta við þessu símanúmeri.", + "Confirm adding phone number": "Staðfestu að bæta við símanúmeri", + "Add Email Address": "Bæta Við Tölvupóstfangi", + "Click the button below to confirm adding this email address.": "Smelltu á hnappinn hér að neðan til að staðfesta að bæta við þessu netfangi.", + "Confirm adding email": "Staðfestu að bæta við tölvupósti", + "Upgrade": "Uppfæra", + "Verify": "Sannreyna", + "Security": "Öryggi", + "Trusted": "Traustað", + "Subscribe": "Skrá", + "Unsubscribe": "Afskrá", + "None": "Ekkert", + "Ignored/Blocked": "Hunsað/Hindrað", + "Trust": "Treysta", + "Flags": "Fánar", + "Symbols": "Tákn", + "Objects": "Hlutir", + "Activities": "Starfsemi", + "Document": "Skjal", + "Complete": "Búið", + "View": "Skoða", + "Preview": "Forskoðun", + "Strikethrough": "Yfirstrikletrað", + "Italics": "Skáletrað", + "Bold": "Feitletrað", + "ID": "Auðkenni (ID)", + "Disconnect": "Aftengja", + "Share": "Deila", + "Revoke": "Afturkalla", + "Discovery": "Uppgötvun", + "Actions": "Aðgerðir", + "Messages": "Skilaboð", + "Summary": "Yfirlit", + "Service": "Þjónusta", + "Removing…": "Er að fjarlægja…", + "Browse": "Skoða", + "Reset": "Endursetja", + "Sounds": "Hljóð", + "edited": "breytt", + "Re-join": "Taka þátt aftur", + "Banana": "Banani", + "Fire": "Eldur", + "Cloud": "Ský", + "Moon": "Tungl", + "Globe": "Heiminn", + "Mushroom": "Sveppur", + "Cactus": "Kaktus", + "Tree": "Tré", + "Flower": "Blóm", + "Butterfly": "Fiðrildi", + "Octopus": "Kolkrabbi", + "Fish": "Fiskur", + "Turtle": "Skjaldbaka", + "Penguin": "Mörgæs", + "Rooster": "Hani", + "Panda": "Pandabjörn", + "Rabbit": "Kanína", + "Elephant": "Fíll", + "Pig": "Svín", + "Unicorn": "Einhyrningur", + "Horse": "Hestur", + "Lion": "Ljón", + "Cat": "Köttur", + "Dog": "Hundur", + "Guest": "Gestur", + "Other": "Annað", + "Confirm": "Staðfesta", + "Username": "Notandanafn", + "Join": "Taka þátt", + "Encrypted": "Dulkóðað", + "Encryption": "Dulkóðun", + "Timeline": "Tímalína", + "Composer": "Ritari", + "Preferences": "Stillingar", + "Versions": "Útgáfur", + "FAQ": "Algengar spurningar", + "Theme": "Þema", + "General": "Almennt", + "No": "Nei", + "Yes": "Já", + "Verified!": "Sannreynt!", + "Retry": "Reyna aftur", + "Download": "Niðurhal", + "Next": "Næsta", + "Legal": "Löglegt", + "Demote": "Leggja til baka", + "%(oneUser)sleft %(count)s times|one": "%(oneUser)sfór", + "%(severalUsers)sleft %(count)s times|one": "%(severalUsers)sfóru", + "%(oneUser)sjoined %(count)s times|one": "%(oneUser)sskráðist", + "%(severalUsers)sjoined %(count)s times|one": "%(severalUsers)sskráðust", + "Stickerpack": "Límmiða pakki", + "Replying": "Svara", + "%(duration)sd": "%(duration)sd", + "%(duration)sh": "%(duration)sklst", + "%(duration)sm": "%(duration)sm", + "%(duration)ss": "%(duration)ss", + "Emoji picker": "Tjáningartáknmyndvalmynd", + "Show less": "Sýna minna" } From 871c48f69b8039f82da7aeaff0b8a6e87ae973a5 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Tue, 18 May 2021 10:02:21 +0100 Subject: [PATCH 085/464] stop assuming that decryption happens ahead of time --- src/components/structures/FilePanel.js | 4 ++++ src/components/structures/RoomView.tsx | 2 +- src/components/views/messages/MessageActionBar.js | 7 +++++++ src/components/views/messages/ViewSourceEvent.js | 7 +++++++ src/indexing/EventIndex.js | 4 ++++ 5 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/components/structures/FilePanel.js b/src/components/structures/FilePanel.js index d5e4b092e2..8f9908f1ca 100644 --- a/src/components/structures/FilePanel.js +++ b/src/components/structures/FilePanel.js @@ -50,6 +50,10 @@ class FilePanel extends React.Component { if (room?.roomId !== this.props?.roomId) return; if (toStartOfTimeline || !data || !data.liveEvent || ev.isRedacted()) return; + if (ev.shouldAttemptDecryption()) { + ev.attemptDecryption(room._client._crypto); + } + if (ev.isBeingDecrypted()) { this.decryptingEvents.add(ev.getId()); } else { diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index c0f3c59457..dbfba13297 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()) return; + if (ev.isBeingDecrypted() || ev.isDecryptionFailure() || ev.shouldAttemptDecryption()) return; this.handleEffects(ev); }; diff --git a/src/components/views/messages/MessageActionBar.js b/src/components/views/messages/MessageActionBar.js index b2f7f8a692..cf3a0a704f 100644 --- a/src/components/views/messages/MessageActionBar.js +++ b/src/components/views/messages/MessageActionBar.js @@ -31,6 +31,7 @@ import {RovingAccessibleTooltipButton, useRovingTabIndex} from "../../../accessi import {replaceableComponent} from "../../../utils/replaceableComponent"; import {canCancel} from "../context_menus/MessageContextMenu"; import Resend from "../../../Resend"; +import { MatrixClientPeg } from "../../../MatrixClientPeg"; const OptionsButton = ({mxEvent, getTile, getReplyThread, permalinkCreator, onFocusChange}) => { const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu(); @@ -122,6 +123,12 @@ export default class MessageActionBar extends React.PureComponent { if (this.props.mxEvent.status && this.props.mxEvent.status !== EventStatus.SENT) { this.props.mxEvent.on("Event.status", this.onSent); } + + if (this.props.mxEvent.shouldAttemptDecryption()) { + const client = MatrixClientPeg.get(); + this.props.mxEvent.attemptDecryption(client._crypto); + } + if (this.props.mxEvent.isBeingDecrypted()) { this.props.mxEvent.once("Event.decrypted", this.onDecrypted); } diff --git a/src/components/views/messages/ViewSourceEvent.js b/src/components/views/messages/ViewSourceEvent.js index adc7a248cd..9a110a8826 100644 --- a/src/components/views/messages/ViewSourceEvent.js +++ b/src/components/views/messages/ViewSourceEvent.js @@ -18,6 +18,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { MatrixClientPeg } from "../../../MatrixClientPeg"; @replaceableComponent("views.messages.ViewSourceEvent") export default class ViewSourceEvent extends React.PureComponent { @@ -36,6 +37,12 @@ export default class ViewSourceEvent extends React.PureComponent { componentDidMount() { const {mxEvent} = this.props; + + const client = MatrixClientPeg.get(); + if (mxEvent.shouldAttemptDecryption()) { + mxEvent.attemptDecryption(client._client._crypto); + } + if (mxEvent.isBeingDecrypted()) { mxEvent.once("Event.decrypted", () => this.forceUpdate()); } diff --git a/src/indexing/EventIndex.js b/src/indexing/EventIndex.js index 857dc5b248..df46d800b1 100644 --- a/src/indexing/EventIndex.js +++ b/src/indexing/EventIndex.js @@ -187,6 +187,10 @@ export default class EventIndex extends EventEmitter { return; } + if (ev.shouldAttemptDecryption()) { + ev.attemptDecryption(room._client._crypto); + } + if (ev.isBeingDecrypted()) { // XXX: Private member access await ev._decryptionPromise; From 1cfd4b6e1aa176f10dfb572cbdb7cad92a7f467a Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Tue, 18 May 2021 11:41:20 +0100 Subject: [PATCH 086/464] Use client.decryptEvent to avoid accessing js-sdk private members --- src/components/structures/FilePanel.js | 5 ++-- src/components/structures/TimelinePanel.js | 3 +- .../views/messages/MessageActionBar.js | 2 +- .../views/messages/ViewSourceEvent.js | 2 +- src/indexing/EventIndex.js | 30 +++++-------------- 5 files changed, 14 insertions(+), 28 deletions(-) diff --git a/src/components/structures/FilePanel.js b/src/components/structures/FilePanel.js index 8f9908f1ca..ff30c25073 100644 --- a/src/components/structures/FilePanel.js +++ b/src/components/structures/FilePanel.js @@ -50,9 +50,8 @@ class FilePanel extends React.Component { if (room?.roomId !== this.props?.roomId) return; if (toStartOfTimeline || !data || !data.liveEvent || ev.isRedacted()) return; - if (ev.shouldAttemptDecryption()) { - ev.attemptDecryption(room._client._crypto); - } + const client = MatrixClientPeg.get(); + client.decryptEvent(ev); if (ev.isBeingDecrypted()) { this.decryptingEvents.add(ev.getId()); diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 5012d91a5f..4918a4a3b4 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -1150,7 +1150,8 @@ class TimelinePanel extends React.Component { .reverse() .forEach(event => { if (event.shouldAttemptDecryption()) { - event.attemptDecryption(MatrixClientPeg.get()._crypto); + const client = MatrixClientPeg.get(); + client.decryptEvent(event); } }); diff --git a/src/components/views/messages/MessageActionBar.js b/src/components/views/messages/MessageActionBar.js index cf3a0a704f..21b7cd64c9 100644 --- a/src/components/views/messages/MessageActionBar.js +++ b/src/components/views/messages/MessageActionBar.js @@ -126,7 +126,7 @@ export default class MessageActionBar extends React.PureComponent { if (this.props.mxEvent.shouldAttemptDecryption()) { const client = MatrixClientPeg.get(); - this.props.mxEvent.attemptDecryption(client._crypto); + client.decryptEvent(this.props.mxEvent); } if (this.props.mxEvent.isBeingDecrypted()) { diff --git a/src/components/views/messages/ViewSourceEvent.js b/src/components/views/messages/ViewSourceEvent.js index 9a110a8826..9e30261fd6 100644 --- a/src/components/views/messages/ViewSourceEvent.js +++ b/src/components/views/messages/ViewSourceEvent.js @@ -40,7 +40,7 @@ export default class ViewSourceEvent extends React.PureComponent { const client = MatrixClientPeg.get(); if (mxEvent.shouldAttemptDecryption()) { - mxEvent.attemptDecryption(client._client._crypto); + client.decryptEvent(mxEvent); } if (mxEvent.isBeingDecrypted()) { diff --git a/src/indexing/EventIndex.js b/src/indexing/EventIndex.js index df46d800b1..4d207d9f49 100644 --- a/src/indexing/EventIndex.js +++ b/src/indexing/EventIndex.js @@ -177,8 +177,10 @@ export default class EventIndex extends EventEmitter { * listener. */ onRoomTimeline = async (ev, room, toStartOfTimeline, removed, data) => { + const client = MatrixClientPeg.get(); + // We only index encrypted rooms locally. - if (!MatrixClientPeg.get().isRoomEncrypted(room.roomId)) return; + if (!client.isRoomEncrypted(room.roomId)) return; // If it isn't a live event or if it's redacted there's nothing to // do. @@ -187,14 +189,7 @@ export default class EventIndex extends EventEmitter { return; } - if (ev.shouldAttemptDecryption()) { - ev.attemptDecryption(room._client._crypto); - } - - if (ev.isBeingDecrypted()) { - // XXX: Private member access - await ev._decryptionPromise; - } + await client.decryptEvent(ev); await this.addLiveEventToIndex(ev); } @@ -522,19 +517,10 @@ export default class EventIndex extends EventEmitter { const decryptionPromises = matrixEvents .filter(event => event.isEncrypted()) .map(event => { - if (event.shouldAttemptDecryption()) { - return event.attemptDecryption(client._crypto, { - isRetry: true, - emit: false, - }); - } else { - // TODO the decryption promise is a private property, this - // should either be made public or we should convert the - // event that gets fired when decryption is done into a - // promise using the once event emitter method: - // https://nodejs.org/api/events.html#events_events_once_emitter_name - return event._decryptionPromise; - } + return client.decryptEvent(event, { + isRetry: true, + emit: false, + }); }); // Let us wait for all the events to get decrypted. From 2732280923aa696c559bd70ba72bc4ac5d83825e Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 18 May 2021 11:54:45 +0100 Subject: [PATCH 087/464] Fix space room hierarchy not updating when removing a room --- src/components/structures/SpaceRoomDirectory.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/components/structures/SpaceRoomDirectory.tsx b/src/components/structures/SpaceRoomDirectory.tsx index 5091131d49..c1e9027b1d 100644 --- a/src/components/structures/SpaceRoomDirectory.tsx +++ b/src/components/structures/SpaceRoomDirectory.tsx @@ -471,8 +471,12 @@ export const SpaceHierarchy: React.FC = ({ 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))); + parentChildMap.get(parentId).delete(childId); + if (parentChildMap.get(parentId).size > 0) { + parentChildMap.set(parentId, new Map(parentChildMap.get(parentId))); + } else { + parentChildMap.delete(parentId); + } } } catch (e) { setError(_t("Failed to remove some rooms. Try again later")); From f9f10de0da767431a8bead387b4b960993537f4a Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Tue, 18 May 2021 13:01:38 +0100 Subject: [PATCH 088/464] use renamed decrypt event method --- src/components/structures/FilePanel.js | 2 +- src/components/structures/TimelinePanel.js | 6 ++---- src/components/views/messages/MessageActionBar.js | 6 ++---- src/components/views/messages/ViewSourceEvent.js | 4 +--- src/indexing/EventIndex.js | 4 ++-- 5 files changed, 8 insertions(+), 14 deletions(-) diff --git a/src/components/structures/FilePanel.js b/src/components/structures/FilePanel.js index ff30c25073..bb7c1f9642 100644 --- a/src/components/structures/FilePanel.js +++ b/src/components/structures/FilePanel.js @@ -51,7 +51,7 @@ class FilePanel extends React.Component { if (toStartOfTimeline || !data || !data.liveEvent || ev.isRedacted()) return; const client = MatrixClientPeg.get(); - client.decryptEvent(ev); + client.decryptEventIfNeeded(ev); if (ev.isBeingDecrypted()) { this.decryptingEvents.add(ev.getId()); diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 4918a4a3b4..af20c31cb2 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -1149,10 +1149,8 @@ class TimelinePanel extends React.Component { arrayFastClone(events) .reverse() .forEach(event => { - if (event.shouldAttemptDecryption()) { - const client = MatrixClientPeg.get(); - client.decryptEvent(event); - } + const client = MatrixClientPeg.get(); + client.decryptEventIfNeeded(event); }); const firstVisibleEventIndex = this._checkForPreJoinUISI(events); diff --git a/src/components/views/messages/MessageActionBar.js b/src/components/views/messages/MessageActionBar.js index 21b7cd64c9..37737519ce 100644 --- a/src/components/views/messages/MessageActionBar.js +++ b/src/components/views/messages/MessageActionBar.js @@ -124,10 +124,8 @@ export default class MessageActionBar extends React.PureComponent { this.props.mxEvent.on("Event.status", this.onSent); } - if (this.props.mxEvent.shouldAttemptDecryption()) { - const client = MatrixClientPeg.get(); - client.decryptEvent(this.props.mxEvent); - } + const client = MatrixClientPeg.get(); + client.decryptEventIfNeeded(this.props.mxEvent); if (this.props.mxEvent.isBeingDecrypted()) { this.props.mxEvent.once("Event.decrypted", this.onDecrypted); diff --git a/src/components/views/messages/ViewSourceEvent.js b/src/components/views/messages/ViewSourceEvent.js index 9e30261fd6..2ec567c5ad 100644 --- a/src/components/views/messages/ViewSourceEvent.js +++ b/src/components/views/messages/ViewSourceEvent.js @@ -39,9 +39,7 @@ export default class ViewSourceEvent extends React.PureComponent { const {mxEvent} = this.props; const client = MatrixClientPeg.get(); - if (mxEvent.shouldAttemptDecryption()) { - client.decryptEvent(mxEvent); - } + client.decryptEventIfNeeded(mxEvent); if (mxEvent.isBeingDecrypted()) { mxEvent.once("Event.decrypted", () => this.forceUpdate()); diff --git a/src/indexing/EventIndex.js b/src/indexing/EventIndex.js index 4d207d9f49..ed4418140b 100644 --- a/src/indexing/EventIndex.js +++ b/src/indexing/EventIndex.js @@ -189,7 +189,7 @@ export default class EventIndex extends EventEmitter { return; } - await client.decryptEvent(ev); + await client.decryptEventIfNeeded(ev); await this.addLiveEventToIndex(ev); } @@ -517,7 +517,7 @@ export default class EventIndex extends EventEmitter { const decryptionPromises = matrixEvents .filter(event => event.isEncrypted()) .map(event => { - return client.decryptEvent(event, { + return client.decryptEventIfNeeded(event, { isRetry: true, emit: false, }); From 655010844adcf45b1427a05a64d27409523e0d8d Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 18 May 2021 13:27:34 +0100 Subject: [PATCH 089/464] Switch to using QueryMatcher for add existing to space dialog This helps it support filtering by alias --- .../dialogs/AddExistingToSpaceDialog.tsx | 55 +++++++++++-------- 1 file changed, 33 insertions(+), 22 deletions(-) diff --git a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx index af52fbce6c..faeaa80c15 100644 --- a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx +++ b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx @@ -38,6 +38,7 @@ import {sortRooms} from "../../../stores/room-list/algorithms/tag-sorting/Recent import ProgressBar from "../elements/ProgressBar"; import {SpaceFeedbackPrompt} from "../../structures/SpaceRoomView"; import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar"; +import QueryMatcher from "../../../autocomplete/QueryMatcher"; interface IProps extends IDialogProps { matrixClient: MatrixClient; @@ -74,37 +75,47 @@ export const AddExistingToSpace: React.FC = ({ onFinished, }) => { const cli = useContext(MatrixClientContext); - const visibleRooms = useMemo(() => sortRooms(cli.getVisibleRooms()), [cli]); + const visibleRooms = useMemo(() => cli.getVisibleRooms().filter(r => r.getMyMembership() === "join"), [cli]); const [selectedToAdd, setSelectedToAdd] = useState(new Set()); const [progress, setProgress] = useState(null); const [error, setError] = useState(null); const [query, setQuery] = useState(""); - const lcQuery = query.toLowerCase(); + const lcQuery = query.toLowerCase().trim(); - const existingSubspaces = SpaceStore.instance.getChildSpaces(space.roomId); - const existingSubspacesSet = new Set(existingSubspaces); - const existingRoomsSet = new Set(SpaceStore.instance.getChildRooms(space.roomId)); + const existingSubspacesSet = useMemo(() => new Set(SpaceStore.instance.getChildSpaces(space.roomId)), [space]); + const existingRoomsSet = useMemo(() => new Set(SpaceStore.instance.getChildRooms(space.roomId)), [space]); - 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; + const [spaces, rooms, dms] = useMemo(() => { + let rooms = visibleRooms; - if (room.isSpaceRoom()) { - if (room !== space && !existingSubspacesSet.has(room)) { - arr[0].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); - } + if (lcQuery) { + const matcher = new QueryMatcher(visibleRooms, { + keys: ["name"], + funcs: [r => r.getCanonicalAlias() ?? r.getAltAliases()?.[0]], + shouldMatchWordsOnly: false, + }); + + rooms = matcher.match(lcQuery); } - return arr; - }, [[], [], []]); + + const joinRule = space.getJoinRule(); + return sortRooms(rooms).reduce((arr, room) => { + if (room.isSpaceRoom()) { + if (room !== space && !existingSubspacesSet.has(room)) { + arr[0].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; + }, [[], [], []]); + }, [visibleRooms, space, lcQuery, existingRoomsSet, existingSubspacesSet]); const addRooms = async () => { setError(null); From edb20267801fa7d20eb2a93ce092627e7c716649 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 18 May 2021 13:31:53 +0100 Subject: [PATCH 090/464] Support any alias not just first --- src/autocomplete/QueryMatcher.ts | 9 +++++++-- .../views/dialogs/AddExistingToSpaceDialog.tsx | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/autocomplete/QueryMatcher.ts b/src/autocomplete/QueryMatcher.ts index ea6e0882fd..73bb37ff0f 100644 --- a/src/autocomplete/QueryMatcher.ts +++ b/src/autocomplete/QueryMatcher.ts @@ -21,7 +21,7 @@ import {removeHiddenChars} from "matrix-js-sdk/src/utils"; interface IOptions { keys: Array; - funcs?: Array<(T) => string>; + funcs?: Array<(T) => string | string[]>; shouldMatchWordsOnly?: boolean; // whether to apply unhomoglyph and strip diacritics to fuzz up the search. Defaults to true fuzzy?: boolean; @@ -69,7 +69,12 @@ export default class QueryMatcher { if (this._options.funcs) { for (const f of this._options.funcs) { - keyValues.push(f(object)); + const v = f(object); + if (Array.isArray(v)) { + keyValues.push(...v); + } else { + keyValues.push(v); + } } } diff --git a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx index faeaa80c15..9a7f96e653 100644 --- a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx +++ b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx @@ -92,7 +92,7 @@ export const AddExistingToSpace: React.FC = ({ if (lcQuery) { const matcher = new QueryMatcher(visibleRooms, { keys: ["name"], - funcs: [r => r.getCanonicalAlias() ?? r.getAltAliases()?.[0]], + funcs: [r => [r.getCanonicalAlias(), ...r.getAltAliases()].filter(Boolean)], shouldMatchWordsOnly: false, }); From 454df8947be27c9fe1529043061b07e007a000c6 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Tue, 18 May 2021 13:46:47 +0100 Subject: [PATCH 091/464] Add mock for new client method --- test/test-utils.js | 1 + 1 file changed, 1 insertion(+) diff --git a/test/test-utils.js b/test/test-utils.js index 6dc02463a5..953693a820 100644 --- a/test/test-utils.js +++ b/test/test-utils.js @@ -95,6 +95,7 @@ export function createTestClient() { getItem: jest.fn(), }, }, + decryptEventIfNeeded: () => Promise.resolve(), }; } From 07d74693af2e3e2c11d4e455e35e9ee1267e3272 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Tue, 18 May 2021 16:00:39 +0100 Subject: [PATCH 092/464] 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 0e221ae5488c99935f09a051f3da121ac5899e47 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Tue, 18 May 2021 16:24:38 +0100 Subject: [PATCH 093/464] 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 84cf35aeb4..397d637125 100644 --- a/src/stores/widgets/StopGapWidget.ts +++ b/src/stores/widgets/StopGapWidget.ts @@ -400,6 +400,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 fc6ff861730ae821b7cd5dda288a5c42a286a3a9 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Tue, 18 May 2021 16:34:00 +0100 Subject: [PATCH 094/464] Add error detail when languges fail to load This will at least log the path that's failing with status code, so we can better confirm the issue. Related to https://github.com/vector-im/element-web/issues/9422 --- src/languageHandler.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/languageHandler.tsx b/src/languageHandler.tsx index f30e735f03..4a3fb30886 100644 --- a/src/languageHandler.tsx +++ b/src/languageHandler.tsx @@ -464,10 +464,14 @@ function getLangsJson(): Promise { request( { method: "GET", url }, (err, response, body) => { - if (err || response.status < 200 || response.status >= 300) { + if (err) { reject(err); return; } + if (response.status < 200 || response.status >= 300) { + reject(new Error(`Failed to load ${url}, got ${response.status}`)); + return; + } resolve(JSON.parse(body)); }, ); @@ -507,10 +511,14 @@ function getLanguage(langPath: string): Promise { request( { method: "GET", url: langPath }, (err, response, body) => { - if (err || response.status < 200 || response.status >= 300) { + if (err) { reject(err); return; } + if (response.status < 200 || response.status >= 300) { + reject(new Error(`Failed to load ${langPath}, got ${response.status}`)); + return; + } resolve(weblateToCounterpart(JSON.parse(body))); }, ); From 00024c794c65c1d67b5e467ee852f397f746f3d2 Mon Sep 17 00:00:00 2001 From: libexus Date: Tue, 18 May 2021 16:56:45 +0000 Subject: [PATCH 095/464] Translated using Weblate (German) Currently translated at 99.6% (2958 of 2968 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 | 34 ++++++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index 0113f1d07f..cfe87ad823 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -1017,7 +1017,7 @@ "Language and region": "Sprache und Region", "Theme": "Design", "Account management": "Benutzerkontenverwaltung", - "For help with using %(brand)s, click here.": "Um Hilfe zur Benutzung von %(brand)s zu erhalten, klicke hier.", + "For help with using %(brand)s, click here.": "Um Hilfe zur Benutzung von %(brand)s zu erhalten, klicke hier.", "For help with using %(brand)s, click here or start a chat with our bot using the button below.": "Um Hilfe zur Benutzung von %(brand)s zu erhalten, klicke hier oder beginne einen Chat mit unserem Bot. Klicke dazu auf den unteren Knopf.", "Chat with %(brand)s Bot": "Chatte mit dem %(brand)s-Bot", "Help & About": "Hilfe und Über", @@ -1236,7 +1236,7 @@ "Name or Matrix ID": "Name oder Matrix-ID", "Your %(brand)s is misconfigured": "Dein %(brand)s ist falsch konfiguriert", "You cannot modify widgets in this room.": "Du darfst in diesem Raum keine Widgets verändern.", - "Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "Ob du die \"Breadcrumbs\"-Funktion nutzt oder nicht (Avatare oberhalb der Raumliste)", + "Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "Ob du die Liste der kürzlich besuchten Räume oberhalb der Raumliste nutzt", "The server does not support the room version specified.": "Der Server unterstützt die angegebene Raumversion nicht.", "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.": "Achtung: Ein Raum-Upgrade wird die Mitglieder des Raumes nicht automatisch auf die neue Version migrieren. Wir werden in der alten Raumversion einen Link zum neuen Raum posten - Raummitglieder müssen dann auf diesen Link klicken um dem neuen Raum beizutreten.", "Replying With Files": "Mit Dateien antworten", @@ -2156,7 +2156,7 @@ "Liberate your communication": "Befreie deine Kommunikation", "Message downloading sleep time(ms)": "Wartezeit zwischen dem Herunterladen von Nachrichten (ms)", "Navigate recent messages to edit": "Letzte Nachrichten zur Bearbeitung ansehen", - "Jump to start/end of the composer": "Springe zum Anfang/Ende der Nachrichteneingabe", + "Jump to start/end of the composer": "Zu Anfang/Ende des Textfelds springen", "Navigate composer history": "Verlauf der Nachrichteneingabe durchsuchen", "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.": "Wenn du dies versehentlich getan hast, kannst du in dieser Sitzung \"sichere Nachrichten\" einrichten, die den Nachrichtenverlauf dieser Sitzung mit einer neuen Wiederherstellungsmethode erneut verschlüsseln.", "Cancel replying to a message": "Nachricht beantworten abbrechen", @@ -3059,7 +3059,7 @@ "Cookie Policy": "Cookie-Richtlinie", "Learn more in our , and .": "Erfahre mehr in unserer , und .", "Failed to connect to your homeserver. Please close this dialog and try again.": "Verbindung zum Homeserver fehlgeschlagen. Bitte schließe diesen Dialog and versuche es erneut.", - "Abort": "Abbrechen", + "Abort": "Beenden", "Upgrade to %(hostSignupBrand)s": "Zu %(hostSignupBrand)s upgraden", "Edit Values": "Werte bearbeiten", "Value in this room:": "Wert in diesem Raum:", @@ -3239,8 +3239,8 @@ "Values at explicit levels in this room:": "Werte für explizite Stufen in diesem Raum:", "Values at explicit levels:": "Werte für explizite Stufen:", "Values at explicit levels in this room": "Werte für explizite Stufen in diesem Raum", - "Confirm abort of host creation": "Bestätige das Abbrechen der Host-Erstellung", - "Are you sure you wish to abort creation of the host? The process cannot be continued.": "Soll die Host-Erstellung wirklich abgebrochen werden? Dieser Prozess kann nicht wieder fortgesetzt werden.", + "Confirm abort of host creation": "Bestätige das Beenden der Host-Erstellung", + "Are you sure you wish to abort creation of the host? The process cannot be continued.": "Soll die Host-Erstellung wirklich beendet werden? Dieser Prozess kann nicht wieder fortgesetzt werden.", "Invite to just this room": "Nur in diesen Raum einladen", "Consult first": "Konsultiere zuerst", "Reset event store?": "Ereignisspeicher zurück setzen?", @@ -3296,7 +3296,7 @@ "You have no ignored users.": "Du ignorierst keine Benutzer.", "Error processing voice message": "Fehler beim Verarbeiten der Sprachnachricht", "To join %(spaceName)s, turn on the Spaces beta": "Um %(spaceName)s beizutreten, aktiviere die Spaces Betaversion", - "To view %(spaceName)s, turn on the Spaces beta": "Um %(spaceName)s zu betreten, aktiviere die Spaces Betaversion", + "To view %(spaceName)s, turn on the Spaces beta": "Um %(spaceName)s zu betreten, aktiviere die Spaces Beta", "Select a room below first": "Wähle zuerst einen Raum aus", "Communities are changing to Spaces": "Spaces ersetzen Communities", "Join the beta": "Beta beitreten", @@ -3308,9 +3308,9 @@ "Adding rooms... (%(progress)s out of %(count)s)|one": "Raum wird hinzugefügt...", "Adding rooms... (%(progress)s out of %(count)s)|other": "Räume werden hinzugefügt... (%(progress)s von %(count)s)", "You can add existing spaces to a space.": "Du kannst existierende Spaces zu einem Space hinzfügen.", - "Feeling experimental?": "Lust auf Experimente?", + "Feeling experimental?": "Willst du die Entwicklung von Element hautnah miterleben?", "You are not allowed to view this server's rooms list": "Du darfst diese Raumliste nicht sehen", - "We didn't find a microphone on your device. Please check your settings and try again.": "Auf deinem Gerät kann kein Mikrofon gefunden werden. Bitte überprüfe deine Einstellungen und versuche es nochmal.", + "We didn't find a microphone on your device. Please check your settings and try again.": "Es konnte kein Mikrofon gefunden werden. Überprüfe deine Einstellungen und versuche es erneut.", "No microphone found": "Kein Mikrofon gefunden", "We were unable to access your microphone. Please check your browser settings and try again.": "Fehler beim Zugriff auf dein Mikrofon. Überprüfe deine Browsereinstellungen und versuche es nochmal.", "Unable to access your microphone": "Fehler beim Zugriff auf Mikrofon", @@ -3327,5 +3327,19 @@ "Spaces are a new way to group rooms and people. To join an existing space you'll need an invite.": "Wir haben Spaces entwickelt, damit ihr eure vielen Räume besser organisieren könnt. Um einen existierenden Space beitreten zu können musst du (noch) von jemandem eingeladen werden.", "Spaces are a new way to group rooms and people.": "Wir haben Spaces entwickelt, damit ihr eure vielen Räume besser organisieren könnt.", "Message search initialisation failed": "Initialisierung der Nachrichtensuche fehlgeschlagen", - "Send and receive voice messages": "Sprachnachrichten" + "Send and receive voice messages": "Sprachnachrichten", + "Search names and descriptions": "Nach Name und Beschreibung filtern", + "Not all selected were added": "Nicht alle Ausgewählten konnten hinzugefügt werden", + "Add reaction": "Reaktion hinzufügen", + "This is an experimental feature. For now, new users receiving an invite will have to open the invite on to actually join.": "Dieses Feature ist experimentell. Falls du eine Einladung erhältst musst du sie momentan noch auf öffnen, um beizutreten.", + "You may contact me if you have any follow up questions": "Kontaktiert mich, falls ihr weitere Fragen zu meinem Feedback habt", + "To leave the beta, visit your settings.": "Du kannst die Beta in den Einstellungen deaktivieren.", + "Your platform and username will be noted to help us use your feedback as much as we can.": "Die Platform von Element und dein Benutzername werden mitgeschickt, damit wir dein Feedback bestmöglich nachvollziehen können.", + "%(featureName)s beta feedback": "%(featureName)s-Beta Feedback", + "Thank you for your feedback, we really appreciate it.": "Uns liegt es am Herzen, Element zu verbessern. Deshalb ein großes Danke für dein Feedback.", + "Beta feedback": "Beta Feedback", + "Your access token gives full access to your account. Do not share it with anyone.": "Dein Zugriffstoken gibt vollen Zugriff auf dein Konto. Teile es niemals mit jemanden anderen.", + "Access Token": "Zugriffstoken", + "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 wieder Communities und Custom Tags verwenden." } From 8eb0a8b3b51d9dae1be5121c0b87128791977486 Mon Sep 17 00:00:00 2001 From: Thibault Martin Date: Tue, 18 May 2021 06:28:27 +0000 Subject: [PATCH 096/464] Translated using Weblate (French) Currently translated at 100.0% (2968 of 2968 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 36446ebba3..dc8b701e35 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -3333,7 +3333,7 @@ "%(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "%(brand)s va être redémarré avec les espaces désactivés. Les communautés et les étiquettes personnalisées seront de nouveau visibles.", "Spaces are a new way to group rooms and people.": "Les espaces sont un nouveau moyen de regrouper les salons et les personnes.", "Message search initialisation failed": "Échec de l’initialisation de la recherche de message", - "Your feedback will help make spaces better. The more detail you can go into, the better.": "Vos commentaires aideront à rendre les espaces meilleurs. N'hésitez pas à entrer dans les détails.", + "Your feedback will help make spaces better. The more detail you can go into, the better.": "Vos commentaires aideront à améliorer les espaces. N’hésitez pas à entrer dans les détails.", "%(featureName)s beta feedback": "Commentaires sur la bêta de %(featureName)s", "Thank you for your feedback, we really appreciate it.": "Merci pour vos commentaires, nous en sommes vraiment reconnaissants.", "Beta feedback": "Commentaires sur la bêta", From 90c660a74f49addca117b1d2acefa665152d19a5 Mon Sep 17 00:00:00 2001 From: LinAGKar Date: Mon, 17 May 2021 19:52:36 +0000 Subject: [PATCH 097/464] Translated using Weblate (Swedish) Currently translated at 100.0% (2968 of 2968 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 | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/sv.json b/src/i18n/strings/sv.json index 74e7a30032..1337dc47b7 100644 --- a/src/i18n/strings/sv.json +++ b/src/i18n/strings/sv.json @@ -3295,5 +3295,7 @@ "%(brand)s will reload with Spaces enabled. Communities and custom tags will be hidden.": "%(brand)s kommer att ladda om med utrymmen aktiverade. Gemenskaper och anpassade taggar kommer att döljas.", "Beta available for web, desktop and Android. Thank you for trying the beta.": "Beta tillgänglig för webben, skrivbord och Android. Tack för att du provar betan.", "If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "Om du lämnar så kommer %(brand)s att ladda om med utrymmen inaktiverade. Gemenskaper och anpassade taggar kommer att synas igen.", - "Spaces are a new way to group rooms and people.": "Utrymmen är nya sätt att gruppera rum och personer." + "Spaces are a new way to group rooms and people.": "Utrymmen är nya sätt att gruppera rum och personer.", + "This is an experimental feature. For now, new users receiving an invite will have to open the invite on to actually join.": "Det här är en experimentell funktion. För tillfället så behöver nya inbjudna användare öppna inbjudan på för att faktiskt gå med.", + "To join %(spaceName)s, turn on the Spaces beta": "För att gå med i %(spaceName)s, aktivera utrymmesbetan" } From da3e0a4cb46e1ce359aff0a8d02c7c4be10f5784 Mon Sep 17 00:00:00 2001 From: Hivaa Date: Tue, 18 May 2021 04:16:18 +0000 Subject: [PATCH 098/464] Translated using Weblate (Persian) Currently translated at 20.2% (601 of 2968 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, 3 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/fa.json b/src/i18n/strings/fa.json index 5948048561..7dcce06ff6 100644 --- a/src/i18n/strings/fa.json +++ b/src/i18n/strings/fa.json @@ -632,5 +632,7 @@ "Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "هرگاه این صفحه شامل اطلاعات قابل شناسایی مانند شناسه‌ی اتاق ، کاربر یا گروه باشد ، این داده‌ها قبل از ارسال به سرور حذف می شوند.", "Your user agent": "نماینده کاربری شما", "Whether you're using %(brand)s as an installed Progressive Web App": "این که آیا شما از%(brand)s به عنوان یک PWA استفاده می‌کنید یا نه", - "Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "این که آیا از ویژگی 'breadcrumbs' (نمایه‌ی کاربری بالای فهرست اتاق‌ها) استفاده می‌کنید یا خیر" + "Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "این که آیا از ویژگی 'breadcrumbs' (نمایه‌ی کاربری بالای فهرست اتاق‌ها) استفاده می‌کنید یا خیر", + "Use an identity server to invite by email. Manage in Settings.": "برای دعوت از یک سرور هویت‌سنجی استفاده نمائید. می‌توانید این مورد را در تنظیمات پیکربندی نمائید.", + "Use an identity server to invite by email. Click continue to use the default identity server (%(defaultIdentityServerName)s) or manage in Settings.": "برای دعوت با استفاده از ایمیل از یک سرور هویت‌سنجی استفاده نمائید. جهت استفاده از سرور هویت‌سنجی پیش‌فرض (%(defaultIdentityServerName)s) بر روی ادامه کلیک کنید، وگرنه آن را در بخش تنظیمات پیکربندی نمائید." } From ec7c1ab9f0423921d4fbab2c40070fc703dd943b Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 18 May 2021 15:40:09 -0600 Subject: [PATCH 099/464] Revert "Try putting room list handling behind a lock" --- src/stores/room-list/algorithms/Algorithm.ts | 325 +++++++++---------- src/utils/MultiLock.ts | 30 -- 2 files changed, 155 insertions(+), 200 deletions(-) delete mode 100644 src/utils/MultiLock.ts diff --git a/src/stores/room-list/algorithms/Algorithm.ts b/src/stores/room-list/algorithms/Algorithm.ts index c50db43d08..024c484c41 100644 --- a/src/stores/room-list/algorithms/Algorithm.ts +++ b/src/stores/room-list/algorithms/Algorithm.ts @@ -34,7 +34,6 @@ import { OrderingAlgorithm } from "./list-ordering/OrderingAlgorithm"; import { getListAlgorithmInstance } from "./list-ordering"; import SettingsStore from "../../../settings/SettingsStore"; import { VisibilityProvider } from "../filters/VisibilityProvider"; -import { MultiLock } from "../../../utils/MultiLock"; /** * Fired when the Algorithm has determined a list has been updated. @@ -78,7 +77,6 @@ export class Algorithm extends EventEmitter { } = {}; private allowedByFilter: Map = new Map(); private allowedRoomsByFilters: Set = new Set(); - private handlerLock = new MultiLock(); /** * Set to true to suspend emissions of algorithm updates. @@ -681,204 +679,191 @@ export class Algorithm extends EventEmitter { public async handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise { if (SettingsStore.getValue("advancedRoomListLogging")) { // TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602 - console.log(`Acquiring lock for ${room.roomId} with cause ${cause}`); + console.log(`Handle room update for ${room.roomId} called with cause ${cause}`); } - const release = await this.handlerLock.acquire(room.roomId); - try { - if (SettingsStore.getValue("advancedRoomListLogging")) { - // TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602 - console.log(`Handle room update for ${room.roomId} called with cause ${cause}`); + if (!this.algorithms) throw new Error("Not ready: no algorithms to determine tags from"); + + // Note: check the isSticky against the room ID just in case the reference is wrong + const isSticky = this._stickyRoom && this._stickyRoom.room && this._stickyRoom.room.roomId === room.roomId; + if (cause === RoomUpdateCause.NewRoom) { + const isForLastSticky = this._lastStickyRoom && this._lastStickyRoom.room === room; + const roomTags = this.roomIdsToTags[room.roomId]; + const hasTags = roomTags && roomTags.length > 0; + + // Don't change the cause if the last sticky room is being re-added. If we fail to + // pass the cause through as NewRoom, we'll fail to lie to the algorithm and thus + // lose the room. + if (hasTags && !isForLastSticky) { + console.warn(`${room.roomId} is reportedly new but is already known - assuming TagChange instead`); + cause = RoomUpdateCause.PossibleTagChange; } - if (!this.algorithms) throw new Error("Not ready: no algorithms to determine tags from"); - // Note: check the isSticky against the room ID just in case the reference is wrong - const isSticky = this._stickyRoom && this._stickyRoom.room && this._stickyRoom.room.roomId === room.roomId; - if (cause === RoomUpdateCause.NewRoom) { - const isForLastSticky = this._lastStickyRoom && this._lastStickyRoom.room === room; - const roomTags = this.roomIdsToTags[room.roomId]; - const hasTags = roomTags && roomTags.length > 0; - - // Don't change the cause if the last sticky room is being re-added. If we fail to - // pass the cause through as NewRoom, we'll fail to lie to the algorithm and thus - // lose the room. - if (hasTags && !isForLastSticky) { - console.warn(`${room.roomId} is reportedly new but is already known - assuming TagChange instead`); - cause = RoomUpdateCause.PossibleTagChange; - } - - // Check to see if the room is known first - let knownRoomRef = this.rooms.includes(room); - if (hasTags && !knownRoomRef) { - console.warn(`${room.roomId} might be a reference change - attempting to update reference`); - this.rooms = this.rooms.map(r => r.roomId === room.roomId ? room : r); - knownRoomRef = this.rooms.includes(room); - if (!knownRoomRef) { - console.warn(`${room.roomId} is still not referenced. It may be sticky.`); - } - } - - // If we have tags for a room and don't have the room referenced, something went horribly - // wrong - the reference should have been updated above. - if (hasTags && !knownRoomRef && !isSticky) { - throw new Error(`${room.roomId} is missing from room array but is known`); - } - - // Like above, update the reference to the sticky room if we need to - if (hasTags && isSticky) { - // Go directly in and set the sticky room's new reference, being careful not - // to trigger a sticky room update ourselves. - this._stickyRoom.room = room; - } - - // If after all that we're still a NewRoom update, add the room if applicable. - // We don't do this for the sticky room (because it causes duplication issues) - // or if we know about the reference (as it should be replaced). - if (cause === RoomUpdateCause.NewRoom && !isSticky && !knownRoomRef) { - this.rooms.push(room); + // Check to see if the room is known first + let knownRoomRef = this.rooms.includes(room); + if (hasTags && !knownRoomRef) { + console.warn(`${room.roomId} might be a reference change - attempting to update reference`); + this.rooms = this.rooms.map(r => r.roomId === room.roomId ? room : r); + knownRoomRef = this.rooms.includes(room); + if (!knownRoomRef) { + console.warn(`${room.roomId} is still not referenced. It may be sticky.`); } } - let didTagChange = false; - if (cause === RoomUpdateCause.PossibleTagChange) { - const oldTags = this.roomIdsToTags[room.roomId] || []; - const newTags = this.getTagsForRoom(room); - const diff = arrayDiff(oldTags, newTags); - if (diff.removed.length > 0 || diff.added.length > 0) { - for (const rmTag of diff.removed) { - if (SettingsStore.getValue("advancedRoomListLogging")) { - // TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602 - console.log(`Removing ${room.roomId} from ${rmTag}`); - } - const algorithm: OrderingAlgorithm = this.algorithms[rmTag]; - if (!algorithm) throw new Error(`No algorithm for ${rmTag}`); - await algorithm.handleRoomUpdate(room, RoomUpdateCause.RoomRemoved); - this._cachedRooms[rmTag] = algorithm.orderedRooms; - this.recalculateFilteredRoomsForTag(rmTag); // update filter to re-sort the list - this.recalculateStickyRoom(rmTag); // update sticky room to make sure it moves if needed - } - for (const addTag of diff.added) { - if (SettingsStore.getValue("advancedRoomListLogging")) { - // TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602 - console.log(`Adding ${room.roomId} to ${addTag}`); - } - const algorithm: OrderingAlgorithm = this.algorithms[addTag]; - if (!algorithm) throw new Error(`No algorithm for ${addTag}`); - await algorithm.handleRoomUpdate(room, RoomUpdateCause.NewRoom); - this._cachedRooms[addTag] = algorithm.orderedRooms; - } + // If we have tags for a room and don't have the room referenced, something went horribly + // wrong - the reference should have been updated above. + if (hasTags && !knownRoomRef && !isSticky) { + throw new Error(`${room.roomId} is missing from room array but is known - trying to find duplicate`); + } - // Update the tag map so we don't regen it in a moment - this.roomIdsToTags[room.roomId] = newTags; + // Like above, update the reference to the sticky room if we need to + if (hasTags && isSticky) { + // Go directly in and set the sticky room's new reference, being careful not + // to trigger a sticky room update ourselves. + this._stickyRoom.room = room; + } + // If after all that we're still a NewRoom update, add the room if applicable. + // We don't do this for the sticky room (because it causes duplication issues) + // or if we know about the reference (as it should be replaced). + if (cause === RoomUpdateCause.NewRoom && !isSticky && !knownRoomRef) { + this.rooms.push(room); + } + } + + let didTagChange = false; + if (cause === RoomUpdateCause.PossibleTagChange) { + const oldTags = this.roomIdsToTags[room.roomId] || []; + const newTags = this.getTagsForRoom(room); + const diff = arrayDiff(oldTags, newTags); + if (diff.removed.length > 0 || diff.added.length > 0) { + for (const rmTag of diff.removed) { if (SettingsStore.getValue("advancedRoomListLogging")) { // TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602 - console.log(`Changing update cause for ${room.roomId} to Timeline to sort rooms`); + console.log(`Removing ${room.roomId} from ${rmTag}`); } - cause = RoomUpdateCause.Timeline; - didTagChange = true; + const algorithm: OrderingAlgorithm = this.algorithms[rmTag]; + if (!algorithm) throw new Error(`No algorithm for ${rmTag}`); + await algorithm.handleRoomUpdate(room, RoomUpdateCause.RoomRemoved); + this._cachedRooms[rmTag] = algorithm.orderedRooms; + this.recalculateFilteredRoomsForTag(rmTag); // update filter to re-sort the list + this.recalculateStickyRoom(rmTag); // update sticky room to make sure it moves if needed + } + for (const addTag of diff.added) { + if (SettingsStore.getValue("advancedRoomListLogging")) { + // TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602 + console.log(`Adding ${room.roomId} to ${addTag}`); + } + const algorithm: OrderingAlgorithm = this.algorithms[addTag]; + if (!algorithm) throw new Error(`No algorithm for ${addTag}`); + await algorithm.handleRoomUpdate(room, RoomUpdateCause.NewRoom); + this._cachedRooms[addTag] = algorithm.orderedRooms; + } + + // Update the tag map so we don't regen it in a moment + this.roomIdsToTags[room.roomId] = newTags; + + if (SettingsStore.getValue("advancedRoomListLogging")) { + // TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602 + console.log(`Changing update cause for ${room.roomId} to Timeline to sort rooms`); + } + cause = RoomUpdateCause.Timeline; + didTagChange = true; + } else { + if (SettingsStore.getValue("advancedRoomListLogging")) { + // TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602 + console.log(`Received no-op update for ${room.roomId} - changing to Timeline update`); + } + cause = RoomUpdateCause.Timeline; + } + + if (didTagChange && isSticky) { + // Manually update the tag for the sticky room without triggering a sticky room + // update. The update will be handled implicitly by the sticky room handling and + // requires no changes on our part, if we're in the middle of a sticky room change. + if (this._lastStickyRoom) { + this._stickyRoom = { + room, + tag: this.roomIdsToTags[room.roomId][0], + position: 0, // right at the top as it changed tags + }; } else { - if (SettingsStore.getValue("advancedRoomListLogging")) { - // TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602 - console.log(`Received no-op update for ${room.roomId} - changing to Timeline update`); - } - cause = RoomUpdateCause.Timeline; - } - - if (didTagChange && isSticky) { - // Manually update the tag for the sticky room without triggering a sticky room - // update. The update will be handled implicitly by the sticky room handling and - // requires no changes on our part, if we're in the middle of a sticky room change. - if (this._lastStickyRoom) { - this._stickyRoom = { - room, - tag: this.roomIdsToTags[room.roomId][0], - position: 0, // right at the top as it changed tags - }; - } else { - // We have to clear the lock as the sticky room change will trigger updates. - await this.setStickyRoom(room); - } + // We have to clear the lock as the sticky room change will trigger updates. + await this.setStickyRoom(room); } } + } - // If the update is for a room change which might be the sticky room, prevent it. We - // need to make sure that the causes (NewRoom and RoomRemoved) are still triggered though - // as the sticky room relies on this. - if (cause !== RoomUpdateCause.NewRoom && cause !== RoomUpdateCause.RoomRemoved) { - if (this.stickyRoom === room) { - if (SettingsStore.getValue("advancedRoomListLogging")) { - // TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602 - console.warn( - `[RoomListDebug] Received ${cause} update for sticky room ${room.roomId} - ignoring`, - ); - } - return false; - } - } - - if (!this.roomIdsToTags[room.roomId]) { - if (CAUSES_REQUIRING_ROOM.includes(cause)) { - if (SettingsStore.getValue("advancedRoomListLogging")) { - // TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602 - console.warn(`Skipping tag update for ${room.roomId} because we don't know about the room`); - } - return false; - } - + // If the update is for a room change which might be the sticky room, prevent it. We + // need to make sure that the causes (NewRoom and RoomRemoved) are still triggered though + // as the sticky room relies on this. + if (cause !== RoomUpdateCause.NewRoom && cause !== RoomUpdateCause.RoomRemoved) { + if (this.stickyRoom === room) { if (SettingsStore.getValue("advancedRoomListLogging")) { // TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602 - console.log(`[RoomListDebug] Updating tags for room ${room.roomId} (${room.name})`); + console.warn(`[RoomListDebug] Received ${cause} update for sticky room ${room.roomId} - ignoring`); } + return false; + } + } - // Get the tags for the room and populate the cache - const roomTags = this.getTagsForRoom(room).filter(t => !isNullOrUndefined(this.cachedRooms[t])); - - // "This should never happen" condition - we specify DefaultTagID.Untagged in getTagsForRoom(), - // which means we should *always* have a tag to go off of. - if (!roomTags.length) throw new Error(`Tags cannot be determined for ${room.roomId}`); - - this.roomIdsToTags[room.roomId] = roomTags; - + if (!this.roomIdsToTags[room.roomId]) { + if (CAUSES_REQUIRING_ROOM.includes(cause)) { if (SettingsStore.getValue("advancedRoomListLogging")) { // TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602 - console.log(`[RoomListDebug] Updated tags for ${room.roomId}:`, roomTags); + console.warn(`Skipping tag update for ${room.roomId} because we don't know about the room`); } - } - - if (SettingsStore.getValue("advancedRoomListLogging")) { - // TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602 - console.log(`[RoomListDebug] Reached algorithmic handling for ${room.roomId} and cause ${cause}`); - } - - const tags = this.roomIdsToTags[room.roomId]; - if (!tags) { - console.warn(`No tags known for "${room.name}" (${room.roomId})`); return false; } - let changed = didTagChange; - for (const tag of tags) { - const algorithm: OrderingAlgorithm = this.algorithms[tag]; - if (!algorithm) throw new Error(`No algorithm for ${tag}`); - - await algorithm.handleRoomUpdate(room, cause); - this._cachedRooms[tag] = algorithm.orderedRooms; - - // Flag that we've done something - this.recalculateFilteredRoomsForTag(tag); // update filter to re-sort the list - this.recalculateStickyRoom(tag); // update sticky room to make sure it appears if needed - changed = true; + if (SettingsStore.getValue("advancedRoomListLogging")) { + // TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602 + console.log(`[RoomListDebug] Updating tags for room ${room.roomId} (${room.name})`); } + // Get the tags for the room and populate the cache + const roomTags = this.getTagsForRoom(room).filter(t => !isNullOrUndefined(this.cachedRooms[t])); + + // "This should never happen" condition - we specify DefaultTagID.Untagged in getTagsForRoom(), + // which means we should *always* have a tag to go off of. + if (!roomTags.length) throw new Error(`Tags cannot be determined for ${room.roomId}`); + + this.roomIdsToTags[room.roomId] = roomTags; + if (SettingsStore.getValue("advancedRoomListLogging")) { // TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602 - console.log( - `[RoomListDebug] Finished handling ${room.roomId} with cause ${cause} (changed=${changed})`, - ); + console.log(`[RoomListDebug] Updated tags for ${room.roomId}:`, roomTags); } - return changed; - } finally { - release(); } + + if (SettingsStore.getValue("advancedRoomListLogging")) { + // TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602 + console.log(`[RoomListDebug] Reached algorithmic handling for ${room.roomId} and cause ${cause}`); + } + + const tags = this.roomIdsToTags[room.roomId]; + if (!tags) { + console.warn(`No tags known for "${room.name}" (${room.roomId})`); + return false; + } + + let changed = didTagChange; + for (const tag of tags) { + const algorithm: OrderingAlgorithm = this.algorithms[tag]; + if (!algorithm) throw new Error(`No algorithm for ${tag}`); + + await algorithm.handleRoomUpdate(room, cause); + this._cachedRooms[tag] = algorithm.orderedRooms; + + // Flag that we've done something + this.recalculateFilteredRoomsForTag(tag); // update filter to re-sort the list + this.recalculateStickyRoom(tag); // update sticky room to make sure it appears if needed + changed = true; + } + + if (SettingsStore.getValue("advancedRoomListLogging")) { + // TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602 + console.log(`[RoomListDebug] Finished handling ${room.roomId} with cause ${cause} (changed=${changed})`); + } + return changed; } } diff --git a/src/utils/MultiLock.ts b/src/utils/MultiLock.ts deleted file mode 100644 index 507a924dda..0000000000 --- a/src/utils/MultiLock.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* -Copyright 2021 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import { EnhancedMap } from "./maps"; -import AwaitLock from "await-lock"; - -export type DoneFn = () => void; - -export class MultiLock { - private locks = new EnhancedMap(); - - public async acquire(key: string): Promise { - const lock = this.locks.getOrCreate(key, new AwaitLock()); - await lock.acquireAsync(); - return () => lock.release(); - } -} From 0d8a7eabc7b7cf203d0c3bcdf78ed8583030eddb Mon Sep 17 00:00:00 2001 From: Jaiwanth Date: Wed, 19 May 2021 12:38:39 +0530 Subject: [PATCH 100/464] Update MemberList on invite permission change Signed-off-by: Jaiwanth --- src/components/views/rooms/MemberList.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/views/rooms/MemberList.js b/src/components/views/rooms/MemberList.js index fbc0e477a1..bb60c958f7 100644 --- a/src/components/views/rooms/MemberList.js +++ b/src/components/views/rooms/MemberList.js @@ -196,6 +196,9 @@ export default class MemberList extends React.Component { event.getType() === "m.room.third_party_invite") { this._updateList(); } + if (event.getContent().invite !== event.getPrevContent().invite) { + this.forceUpdate(); + } }; _updateList = rate_limited_func(() => { From e18120f4122a87e35b10907f9afb65e28758dcc0 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 19 May 2021 08:54:23 +0100 Subject: [PATCH 101/464] Show DMs in space for invited members too, to match Android impl --- src/stores/SpaceStore.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index ba2b91aa2c..05fe52aa2b 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -361,7 +361,8 @@ export class SpaceStoreClass extends AsyncStoreWithClient { const space = this.matrixClient?.getRoom(spaceId); // Add relevant DMs - space?.getJoinedMembers().forEach(member => { + space?.getMembers().forEach(member => { + if (member.membership !== "join" && member.membership !== "invite") return; DMRoomMap.shared().getDMRoomsForUserId(member.userId).forEach(roomId => { roomIds.add(roomId); }); 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 102/464] 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 103/464] 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 104/464] 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 105/464] 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 e78206301fbc1951791b1dbd41693e1feae60244 Mon Sep 17 00:00:00 2001 From: Jaiwanth Date: Wed, 19 May 2021 14:31:04 +0530 Subject: [PATCH 106/464] Modify to avoid forceUpdate --- src/components/views/rooms/MemberList.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/components/views/rooms/MemberList.js b/src/components/views/rooms/MemberList.js index bb60c958f7..f991827091 100644 --- a/src/components/views/rooms/MemberList.js +++ b/src/components/views/rooms/MemberList.js @@ -136,11 +136,13 @@ export default class MemberList extends React.Component { _getMembersState(members) { // set the state after determining _showPresence to make sure it's // taken into account while rerendering + const cli = MatrixClientPeg.get(); return { loading: false, members: members, filteredJoinedMembers: this._filterMembers(members, 'join'), filteredInvitedMembers: this._filterMembers(members, 'invite'), + canInvite: cli.getRoom(this.props.roomId).canInvite(cli.getUserId()), // ideally we'd size this to the page height, but // in practice I find that a little constraining @@ -196,9 +198,10 @@ export default class MemberList extends React.Component { event.getType() === "m.room.third_party_invite") { this._updateList(); } - if (event.getContent().invite !== event.getPrevContent().invite) { - this.forceUpdate(); - } + + const cli = MatrixClientPeg.get(); + const canInvite = cli.getRoom(this.props.roomId).canInvite(cli.getUserId()); + if (canInvite !== this.state.canInvite) this.setState({canInvite}); }; _updateList = rate_limited_func(() => { @@ -458,8 +461,6 @@ export default class MemberList extends React.Component { let inviteButton; if (room && room.getMyMembership() === 'join') { - const canInvite = room.canInvite(cli.getUserId()); - let inviteButtonText = _t("Invite to this room"); const chat = CommunityPrototypeStore.instance.getSelectedCommunityGeneralChat(); if (chat && chat.roomId === this.props.roomId) { @@ -470,7 +471,7 @@ export default class MemberList extends React.Component { const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); inviteButton = - + { inviteButtonText } ; } From f7d0afcd287d79a8fc673197facb055311c3f840 Mon Sep 17 00:00:00 2001 From: Germain Date: Wed, 19 May 2021 10:07:02 +0100 Subject: [PATCH 107/464] Performance monitoring measurements (#6041) --- src/@types/global.d.ts | 3 + src/components/structures/MatrixChat.tsx | 46 ++---- src/performance/entry-names.ts | 57 ++++++++ src/performance/index.ts | 178 +++++++++++++++++++++++ test/end-to-end-tests/.gitignore | 1 + test/end-to-end-tests/src/session.js | 2 +- test/end-to-end-tests/start.js | 20 ++- 7 files changed, 275 insertions(+), 32 deletions(-) create mode 100644 src/performance/entry-names.ts create mode 100644 src/performance/index.ts diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index f04a2ff237..63966d96fa 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 from "../performance"; declare global { interface Window { @@ -79,6 +80,8 @@ declare global { mxVoiceRecordingStore: VoiceRecordingStore; mxTypingStore: TypingStore; mxEventIndexPeg: EventIndexPeg; + mxPerformanceMonitor: PerformanceMonitor; + mxPerformanceEntryNames: any; } interface Document { diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 288acc108a..4c7fca2fec 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,22 @@ 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.instance.start(PerformanceEntryNames.PAGE_CHANGE); } stopPageChangeTimer() { - // Tor doesn't support performance - if (!performance || !performance.mark) return null; + const perfMonitor = PerformanceMonitor.instance; - 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(); + perfMonitor.stop(PerformanceEntryNames.PAGE_CHANGE); - // In practice, sometimes the entries list is empty, so we get no measurement - if (!measurement) return null; + const entries = perfMonitor.getEntries({ + name: PerformanceEntryNames.PAGE_CHANGE, + }); + const measurement = entries.pop(); - return measurement.duration; + return measurement + ? measurement.duration + : null; } shouldTrackPageChange(prevState: IState, state: IState) { @@ -1632,11 +1614,13 @@ export default class MatrixChat extends React.PureComponent { action: 'start_registration', params: params, }); + PerformanceMonitor.instance.start(PerformanceEntryNames.REGISTER); } else if (screen === 'login') { dis.dispatch({ action: 'start_login', params: params, }); + PerformanceMonitor.instance.start(PerformanceEntryNames.LOGIN); } else if (screen === 'forgot_password') { dis.dispatch({ action: 'start_password_recovery', @@ -1965,6 +1949,8 @@ export default class MatrixChat extends React.PureComponent { // Create and start the client await Lifecycle.setLoggedIn(credentials); await this.postLoginSetup(); + PerformanceMonitor.instance.stop(PerformanceEntryNames.LOGIN); + PerformanceMonitor.instance.stop(PerformanceEntryNames.REGISTER); }; // 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..bfb5b4a9c7 --- /dev/null +++ b/src/performance/index.ts @@ -0,0 +1,178 @@ +/* +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 { PerformanceEntryNames } from "./entry-names"; + +interface GetEntriesOptions { + name?: string, + type?: string, +} + +type PerformanceCallbackFunction = (entry: PerformanceEntry[]) => void; + +interface PerformanceDataListener { + entryNames?: string[], + callback: PerformanceCallbackFunction +} + +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} + */ + start(name: string, id?: string): void { + if (!this.supportsPerformanceApi()) { + return; + } + const key = this.buildKey(name, id); + + if (performance.getEntriesByName(this.START_PREFIX + key).length > 0) { + console.warn(`Recording already started for: ${name}`); + return; + } + + performance.mark(this.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} + */ + stop(name: string, id?: string): PerformanceEntry { + if (!this.supportsPerformanceApi()) { + return; + } + 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(this.STOP_PREFIX + key); + performance.measure( + key, + this.START_PREFIX + key, + this.STOP_PREFIX + key, + ); + + 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 + this.entries.push(measurement); + + this.listeners.forEach(listener => { + if (this.shouldEmit(listener, measurement)) { + listener.callback([measurement]) + } + }); + + return measurement; + } + + clear(name: string, id?: string): void { + if (!this.supportsPerformanceApi()) { + return; + } + const key = this.buildKey(name, id); + performance.clearMarks(this.START_PREFIX + key); + performance.clearMarks(this.STOP_PREFIX + key); + } + + 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; + }); + } + + addPerformanceDataCallback(listener: PerformanceDataListener, buffer = false) { + this.listeners.push(listener); + if (buffer) { + const toEmit = this.entries.filter(entry => this.shouldEmit(listener, entry)); + if (toEmit.length > 0) { + listener.callback(toEmit); + } + } + } + + removePerformanceDataCallback(callback?: PerformanceCallbackFunction) { + if (!callback) { + this.listeners = []; + } else { + 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}` : ''}`; + } +} + + +// Convenience exports +export { + PerformanceEntryNames, +} + +// Exposing those to the window object to bridge them from tests +window.mxPerformanceMonitor = PerformanceMonitor.instance; +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..f29b485c84 100644 --- a/test/end-to-end-tests/start.js +++ b/test/end-to-end-tests/start.js @@ -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 8a3db3b40ac466c17368ba24571f06820ac84db7 Mon Sep 17 00:00:00 2001 From: Nique Woodhouse Date: Wed, 19 May 2021 10:25:54 +0100 Subject: [PATCH 108/464] 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 9f4c75fd4f1e26b9cc916db46ec9200bd35ed3d4 Mon Sep 17 00:00:00 2001 From: jelv Date: Tue, 18 May 2021 19:05:16 +0000 Subject: [PATCH 109/464] Translated using Weblate (Dutch) Currently translated at 100.0% (2968 of 2968 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, 4 insertions(+), 4 deletions(-) diff --git a/src/i18n/strings/nl.json b/src/i18n/strings/nl.json index 94da2fccc1..867478453f 100644 --- a/src/i18n/strings/nl.json +++ b/src/i18n/strings/nl.json @@ -6,7 +6,7 @@ "Admin": "Beheerder", "Advanced": "Geavanceerd", "Always show message timestamps": "Altijd tijdstempels van berichten tonen", - "Authentication": "Authenticatie", + "Authentication": "Login bevestigen", "%(items)s and %(lastItem)s": "%(items)s en %(lastItem)s", "and %(count)s others...|other": "en %(count)s anderen…", "and %(count)s others...|one": "en één andere…", @@ -1689,7 +1689,7 @@ "wait and try again later": "wachten en het later weer proberen", "Use an Integration Manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "Gebruik een integratiebeheerder (%(serverName)s) om robots, widgets en stickerpakketten te beheren.", "Use an Integration Manager to manage bots, widgets, and sticker packs.": "Gebruik een integratiebeheerder om robots, widgets en stickerpakketten te beheren.", - "Manage integrations": "Beheer integraties", + "Manage integrations": "Integratiebeheerder", "Integration Managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Integratiebeheerders ontvangen configuratie-informatie en kunnen widgets aanpassen, gespreksuitnodigingen versturen en machtsniveau’s namens u aanpassen.", "Ban list rules - %(roomName)s": "Banlijstregels - %(roomName)s", "Server rules": "Serverregels", @@ -1884,7 +1884,7 @@ "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.": "Verifieer dit apparaat om het als vertrouwd te markeren. Door dit apparaat te vertrouwen geeft u extra gemoedsrust aan uzelf en andere gebruikers bij het gebruik van eind-tot-eind-versleutelde berichten.", "Verifying this device will mark it as trusted, and users who have verified with you will trust this device.": "Dit apparaat verifiëren zal het als vertrouwd markeren, en gebruikers die met u geverifieerd hebben zullen het vertrouwen.", "Integrations are disabled": "Integraties zijn uitgeschakeld", - "Enable 'Manage Integrations' in Settings to do this.": "Schakel in het Algemene Instellingenmenu ‘Beheer integraties’ in om dit te doen.", + "Enable 'Manage Integrations' in Settings to do this.": "Schakel de ‘Integratiebeheerder’ in in uw Instellingen om dit te doen.", "Integrations not allowed": "Integraties niet toegestaan", "Your %(brand)s doesn't allow you to use an Integration Manager to do this. Please contact an admin.": "Uw %(brand)s laat u geen integratiebeheerder gebruiken om dit te doen. Neem contact op met een beheerder.", "Failed to invite the following users to chat: %(csvUsers)s": "Het uitnodigen van volgende gebruikers voor gesprek is mislukt: %(csvUsers)s", @@ -2921,7 +2921,7 @@ "%(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 kan versleutelde berichten niet veilig lokaal opslaan in een webbrowser. Gebruik %(brand)s Desktop om versleutelde berichten in zoekresultaten te laten verschijnen.", "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|one": "Veilig lokaal opslaan van versleutelde berichten zodat ze in de zoekresultaten verschijnen, gebruik %(size)s voor het opslaan van berichten uit %(rooms)s gesprek.", "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|other": "Veilig lokaal opslaan van versleutelde berichten zodat ze in de zoekresultaten verschijnen, gebruik %(size)s voor het opslaan van berichten uit %(rooms)s gesprekken.", - "Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.": "Verifieer elke sessie die door een gebruiker wordt gebruikt afzonderlijk om deze te markeren als vertrouwd, niet vertrouwend op kruislings ondertekende apparaten.", + "Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.": "Verifieer elke sessie die door een gebruiker wordt gebruikt afzonderlijk. Dit markeert hem als vertrouwd zonder te vertrouwen op kruislings ondertekende apparaten.", "User signing private key:": "Gebruikerondertekening-privésleutel:", "Master private key:": "Hoofdprivésleutel:", "Self signing private key:": "Zelfondertekening-privésleutel:", From 5ea4f91654c57a16bcfb3c0a1b4746b799f1482f Mon Sep 17 00:00:00 2001 From: Hivaa Date: Wed, 19 May 2021 07:33:47 +0000 Subject: [PATCH 110/464] Translated using Weblate (Persian) Currently translated at 20.2% (602 of 2968 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 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/fa.json b/src/i18n/strings/fa.json index 7dcce06ff6..0e6a9116d2 100644 --- a/src/i18n/strings/fa.json +++ b/src/i18n/strings/fa.json @@ -634,5 +634,6 @@ "Whether you're using %(brand)s as an installed Progressive Web App": "این که آیا شما از%(brand)s به عنوان یک PWA استفاده می‌کنید یا نه", "Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "این که آیا از ویژگی 'breadcrumbs' (نمایه‌ی کاربری بالای فهرست اتاق‌ها) استفاده می‌کنید یا خیر", "Use an identity server to invite by email. Manage in Settings.": "برای دعوت از یک سرور هویت‌سنجی استفاده نمائید. می‌توانید این مورد را در تنظیمات پیکربندی نمائید.", - "Use an identity server to invite by email. Click continue to use the default identity server (%(defaultIdentityServerName)s) or manage in Settings.": "برای دعوت با استفاده از ایمیل از یک سرور هویت‌سنجی استفاده نمائید. جهت استفاده از سرور هویت‌سنجی پیش‌فرض (%(defaultIdentityServerName)s) بر روی ادامه کلیک کنید، وگرنه آن را در بخش تنظیمات پیکربندی نمائید." + "Use an identity server to invite by email. Click continue to use the default identity server (%(defaultIdentityServerName)s) or manage in Settings.": "برای دعوت با استفاده از ایمیل از یک سرور هویت‌سنجی استفاده نمائید. جهت استفاده از سرور هویت‌سنجی پیش‌فرض (%(defaultIdentityServerName)s) بر روی ادامه کلیک کنید، وگرنه آن را در بخش تنظیمات پیکربندی نمائید.", + "Joins room with given address": "به اتاق با آدرس داده‌شده بپیوندید" } From 575e27ea0e2c878235a998a2e79066046d001e97 Mon Sep 17 00:00:00 2001 From: Fake Mail Date: Tue, 18 May 2021 23:26:38 +0000 Subject: [PATCH 111/464] Translated using Weblate (Bulgarian) Currently translated at 87.6% (2601 of 2968 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/bg/ --- src/i18n/strings/bg.json | 70 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 68 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/bg.json b/src/i18n/strings/bg.json index 8c6ea60a8d..294d5a4979 100644 --- a/src/i18n/strings/bg.json +++ b/src/i18n/strings/bg.json @@ -728,7 +728,7 @@ "Enable them now": "Включете ги сега", "Toolbox": "Инструменти", "Collecting logs": "Събиране на логове", - "You must specify an event type!": "Трябва да укажате тип на събитието", + "You must specify an event type!": "Трябва да укажате тип на събитието!", "(HTTP status %(httpStatus)s)": "(HTTP статус %(httpStatus)s)", "All Rooms": "Във всички стаи", "Wednesday": "Сряда", @@ -2831,5 +2831,71 @@ "Åland Islands": "Оландски острови", "Afghanistan": "Афганистан", "United States": "Съединените щати", - "United Kingdom": "Обединеното кралство" + "United Kingdom": "Обединеното кралство", + "Manage & explore rooms": "Управление и откриване на стаи", + "Space options": "Опции на пространството", + "Workspace: ": "Работна област: ", + "Channel: ": "Канал: ", + "Spaces are a new way to group rooms and people. To join an existing space you'll need an invite.": "Пространствата са нов начин за групиране на стаи и хора. За да се присъедините към съществуващо пространство, се нуждаете от покана.", + "Open space for anyone, best for communities": "Открийте пространство за всеки, най-добро за общности", + "Add existing room": "Добави съществуваща стая", + "Leave space": "Напусни пространство", + "Invite with email or username": "Покани чрез имейл или потребителско име", + "Invite people": "Покани хора", + "Share invite link": "Сподели връзка с покана", + "Click to copy": "Натиснете за копиране", + "Delete": "Изтрий", + "Please enter a name for the space": "Моля, въведете име на пространството", + "Create a space": "Създаване на пространство", + "Add some details to help people recognise it.": "Добавете някои подробности, за да помогнете на хората да го разпознаят.", + "Invite only, best for yourself or teams": "Само с покана, най-добро за вас самият или отбори", + "Public": "Публично", + "Private": "Лично", + "You can change this later": "Можете да го промените по-късно", + "Your public space": "Вашето публично пространство", + "Your private space": "Вашето лично пространство", + "You can change these anytime.": "Можете да ги промените по всяко време.", + "Creating...": "Създаване...", + "Collapse space panel": "Свий панел с пространства", + "Expand space panel": "Разшири панел с пространства", + "unknown person": "", + "sends snowfall": "изпраща снеговалеж", + "Sends the given message with snowfall": "Изпраща даденото съобщение със снеговалеж", + "Sends the given message with fireworks": "Изпраща даденото съобщение с фойерверки", + "sends fireworks": "изпраща фойерверки", + "sends confetti": "изпраща конфети", + "Sends the given message with confetti": "Изпраща даденото съобщение с конфети", + "Show chat effects (animations when receiving e.g. confetti)": "Покажи чат ефектите (анимации при получаване, като например конфети)", + "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 за търсене", + "Your feedback will help make spaces better. The more detail you can go into, the better.": "Вашата обратна връзка ще направи пространствата по-добри. Kолкото повече изпаднете в подобробности, толкова по-добре.", + "Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.": "Бета версията е налична за уеб, десктоп и Android. Някои функции може да не са налични на вашия Home сървър.", + "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 ще се презареди с пуснати Пространства. Общностите и собствените етикети ще бъдат скрити.", + "If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "Ако напуснете, %(brand)s ще се презареди със изключени Пространства. Общностите и собствените етикети ще бъдат видими отново.", + "Beta available for web, desktop and Android. Thank you for trying the beta.": "Бета версията е налична за уеб, десктоп и Android. Благодарим ви, че изпробвахте бета версията.", + "Spaces are a new way to group rooms and people.": "Пространствата са нов начин за групиране на стаи и хора.", + "Spaces": "Пространства", + "Check your devices": "Проверете устройствата си", + "%(deviceId)s from %(ip)s": "%(deviceId)s от %(ip)s", + "This homeserver has been blocked by it's administrator.": "Този Home сървър е бил блокиран от администратора му.", + "Use app for a better experience": "Използвайте приложението за по-добра работа", + "Use app": "Използване на приложението", + "Element Web is experimental on mobile. For a better experience and the latest features, use our free native app.": "Element Web е експериментален на мобилни устройства. За по-добра работа и най-новите функции, използвайте нашето безплатно приложение.", + "Review to ensure your account is safe": "Прегледайте, за да уверите, че профилът ви е в безопастност", + "You have unverified logins": "Имате неверифицирани сесии", + "Share your public space": "Споделете публичното си място", + "Invite to %(spaceName)s": "Покани в %(spaceName)s", + "%(senderName)s has updated the widget layout": "%(senderName)s обнови оформлението на приспособлението", + "Sends the given message as a spoiler": "Изпраща даденото съобщение като спойлер", + "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.": "Вашият Home сървър отхвърли вашия опит за влизане. Това може да се дължи на неща, които просто отнемат твърде много време. Моля, опитайте отново. Ако това продължи, моля, свържете се със админитратора на вашия Home сървър.", + "Your homeserver was unreachable and was not able to log you in. Please try again. If this continues, please contact your homeserver administrator.": "Вашият Home сървър беше недостижим и не можа да ви впише. Моля, опитайте отново. Ако това продължи, моля, свържете се със админитратора на вашия Home сървър.", + "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.": "Помолихме браузъра да запомни кой Home сървър използвате за влизане, но за съжаление браузърът ви го е забравил. Отидете на страницата за влизане и опитайте отново.", + "Already in call": "Вече в разговор", + "You're already in a call with this person.": "Вече сте в разговор в този човек.", + "Too Many Calls": "Твърде много повиквания", + "Call failed because microphone could not be accessed. Check that a microphone is plugged in and set up correctly.": "Неуспешно повикване поради неуспешен достъп до микрофон. Проверете дали микрофонът е включен и настроен правилно." } From 78a8c9e10ecdbd6fadb6d9c949c32b9a6b185aab Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 19 May 2021 10:46:11 +0100 Subject: [PATCH 112/464] Fix issue when a room without a name or alias is marked as suggested --- src/components/views/rooms/RoomList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomList.tsx b/src/components/views/rooms/RoomList.tsx index fdf38784cf..b042414c85 100644 --- a/src/components/views/rooms/RoomList.tsx +++ b/src/components/views/rooms/RoomList.tsx @@ -428,7 +428,7 @@ export default class RoomList extends React.PureComponent { private renderSuggestedRooms(): ReactComponentElement[] { return this.state.suggestedRooms.map(room => { - const name = room.name || room.canonical_alias || room.aliases.pop() || _t("Empty room"); + const name = room.name || room.canonical_alias || room.aliases?.[0] || _t("Empty room"); const avatar = ( Date: Wed, 19 May 2021 15:01:05 +0530 Subject: [PATCH 113/464] Add getMember mock --- test/components/views/rooms/MemberList-test.js | 1 + test/test-utils.js | 1 + 2 files changed, 2 insertions(+) diff --git a/test/components/views/rooms/MemberList-test.js b/test/components/views/rooms/MemberList-test.js index 093e5588d0..50b40dea20 100644 --- a/test/components/views/rooms/MemberList-test.js +++ b/test/components/views/rooms/MemberList-test.js @@ -88,6 +88,7 @@ describe('MemberList', () => { }; memberListRoom.currentState = { members: {}, + getMember: jest.fn(), getStateEvents: (eventType, stateKey) => stateKey === undefined ? [] : null, // ignore 3pid invites }; for (const member of [...adminUsers, ...moderatorUsers, ...defaultUsers]) { diff --git a/test/test-utils.js b/test/test-utils.js index 953693a820..d7c960228c 100644 --- a/test/test-utils.js +++ b/test/test-utils.js @@ -245,6 +245,7 @@ export function mkStubRoom(roomId = null) { maySendMessage: jest.fn().mockReturnValue(true), currentState: { getStateEvents: jest.fn(), + getMember: jest.fn(), mayClientSendStateEvent: jest.fn().mockReturnValue(true), maySendStateEvent: jest.fn().mockReturnValue(true), maySendEvent: jest.fn().mockReturnValue(true), From 5daca41bfb049c12ecdb5edfb326f5c257287578 Mon Sep 17 00:00:00 2001 From: Nique Woodhouse Date: Wed, 19 May 2021 11:17:34 +0100 Subject: [PATCH 114/464] 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 115/464] 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 116/464] 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 117/464] 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 118/464] 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 88d3706c04fef7e23d997aa9b1548e3d02142549 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 19 May 2021 12:34:27 +0100 Subject: [PATCH 119/464] mock getMembers on mkStubRoom --- test/test-utils.js | 1 + 1 file changed, 1 insertion(+) diff --git a/test/test-utils.js b/test/test-utils.js index 953693a820..bad3547133 100644 --- a/test/test-utils.js +++ b/test/test-utils.js @@ -234,6 +234,7 @@ export function mkStubRoom(roomId = null) { }), getMembersWithMembership: jest.fn().mockReturnValue([]), getJoinedMembers: jest.fn().mockReturnValue([]), + getMembers: jest.fn().mockReturnValue([]), getPendingEvents: () => [], getLiveTimeline: () => stubTimeline, getUnfilteredTimelineSet: () => null, From 6e25e42e66d6d38eb487b6c8d4f0597528f5d893 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 19 May 2021 13:00:46 +0100 Subject: [PATCH 120/464] Show subspace rooms count even if it is 0 for consistency --- src/components/structures/SpaceRoomDirectory.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/SpaceRoomDirectory.tsx b/src/components/structures/SpaceRoomDirectory.tsx index c1e9027b1d..2881b6c3ea 100644 --- a/src/components/structures/SpaceRoomDirectory.tsx +++ b/src/components/structures/SpaceRoomDirectory.tsx @@ -152,7 +152,7 @@ const Tile: React.FC = ({ } let description = _t("%(count)s members", { count: room.num_joined_members }); - if (numChildRooms) { + if (numChildRooms !== undefined) { description += " · " + _t("%(count)s rooms", { count: numChildRooms }); } if (room.topic) { From d73eb0c70f23b4d54a928db6399d2a855a350654 Mon Sep 17 00:00:00 2001 From: Jaiwanth Date: Wed, 19 May 2021 17:46:10 +0530 Subject: [PATCH 121/464] Update MemberList.js --- src/components/views/rooms/MemberList.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/components/views/rooms/MemberList.js b/src/components/views/rooms/MemberList.js index f991827091..55fe650b7b 100644 --- a/src/components/views/rooms/MemberList.js +++ b/src/components/views/rooms/MemberList.js @@ -133,16 +133,21 @@ export default class MemberList extends React.Component { } } + get canInvite() { + const cli = MatrixClientPeg.get(); + const room = cli.getRoom(this.props.roomId); + return room && room.canInvite(cli.getUserId()); + } + _getMembersState(members) { // set the state after determining _showPresence to make sure it's // taken into account while rerendering - const cli = MatrixClientPeg.get(); return { loading: false, members: members, filteredJoinedMembers: this._filterMembers(members, 'join'), filteredInvitedMembers: this._filterMembers(members, 'invite'), - canInvite: cli.getRoom(this.props.roomId).canInvite(cli.getUserId()), + canInvite: this.canInvite, // ideally we'd size this to the page height, but // in practice I find that a little constraining @@ -199,9 +204,7 @@ export default class MemberList extends React.Component { this._updateList(); } - const cli = MatrixClientPeg.get(); - const canInvite = cli.getRoom(this.props.roomId).canInvite(cli.getUserId()); - if (canInvite !== this.state.canInvite) this.setState({canInvite}); + if (this.canInvite !== this.state.canInvite) this.setState({ canInvite: this.canInvite }); }; _updateList = rate_limited_func(() => { From 6170403c103f2fe9f6c4404c6c49f3cececf7299 Mon Sep 17 00:00:00 2001 From: Germain Date: Wed, 19 May 2021 13:25:52 +0100 Subject: [PATCH 122/464] Depile encrypted events to find the most suitable one for preview (#6056) --- src/components/views/rooms/RoomTile.tsx | 16 +++++++++------- src/stores/room-list/MessagePreviewStore.ts | 11 +++++++---- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/components/views/rooms/RoomTile.tsx b/src/components/views/rooms/RoomTile.tsx index edd644fd65..60368ce250 100644 --- a/src/components/views/rooms/RoomTile.tsx +++ b/src/components/views/rooms/RoomTile.tsx @@ -100,8 +100,10 @@ export default class RoomTile extends React.PureComponent { hasUnsentEvents: this.countUnsentEvents() > 0, // generatePreview() will return nothing if the user has previews disabled - messagePreview: this.generatePreview(), + messagePreview: "", }; + this.generatePreview(); + this.notificationState = RoomNotificationStateStore.instance.getRoomState(this.props.room); this.roomProps = EchoChamber.forRoom(this.props.room); if (this.props.resizeNotifier) { @@ -123,7 +125,7 @@ export default class RoomTile extends React.PureComponent { private onResize = () => { if (this.showMessagePreview && !this.state.messagePreview) { - this.setState({messagePreview: this.generatePreview()}); + this.generatePreview(); } }; @@ -147,7 +149,7 @@ export default class RoomTile extends React.PureComponent { public componentDidUpdate(prevProps: Readonly, prevState: Readonly) { if (prevProps.showMessagePreview !== this.props.showMessagePreview && this.showMessagePreview) { - this.setState({messagePreview: this.generatePreview()}); + this.generatePreview(); } if (prevProps.room?.roomId !== this.props.room?.roomId) { MessagePreviewStore.instance.off( @@ -236,17 +238,17 @@ export default class RoomTile extends React.PureComponent { private onRoomPreviewChanged = (room: Room) => { if (this.props.room && room.roomId === this.props.room.roomId) { - // generatePreview() will return nothing if the user has previews disabled - this.setState({messagePreview: this.generatePreview()}); + this.generatePreview(); } }; - private generatePreview(): string | null { + private async generatePreview() { if (!this.showMessagePreview) { return null; } - return MessagePreviewStore.instance.getPreviewForRoom(this.props.room, this.props.tag); + const messagePreview = await MessagePreviewStore.instance.getPreviewForRoom(this.props.room, this.props.tag); + this.setState({ messagePreview }); } private scrollIntoView = () => { diff --git a/src/stores/room-list/MessagePreviewStore.ts b/src/stores/room-list/MessagePreviewStore.ts index 1da0e661e8..10e5cf554e 100644 --- a/src/stores/room-list/MessagePreviewStore.ts +++ b/src/stores/room-list/MessagePreviewStore.ts @@ -94,10 +94,10 @@ export class MessagePreviewStore extends AsyncStoreWithClient { * @param inTagId The tag ID in which the room resides * @returns The preview, or null if none present. */ - public getPreviewForRoom(room: Room, inTagId: TagID): string { + public async getPreviewForRoom(room: Room, inTagId: TagID): Promise { if (!room) return null; // invalid room, just return nothing - if (!this.previews.has(room.roomId)) this.generatePreview(room, inTagId); + if (!this.previews.has(room.roomId)) await this.generatePreview(room, inTagId); const previews = this.previews.get(room.roomId); if (!previews) return null; @@ -108,7 +108,7 @@ export class MessagePreviewStore extends AsyncStoreWithClient { return previews.get(inTagId); } - private generatePreview(room: Room, tagId?: TagID) { + private async generatePreview(room: Room, tagId?: TagID) { const events = room.timeline; if (!events) return; // should only happen in tests @@ -130,6 +130,9 @@ export class MessagePreviewStore extends AsyncStoreWithClient { } const event = events[i]; + + await this.matrixClient.decryptEventIfNeeded(event); + const previewDef = PREVIEWS[event.getType()]; if (!previewDef) continue; if (previewDef.isState && isNullOrUndefined(event.getStateKey())) continue; @@ -174,7 +177,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 - this.generatePreview(this.matrixClient.getRoom(event.getRoomId()), TAG_ANY); + await this.generatePreview(this.matrixClient.getRoom(event.getRoomId()), TAG_ANY); } } } From 0a018500e223e7f791be50f9e72f468a61bfdbde Mon Sep 17 00:00:00 2001 From: Hivaa Date: Wed, 19 May 2021 10:30:22 +0000 Subject: [PATCH 123/464] Translated using Weblate (Persian) Currently translated at 21.0% (625 of 2968 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 | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/fa.json b/src/i18n/strings/fa.json index 0e6a9116d2..e248bc4c0f 100644 --- a/src/i18n/strings/fa.json +++ b/src/i18n/strings/fa.json @@ -635,5 +635,28 @@ "Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "این که آیا از ویژگی 'breadcrumbs' (نمایه‌ی کاربری بالای فهرست اتاق‌ها) استفاده می‌کنید یا خیر", "Use an identity server to invite by email. Manage in Settings.": "برای دعوت از یک سرور هویت‌سنجی استفاده نمائید. می‌توانید این مورد را در تنظیمات پیکربندی نمائید.", "Use an identity server to invite by email. Click continue to use the default identity server (%(defaultIdentityServerName)s) or manage in Settings.": "برای دعوت با استفاده از ایمیل از یک سرور هویت‌سنجی استفاده نمائید. جهت استفاده از سرور هویت‌سنجی پیش‌فرض (%(defaultIdentityServerName)s) بر روی ادامه کلیک کنید، وگرنه آن را در بخش تنظیمات پیکربندی نمائید.", - "Joins room with given address": "به اتاق با آدرس داده‌شده بپیوندید" + "Joins room with given address": "به اتاق با آدرس داده‌شده بپیوندید", + "WARNING: Session already verified, but keys do NOT MATCH!": "هشدار امنیتی: نشست پیش از این تائید شده، اما کلیدها مطابقت ندارد!", + "Session already verified!": "نشست پیش از این تائید شده‌است!", + "Unknown (user, session) pair:": "جفت (کاربر، نشست) ناشناخته:", + "Verifies a user, session, and pubkey tuple": "یک کاربر، نشست و عبارت کلید عمومی را تائید می‌کند", + "You cannot modify widgets in this room.": "شما امکان تغییر ویجت‌ها در این اتاق را ندارید.", + "Please supply a https:// or http:// widget URL": "لطفا نشانی یک ویجت را به پروتکل http:// یا https:// وارد کنید", + "Please supply a widget URL or embed code": "لطفا نشانی (URL) ویجت یا یک کد قابل جاسازی (embeded) وارد کنید", + "Adds a custom widget by URL to the room": "یک ویجت سفارشی را با استفاده از نشانی (URL) به اتاق اضافه می‌کند", + "Opens the Developer Tools dialog": "پنجره‌ی ابزار توسعه را باز می‌کند", + "Could not find user in room": "کاربر در اتاق یافت نشد", + "Command failed": "دستور موفقیت‌آمیز نبود", + "Define the power level of a user": "سطح قدرت یک کاربر را تعریف کنید", + "You are no longer ignoring %(userId)s": "شما دیگر کاربر %(userId)s را نادیده نمی‌گیرید", + "Unignored user": "کاربران نادیده گرفته‌نشده", + "Stops ignoring a user, showing their messages going forward": "توقف نادیده گرفتن یک کاربر، باعث می‌شود پیام‌های او به شما نمایش داده شود", + "You are now ignoring %(userId)s": "شما هم‌اکنون کاربر %(userId)s را نادیده گرفتید", + "Ignored user": "کاربران نادیده گرفته‌شده", + "Ignores a user, hiding their messages from you": "نادیده گرفتن یک کاربر، باعث می‌شود پیام‌های او به شما نمایش داده نشود", + "Unbans user with given ID": "رفع تحریم کاربر با شناسه‌ی مذکور", + "Bans user with given id": "تحریم کاربر با شناسه‌ی مذکور", + "Kicks user with given id": "اخراج کاربر با شناسه‌ی مذکور", + "Unrecognised room address:": "آدرس اتاق قابل تشخیص نیست:", + "Leave room": "ترک اتاق" } From cf8e49729acb733ff81f943831a6bdf39352f8b2 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Wed, 19 May 2021 14:32:49 +0100 Subject: [PATCH 124/464] 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 125/464] 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 126/464] 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 127/464] 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 abd290938af0efd358a9e143315a19dd378352d8 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 19 May 2021 17:12:25 +0100 Subject: [PATCH 128/464] Fix room name not wrapping in summary card --- res/css/views/right_panel/_RoomSummaryCard.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/res/css/views/right_panel/_RoomSummaryCard.scss b/res/css/views/right_panel/_RoomSummaryCard.scss index 36882f4e8b..dc7804d072 100644 --- a/res/css/views/right_panel/_RoomSummaryCard.scss +++ b/res/css/views/right_panel/_RoomSummaryCard.scss @@ -36,6 +36,7 @@ limitations under the License. -webkit-box-orient: vertical; overflow: hidden; text-overflow: ellipsis; + white-space: pre-wrap; } .mx_RoomSummaryCard_avatar { From f52dc9a3eaa515991fd1000fe689f5f70607b63b Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 19 May 2021 17:12:31 +0100 Subject: [PATCH 129/464] Fix room name not updating whilst summary card is open --- src/components/views/right_panel/RoomSummaryCard.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/components/views/right_panel/RoomSummaryCard.tsx b/src/components/views/right_panel/RoomSummaryCard.tsx index bbe61807ae..88928290f4 100644 --- a/src/components/views/right_panel/RoomSummaryCard.tsx +++ b/src/components/views/right_panel/RoomSummaryCard.tsx @@ -45,6 +45,7 @@ import {ChevronFace, ContextMenuTooltipButton, useContextMenu} from "../../struc 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"; interface IProps { room: Room; @@ -249,7 +250,13 @@ const RoomSummaryCard: React.FC = ({ room, onClose }) => { />
    -

    { room.name }

    + + { name => ( +

    + { name } +

    + )} +
    { alias }
    From 83224dc7b66b6d3b51d6d58568759611726b0d04 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Wed, 19 May 2021 13:32:27 -0400 Subject: [PATCH 130/464] 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 131/464] 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 132/464] 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 133/464] 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 134/464] 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 135/464] 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 136/464] 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 49c853a304b32ced6aa7bc5e74e07e23aa0d8dca Mon Sep 17 00:00:00 2001 From: Germain Date: Wed, 19 May 2021 11:27:32 +0100 Subject: [PATCH 137/464] 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 dbfba13297..d822b6a839 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 66ffaf945ffc67c6a18f4c111cb07465fca21ca8 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Thu, 20 May 2021 10:43:57 +0100 Subject: [PATCH 138/464] Cache normalized room name --- .../room-list/filters/NameFilterCondition.ts | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/src/stores/room-list/filters/NameFilterCondition.ts b/src/stores/room-list/filters/NameFilterCondition.ts index 8e63c23131..2a652a091f 100644 --- a/src/stores/room-list/filters/NameFilterCondition.ts +++ b/src/stores/room-list/filters/NameFilterCondition.ts @@ -17,7 +17,7 @@ limitations under the License. import { Room } from "matrix-js-sdk/src/models/room"; import { FILTER_CHANGED, FilterKind, IFilterCondition } from "./IFilterCondition"; import { EventEmitter } from "events"; -import { removeHiddenChars } from "matrix-js-sdk/src/utils"; +import { normalize } from "matrix-js-sdk/src/utils"; import { throttle } from "lodash"; /** @@ -65,17 +65,7 @@ export class NameFilterCondition extends EventEmitter implements IFilterConditio return this.matches(room.name); } - private normalize(val: string): string { - // Note: we have to match the filter with the removeHiddenChars() room name because the - // function strips spaces and other characters (M becomes RN for example, in lowercase). - return removeHiddenChars(val.toLowerCase()) - // Strip all punctuation - .replace(/[\\'!"#$%&()*+,\-./:;<=>?@[\]^_`{|}~\u2000-\u206f\u2e00-\u2e7f]/g, "") - // We also doubly convert to lowercase to work around oddities of the library. - .toLowerCase(); - } - - public matches(val: string): boolean { - return this.normalize(val).includes(this.normalize(this.search)); + public matches(normalizedName: string): boolean { + return normalizedName.includes(normalize(this.search)); } } From 83e2461155ccf9a197c6a4553d7920184884dab1 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Thu, 20 May 2021 10:57:20 +0100 Subject: [PATCH 139/464] call matches with normalized name --- src/stores/room-list/filters/NameFilterCondition.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stores/room-list/filters/NameFilterCondition.ts b/src/stores/room-list/filters/NameFilterCondition.ts index 2a652a091f..7ec91a3249 100644 --- a/src/stores/room-list/filters/NameFilterCondition.ts +++ b/src/stores/room-list/filters/NameFilterCondition.ts @@ -62,7 +62,7 @@ export class NameFilterCondition extends EventEmitter implements IFilterConditio if (!room.name) return false; // should realistically not happen: the js-sdk always calculates a name - return this.matches(room.name); + return this.matches(room.normalizedName); } public matches(normalizedName: string): boolean { From 422740f13b3da84164739a9700efc48854f49c17 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Thu, 20 May 2021 11:04:17 +0100 Subject: [PATCH 140/464] normalize displayName --- src/components/views/rooms/RoomSublist.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomSublist.tsx b/src/components/views/rooms/RoomSublist.tsx index dd80e9d40a..f9881d33ae 100644 --- a/src/components/views/rooms/RoomSublist.tsx +++ b/src/components/views/rooms/RoomSublist.tsx @@ -18,6 +18,7 @@ limitations under the License. import * as React from "react"; import { createRef, ReactComponentElement } from "react"; +import { normalize } from "matrix-js-sdk/src/utils"; import { Room } from "matrix-js-sdk/src/models/room"; import classNames from 'classnames'; import { RovingAccessibleButton, RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex"; @@ -259,7 +260,7 @@ export default class RoomSublist extends React.Component { const nameCondition = RoomListStore.instance.getFirstNameFilterCondition(); if (nameCondition) { stateUpdates.filteredExtraTiles = this.props.extraTiles - .filter(t => nameCondition.matches(t.props.displayName || "")); + .filter(t => nameCondition.matches(normalize(t.props.displayName || ""))); } else if (this.state.filteredExtraTiles) { stateUpdates.filteredExtraTiles = null; } From dab75f9b88fbf05edefd1f83efc8ced8596bc628 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 20 May 2021 12:20:53 +0100 Subject: [PATCH 141/464] Fix add reaction prompt showing even when user is not joined to room --- src/components/views/messages/ReactionsRow.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/views/messages/ReactionsRow.tsx b/src/components/views/messages/ReactionsRow.tsx index 8809302088..f5910473ff 100644 --- a/src/components/views/messages/ReactionsRow.tsx +++ b/src/components/views/messages/ReactionsRow.tsx @@ -198,7 +198,8 @@ export default class ReactionsRow extends React.PureComponent { const cli = this.context; let addReactionButton; - if (cli.getRoom(mxEvent.getRoomId()).currentState.maySendEvent(EventType.Reaction, cli.getUserId())) { + const room = cli.getRoom(mxEvent.getRoomId()); + if (room.getMyMembership() === "join" && room.currentState.maySendEvent(EventType.Reaction, cli.getUserId())) { addReactionButton = ; } From bee1a88f0b1dae37a177461dddfc0f0769836c52 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 20 May 2021 13:34:31 +0100 Subject: [PATCH 142/464] Reduce noise in tests This disables a common log message to cut down the test log size and make it easier to read messages specific to each test. --- src/languageHandler.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/languageHandler.tsx b/src/languageHandler.tsx index 4a3fb30886..26c89afec6 100644 --- a/src/languageHandler.tsx +++ b/src/languageHandler.tsx @@ -344,7 +344,10 @@ export function setLanguage(preferredLangs: string | string[]) { counterpart.registerTranslations(langToUse, langData); counterpart.setLocale(langToUse); SettingsStore.setValue("language", null, SettingLevel.DEVICE, langToUse); - console.log("set language to " + langToUse); + // Adds a lot of noise to test runs, so disable logging there. + if (process.env.NODE_ENV !== "test") { + console.log("set language to " + langToUse); + } // Set 'en' as fallback language: if (langToUse !== "en") { From d3623217068ca9d2bc8cb170ac1f9f8329b9de3a Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Thu, 20 May 2021 15:25:20 +0100 Subject: [PATCH 143/464] 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 144/464] 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( -
  • - - - -
  • , + + + , ); 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 145/464] 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 018a75ea1e75a791585f5f0b3ce1fe9b26d274de Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 20 May 2021 09:10:21 -0600 Subject: [PATCH 146/464] Upgrade showChatEffects to room-level setting exposure --- src/settings/Settings.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 577f23fa3a..37f493f427 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -730,7 +730,7 @@ export const SETTINGS: {[setting: string]: ISetting} = { default: Layout.Group, }, "showChatEffects": { - supportedLevels: LEVELS_ACCOUNT_SETTINGS, + supportedLevels: LEVELS_ROOM_SETTINGS_WITH_ROOM, displayName: _td("Show chat effects (animations when receiving e.g. confetti)"), default: true, controller: new ReducedMotionController(), From 9e55f2409214f97014143549997e5d445d9787ed Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Thu, 20 May 2021 16:11:33 +0100 Subject: [PATCH 147/464] 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 148/464] 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 149/464] 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 150/464] 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 151/464] 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 152/464] 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 153/464] 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 073127aa3cac6ff6837bcfc710f310c2e19f8a05 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 20 May 2021 18:47:12 +0100 Subject: [PATCH 154/464] Fix handling of via servers for suggested rooms --- .../structures/SpaceRoomDirectory.tsx | 6 ++--- src/components/views/rooms/RoomList.tsx | 9 +++---- src/stores/SpaceStore.tsx | 24 +++++++++++++++---- 3 files changed, 28 insertions(+), 11 deletions(-) diff --git a/src/components/structures/SpaceRoomDirectory.tsx b/src/components/structures/SpaceRoomDirectory.tsx index 2881b6c3ea..dde8dd8331 100644 --- a/src/components/structures/SpaceRoomDirectory.tsx +++ b/src/components/structures/SpaceRoomDirectory.tsx @@ -76,7 +76,7 @@ export interface ISpaceSummaryEvent { order?: string; suggested?: boolean; auto_join?: boolean; - via?: string; + via?: string[]; }; } /* eslint-enable camelcase */ @@ -356,9 +356,9 @@ export const useSpaceSummary = (cli: MatrixClient, space: Room, refreshToken?: a parentChildRelations.getOrCreate(ev.room_id, new Map()).set(ev.state_key, ev); childParentRelations.getOrCreate(ev.state_key, new Set()).add(ev.room_id); } - if (Array.isArray(ev.content["via"])) { + if (Array.isArray(ev.content.via)) { const set = viaMap.getOrCreate(ev.state_key, new Set()); - ev.content["via"].forEach(via => set.add(via)); + ev.content.via.forEach(via => set.add(via)); } }); diff --git a/src/components/views/rooms/RoomList.tsx b/src/components/views/rooms/RoomList.tsx index b042414c85..7b0dadeca5 100644 --- a/src/components/views/rooms/RoomList.tsx +++ b/src/components/views/rooms/RoomList.tsx @@ -46,11 +46,10 @@ import { IconizedContextMenuOption, IconizedContextMenuOptionList } from "../con import AccessibleButton from "../elements/AccessibleButton"; import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore"; import CallHandler from "../../../CallHandler"; -import SpaceStore, {SUGGESTED_ROOMS} from "../../../stores/SpaceStore"; +import SpaceStore, {ISuggestedRoom, SUGGESTED_ROOMS} from "../../../stores/SpaceStore"; import {showAddExistingRooms, showCreateNewRoom, showSpaceInvite} from "../../../utils/space"; import {replaceableComponent} from "../../../utils/replaceableComponent"; import RoomAvatar from "../avatars/RoomAvatar"; -import { ISpaceSummaryRoom } from "../../structures/SpaceRoomDirectory"; interface IProps { onKeyDown: (ev: React.KeyboardEvent) => void; @@ -66,7 +65,7 @@ interface IState { sublists: ITagMap; isNameFiltering: boolean; currentRoomId?: string; - suggestedRooms: ISpaceSummaryRoom[]; + suggestedRooms: ISuggestedRoom[]; } const TAG_ORDER: TagID[] = [ @@ -363,7 +362,7 @@ export default class RoomList extends React.PureComponent { return room; }; - private updateSuggestedRooms = (suggestedRooms: ISpaceSummaryRoom[]) => { + private updateSuggestedRooms = (suggestedRooms: ISuggestedRoom[]) => { this.setState({ suggestedRooms }); }; @@ -443,7 +442,9 @@ export default class RoomList extends React.PureComponent { const viewRoom = () => { defaultDispatcher.dispatch({ action: "view_room", + room_alias: room.canonical_alias || room.aliases?.[0], room_id: room.room_id, + via_servers: room.viaServers, oobData: { avatarUrl: room.avatar_url, name, diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index 05fe52aa2b..9488bf3cbd 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -45,6 +45,10 @@ export const UPDATE_INVITED_SPACES = Symbol("invited-spaces"); export const UPDATE_SELECTED_SPACE = Symbol("selected-space"); // Space Room ID will be emitted when a Space's children change +export interface ISuggestedRoom extends ISpaceSummaryRoom { + viaServers: string[]; +} + const MAX_SUGGESTED_ROOMS = 20; const getSpaceContextKey = (space?: Room) => `mx_space_context_${space?.roomId || "ALL_ROOMS"}`; @@ -89,7 +93,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { private spaceFilteredRooms = new Map>(); // The space currently selected in the Space Panel - if null then All Rooms is selected private _activeSpace?: Room = null; - private _suggestedRooms: ISpaceSummaryRoom[] = []; + private _suggestedRooms: ISuggestedRoom[] = []; private _invitedSpaces = new Set(); public get invitedSpaces(): Room[] { @@ -104,7 +108,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { return this._activeSpace || null; } - public get suggestedRooms(): ISpaceSummaryRoom[] { + public get suggestedRooms(): ISuggestedRoom[] { return this._suggestedRooms; } @@ -160,10 +164,22 @@ export class SpaceStoreClass extends AsyncStoreWithClient { if (space) { const data = await this.fetchSuggestedRooms(space); if (this._activeSpace === space) { + const viaMap = new EnhancedMap>(); + data.events.forEach(ev => { + if (ev.type === EventType.SpaceChild && ev.content.via?.length) { + ev.content.via.forEach(via => { + viaMap.getOrCreate(ev.state_key, new Set()).add(via); + }); + } + }); + this._suggestedRooms = data.rooms.filter(roomInfo => { return roomInfo.room_type !== RoomType.Space && this.matrixClient.getRoom(roomInfo.room_id)?.getMyMembership() !== "join"; - }); + }).map(roomInfo => ({ + ...roomInfo, + viaServers: Array.from(viaMap.get(roomInfo.room_id) || []), + })); this.emit(SUGGESTED_ROOMS, this._suggestedRooms); } } @@ -222,7 +238,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { return room?.currentState.getStateEvents(EventType.SpaceParent) .filter(ev => { const content = ev.getContent(); - if (!content?.via) return false; + if (!content?.via?.length) return false; // TODO apply permissions check to verify that the parent mapping is valid if (canonicalOnly && !content?.canonical) return false; return true; From e984a4f0cdac4b9bfbfb06a68dbc1d9cb07bf766 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 20 May 2021 20:12:28 +0100 Subject: [PATCH 155/464] rejig the code to make types happy --- src/components/structures/SpaceRoomView.tsx | 6 ++- src/stores/SpaceStore.tsx | 44 ++++++++++----------- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx index 56ee9dd62a..ea4c75e25c 100644 --- a/src/components/structures/SpaceRoomView.tsx +++ b/src/components/structures/SpaceRoomView.tsx @@ -806,7 +806,7 @@ export default class SpaceRoomView extends React.PureComponent { let suggestedRooms = SpaceStore.instance.suggestedRooms; if (SpaceStore.instance.activeSpace !== this.props.space) { // the space store has the suggested rooms loaded for a different space, fetch the right ones - suggestedRooms = (await SpaceStore.instance.fetchSuggestedRooms(this.props.space, 1)).rooms; + suggestedRooms = (await SpaceStore.instance.fetchSuggestedRooms(this.props.space, 1)); } if (suggestedRooms.length) { @@ -814,9 +814,11 @@ export default class SpaceRoomView extends React.PureComponent { defaultDispatcher.dispatch({ action: "view_room", room_id: room.room_id, + room_alias: room.canonical_alias || room.aliases?.[0], + via_servers: room.viaServers, oobData: { avatarUrl: room.avatar_url, - name: room.name || room.canonical_alias || room.aliases.pop() || _t("Empty room"), + name: room.name || room.canonical_alias || room.aliases?.[0] || _t("Empty room"), }, }); return; diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index 9488bf3cbd..40997d30a8 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -162,43 +162,41 @@ export class SpaceStoreClass extends AsyncStoreWithClient { } if (space) { - const data = await this.fetchSuggestedRooms(space); + const suggestedRooms = await this.fetchSuggestedRooms(space); if (this._activeSpace === space) { - const viaMap = new EnhancedMap>(); - data.events.forEach(ev => { - if (ev.type === EventType.SpaceChild && ev.content.via?.length) { - ev.content.via.forEach(via => { - viaMap.getOrCreate(ev.state_key, new Set()).add(via); - }); - } - }); - - this._suggestedRooms = data.rooms.filter(roomInfo => { - return roomInfo.room_type !== RoomType.Space - && this.matrixClient.getRoom(roomInfo.room_id)?.getMyMembership() !== "join"; - }).map(roomInfo => ({ - ...roomInfo, - viaServers: Array.from(viaMap.get(roomInfo.room_id) || []), - })); + this._suggestedRooms = suggestedRooms; this.emit(SUGGESTED_ROOMS, this._suggestedRooms); } } } - public fetchSuggestedRooms = async (space: Room, limit = MAX_SUGGESTED_ROOMS) => { + public fetchSuggestedRooms = async (space: Room, limit = MAX_SUGGESTED_ROOMS): Promise => { try { const data: { rooms: ISpaceSummaryRoom[]; events: ISpaceSummaryEvent[]; } = await this.matrixClient.getSpaceSummary(space.roomId, 0, true, false, limit); - return data; + + const viaMap = new EnhancedMap>(); + data.events.forEach(ev => { + if (ev.type === EventType.SpaceChild && ev.content.via?.length) { + ev.content.via.forEach(via => { + viaMap.getOrCreate(ev.state_key, new Set()).add(via); + }); + } + }); + + return data.rooms.filter(roomInfo => { + return roomInfo.room_type !== RoomType.Space + && this.matrixClient.getRoom(roomInfo.room_id)?.getMyMembership() !== "join"; + }).map(roomInfo => ({ + ...roomInfo, + viaServers: Array.from(viaMap.get(roomInfo.room_id) || []), + })); } catch (e) { console.error(e); } - return { - rooms: [], - events: [], - }; + return []; }; public addRoomToSpace(space: Room, roomId: string, via: string[], suggested = false, autoJoin = false) { From 332412782ed18b6fcd922df4334b4bbf48e82e47 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Thu, 20 May 2021 17:31:10 -0400 Subject: [PATCH 156/464] Remove logo spinner Removed since design wants to avoid associating slowness with the brand. Signed-off-by: Robin Townsend --- res/css/structures/_ContextualMenu.scss | 5 - res/img/logo-spinner.svg | 141 ------------------ .../views/elements/InlineSpinner.js | 23 +-- src/components/views/elements/Spinner.js | 40 ++--- src/components/views/right_panel/UserInfo.tsx | 2 +- src/i18n/strings/en_EN.json | 1 - src/settings/Settings.tsx | 6 - 7 files changed, 14 insertions(+), 204 deletions(-) delete mode 100644 res/img/logo-spinner.svg diff --git a/res/css/structures/_ContextualMenu.scss b/res/css/structures/_ContextualMenu.scss index 658033339a..4b33427a87 100644 --- a/res/css/structures/_ContextualMenu.scss +++ b/res/css/structures/_ContextualMenu.scss @@ -115,8 +115,3 @@ limitations under the License. border-top: 8px solid $menu-bg-color; border-right: 8px solid transparent; } - -.mx_ContextualMenu_spinner { - display: block; - margin: 0 auto; -} diff --git a/res/img/logo-spinner.svg b/res/img/logo-spinner.svg deleted file mode 100644 index 08965e982e..0000000000 --- a/res/img/logo-spinner.svg +++ /dev/null @@ -1,141 +0,0 @@ - - - start - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/components/views/elements/InlineSpinner.js b/src/components/views/elements/InlineSpinner.js index 757e809564..bbbe60d500 100644 --- a/src/components/views/elements/InlineSpinner.js +++ b/src/components/views/elements/InlineSpinner.js @@ -16,7 +16,6 @@ limitations under the License. import React from "react"; import {_t} from "../../../languageHandler"; -import SettingsStore from "../../../settings/SettingsStore"; import {replaceableComponent} from "../../../utils/replaceableComponent"; @replaceableComponent("views.elements.InlineSpinner") @@ -24,31 +23,15 @@ export default class InlineSpinner extends React.Component { render() { const w = this.props.w || 16; const h = this.props.h || 16; - const imgClass = this.props.imgClassName || ""; - let icon; - if (SettingsStore.getValue('feature_new_spinner')) { - icon = ( - - ); - } else { - icon = ( + return ( +
    - ); - } - - return ( -
    { icon }
    +
    ); } } diff --git a/src/components/views/elements/Spinner.js b/src/components/views/elements/Spinner.js index 43030d01d5..3ad8444bd6 100644 --- a/src/components/views/elements/Spinner.js +++ b/src/components/views/elements/Spinner.js @@ -18,41 +18,21 @@ limitations under the License. import React from "react"; import PropTypes from "prop-types"; import {_t} from "../../../languageHandler"; -import SettingsStore from "../../../settings/SettingsStore"; -const Spinner = ({w = 32, h = 32, imgClassName, message}) => { - let icon; - if (SettingsStore.getValue('feature_new_spinner')) { - icon = ( - - ); - } else { - icon = ( -
    - ); - } +const Spinner = ({w = 32, h = 32, message}) => ( +
    + { message &&
    { message }
     
    } +
    +
    +); - return ( -
    - { message &&
    { message }
     
    } - { icon } -
    - ); -}; Spinner.propTypes = { w: PropTypes.number, h: PropTypes.number, - imgClassName: PropTypes.string, message: PropTypes.node, }; diff --git a/src/components/views/right_panel/UserInfo.tsx b/src/components/views/right_panel/UserInfo.tsx index d5f67623a2..9798b282f6 100644 --- a/src/components/views/right_panel/UserInfo.tsx +++ b/src/components/views/right_panel/UserInfo.tsx @@ -1307,7 +1307,7 @@ const BasicUserInfo: React.FC<{ } if (pendingUpdateCount > 0) { - spinner = ; + spinner = ; } let memberDetails; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index d1fe791319..d6eef99dbf 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -793,7 +793,6 @@ "Send and receive voice messages": "Send and receive voice messages", "Render LaTeX maths in messages": "Render LaTeX maths in messages", "Communities v2 prototypes. Requires compatible homeserver. Highly experimental - use with caution.": "Communities v2 prototypes. Requires compatible homeserver. Highly experimental - use with caution.", - "New spinner design": "New spinner design", "Message Pinning": "Message Pinning", "Custom user status messages": "Custom user status messages", "Group & filter rooms by custom tags (refresh to apply changes)": "Group & filter rooms by custom tags (refresh to apply changes)", diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 37f493f427..6ff14c16b5 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -197,12 +197,6 @@ export const SETTINGS: {[setting: string]: ISetting} = { default: false, controller: new IncompatibleController("feature_spaces"), }, - "feature_new_spinner": { - isFeature: true, - displayName: _td("New spinner design"), - supportedLevels: LEVELS_FEATURE, - default: false, - }, "feature_pinning": { isFeature: true, displayName: _td("Message Pinning"), From 431b4607a4c7489e91f4886c6f5b8d8e9beeb855 Mon Sep 17 00:00:00 2001 From: c-cal Date: Thu, 20 May 2021 15:07:41 +0000 Subject: [PATCH 157/464] 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 158/464] 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 159/464] 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 160/464] 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 161/464] 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 162/464] 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 214/464] 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 215/464] 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 216/464] 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 217/464] 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 218/464] 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 219/464] 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 220/464] 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 221/464] 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 222/464] 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 223/464] 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 224/464] 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 225/464] 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 226/464] 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 227/464] 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 228/464] 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:57:24 +0100 Subject: [PATCH 229/464] 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 230/464] 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, 22 Feb 2021 17:43:15 +0100 Subject: [PATCH 231/464] 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 232/464] 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 233/464] 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 234/464] 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 235/464] 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 236/464] 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 237/464] 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 238/464] 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 239/464] 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 240/464] 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 241/464] 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 242/464] 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 243/464] 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 244/464] 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 245/464] 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 246/464] 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 247/464] 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 248/464] 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 249/464] 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 (

  • - { 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 250/464] 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 251/464] 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 252/464] 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 253/464] 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 254/464] 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 255/464] 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 256/464] 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 257/464] 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 258/464] 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 259/464] 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 260/464] 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 261/464] 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 262/464] 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 263/464] 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 264/464] 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 265/464] 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 266/464] 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 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 267/464] 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 268/464] 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 269/464] 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 270/464] 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 273/464] 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 274/464] 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 275/464] 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 276/464] 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 277/464] 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 278/464] 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 279/464] 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 280/464] 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 281/464] 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 284/464] 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 285/464] 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 286/464] 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 287/464] 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 288/464] 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 289/464] 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 290/464] 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 291/464] 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 292/464] 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 293/464] 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 294/464] 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 295/464] 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 296/464] 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 297/464] 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 298/464] 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 299/464] 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 300/464] 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 301/464] 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 302/464] 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 303/464] 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 304/464] 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 305/464] 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 306/464] 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 307/464] 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 308/464] 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 309/464] 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 310/464] 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 311/464] 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 312/464] 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 313/464] 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 314/464] 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 315/464] 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 316/464] 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 317/464] 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 318/464] 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 319/464] 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 320/464] 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 321/464] 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 322/464] 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 323/464] 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 324/464] 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 325/464] 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 326/464] 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 327/464] 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 328/464] 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 329/464] 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 330/464] 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 331/464] 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 332/464] 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 333/464] 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 334/464] 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 335/464] 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 f0f82107909a120c6d31b3a6f0bdef46a2b04ea2 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 1 Jun 2021 19:04:52 +0100 Subject: [PATCH 336/464] 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 337/464] 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 338/464] 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 339/464] 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 340/464] 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 341/464] 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 342/464] 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 343/464] 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 344/464] 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 345/464] 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 346/464] 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 347/464] 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 348/464] 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 349/464] 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 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 350/464] 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 2c4fa73a4571ff3da3ae55e60ae8facc7748006e Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 2 Jun 2021 17:39:13 +0100 Subject: [PATCH 351/464] 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 352/464] 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 353/464] 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 354/464] 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 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 355/464] 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 356/464] 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 357/464] 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 8ef95a62378a8fca563aa094ee897da06e05c438 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 3 Jun 2021 14:38:13 +0100 Subject: [PATCH 358/464] 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 359/464] 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 360/464] 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 361/464] 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 362/464] 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 363/464] 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 364/464] 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 365/464] 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 366/464] 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 367/464] 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 368/464] 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 369/464] 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 370/464] 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 0f64f4d692f36287ace222eb13e07244fa6e1c63 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Sat, 5 Jun 2021 10:49:44 -0400 Subject: [PATCH 371/464] 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 372/464] 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 373/464] 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 374/464] 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 375/464] 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 ea2120bdfdb6b75b25973fb89e63df5dc2a9b72b Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Sun, 6 Jun 2021 01:55:01 -0400 Subject: [PATCH 376/464] 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 377/464] 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 378/464] 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 379/464] 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 380/464] 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 381/464] 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 382/464] 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 383/464] 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 384/464] 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 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 385/464] 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 386/464] 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 ( -
    +
  • { ircTimestamp } { avatar } { sender } @@ -1129,7 +1133,7 @@ export default class EventTile extends React.Component { showUrlPreview={false} />
  • -
    + ); } 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 387/464] 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 388/464] 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 389/464] 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 390/464] 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 391/464] 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 392/464] 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 393/464] 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 394/464] 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 395/464] 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 396/464] 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 397/464] 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 ( -
    +
  • { children } -
  • + ); } From ea6904ce2a10e9aa368db43a900ea95fbb134911 Mon Sep 17 00:00:00 2001 From: Germain Date: Mon, 7 Jun 2021 12:37:11 +0100 Subject: [PATCH 398/464] 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 399/464] 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 400/464] 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 401/464] 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 402/464] 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 403/464] 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 404/464] 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 405/464] 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 406/464] 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 16:21:53 +0100 Subject: [PATCH 407/464] 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 296186b844d286062e0fe2e401320d267bb2717b Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Mon, 7 Jun 2021 16:21:53 +0100 Subject: [PATCH 408/464] 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 409/464] 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 410/464] 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 411/464] 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 412/464] 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 413/464] 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 414/464] 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 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 415/464] 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 416/464] 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 417/464] 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 418/464] 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 419/464] 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 a8dab04d5eed882f90244f0a5b1ec54cbfaaea58 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Mon, 7 Jun 2021 19:10:45 -0400 Subject: [PATCH 420/464] 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 421/464] 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 422/464] 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 423/464] 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 424/464] 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 425/464] 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 f84ee2276995a5abb42225fa443e8d777dddee65 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 8 Jun 2021 16:36:53 +0100 Subject: [PATCH 426/464] Add comment --- src/components/views/right_panel/PinnedMessagesCard.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/views/right_panel/PinnedMessagesCard.tsx b/src/components/views/right_panel/PinnedMessagesCard.tsx index 3a4dc9cc92..a72731522f 100644 --- a/src/components/views/right_panel/PinnedMessagesCard.tsx +++ b/src/components/views/right_panel/PinnedMessagesCard.tsx @@ -160,6 +160,7 @@ const PinnedMessagesCard = ({ room, onClose }: IProps) => { } else { content =
    + { /* XXX: We reuse the classes for simplicity, but deliberately not the components for non-interactivity. */ }
    From 93eb9feaa7931700323d8406a9ecd912c3f660d6 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 8 Jun 2021 16:42:58 +0100 Subject: [PATCH 427/464] iterate PR based on feedback --- src/components/views/dialogs/InviteDialog.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/views/dialogs/InviteDialog.tsx b/src/components/views/dialogs/InviteDialog.tsx index 557ea416a8..d7f1644d80 100644 --- a/src/components/views/dialogs/InviteDialog.tsx +++ b/src/components/views/dialogs/InviteDialog.tsx @@ -1249,12 +1249,12 @@ export default class InviteDialog extends React.PureComponent { + private onCopyClick = async e => { e.preventDefault(); const target = e.target; // copy target before we go async and React throws it away @@ -1348,7 +1348,7 @@ export default class InviteDialog extends React.PureComponent { _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.") }

    +

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

    ; const link = makeUserPermalink(MatrixClientPeg.get().getUserId()); footer =
    From 2b99afc741a4b4b9c71f9bc454cf1d433b93f564 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 8 Jun 2021 17:05:56 +0100 Subject: [PATCH 428/464] 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 2e8e2b39d4..a5107cba70 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2256,7 +2256,7 @@ "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 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.", + "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", From 90982f9b3fd1345c5af5e66a46986dd11e12dd2a Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 8 Jun 2021 17:31:08 +0100 Subject: [PATCH 429/464] Fix upgrade to element home button in top left menu --- src/components/structures/HostSignupAction.tsx | 5 ++++- src/components/structures/UserMenu.tsx | 4 +--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/components/structures/HostSignupAction.tsx b/src/components/structures/HostSignupAction.tsx index 769775d549..46e158d6c8 100644 --- a/src/components/structures/HostSignupAction.tsx +++ b/src/components/structures/HostSignupAction.tsx @@ -24,13 +24,16 @@ import { HostSignupStore } from "../../stores/HostSignupStore"; import SdkConfig from "../../SdkConfig"; import {replaceableComponent} from "../../utils/replaceableComponent"; -interface IProps {} +interface IProps { + onClick?(): void; +} interface IState {} @replaceableComponent("structures.HostSignupAction") export default class HostSignupAction extends React.PureComponent { private openDialog = async () => { + this.props.onClick?.(); await HostSignupStore.instance.setHostSignupActive(true); } diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx index fb4829f879..6a449cf1a2 100644 --- a/src/components/structures/UserMenu.tsx +++ b/src/components/structures/UserMenu.tsx @@ -366,9 +366,7 @@ export default class UserMenu extends React.Component { const mxDomain = MatrixClientPeg.get().getDomain(); const validDomains = hostSignupDomains.filter(d => (d === mxDomain || mxDomain.endsWith(`.${d}`))); if (!hostSignupConfig.domains || validDomains.length > 0) { - topSection =
    - -
    ; + topSection = ; } } } From 7155c033f23048acab1c864e758bfed2e3344b93 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 8 Jun 2021 19:20:10 +0000 Subject: [PATCH 430/464] Bump trim-newlines from 3.0.0 to 3.0.1 Bumps [trim-newlines](https://github.com/sindresorhus/trim-newlines) from 3.0.0 to 3.0.1. - [Release notes](https://github.com/sindresorhus/trim-newlines/releases) - [Commits](https://github.com/sindresorhus/trim-newlines/commits) --- updated-dependencies: - dependency-name: trim-newlines dependency-type: indirect ... Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 97179550c9..21243f4f58 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7969,9 +7969,9 @@ tree-kill@^1.2.2: integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== trim-newlines@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.0.tgz#79726304a6a898aa8373427298d54c2ee8b1cb30" - integrity sha512-C4+gOpvmxaSMKuEf9Qc134F1ZuOHVXKRbtEflf4NTtuuJDEIJ9p5PXsalL8SkeRw+qit1Mo+yuvMPAKwWg/1hA== + version "3.0.1" + resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.1.tgz#260a5d962d8b752425b32f3a7db0dcacd176c144" + integrity sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw== trough@^1.0.0: version "1.0.5" From dbb8aa47dcbaf0558607e705727c9c8e86bc12a3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 8 Jun 2021 20:08:39 +0000 Subject: [PATCH 431/464] Bump css-what from 5.0.0 to 5.0.1 Bumps [css-what](https://github.com/fb55/css-what) from 5.0.0 to 5.0.1. - [Release notes](https://github.com/fb55/css-what/releases) - [Commits](https://github.com/fb55/css-what/compare/v5.0.0...v5.0.1) --- updated-dependencies: - dependency-name: css-what dependency-type: indirect ... Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 21243f4f58..7c12ac51d1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2721,9 +2721,9 @@ css-select@^4.1.2: nth-check "^2.0.0" css-what@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/css-what/-/css-what-5.0.0.tgz#f0bf4f8bac07582722346ab243f6a35b512cfc47" - integrity sha512-qxyKHQvgKwzwDWC/rGbT821eJalfupxYW2qbSJSAtdSTimsr/MlaGONoNLllaUPZWf8QnbcKM/kPVYUQuEKAFA== + version "5.0.1" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-5.0.1.tgz#3efa820131f4669a8ac2408f9c32e7c7de9f4cad" + integrity sha512-FYDTSHb/7KXsWICVsxdmiExPjCfRC4qRFBdVwv7Ax9hMnvMmEjP9RfxTEZ3qPZGmADDn2vAKSo9UcN1jKVYscg== cssesc@^3.0.0: version "3.0.0" From a28f5754c7cd86d7b6d795c8fcfc6a7468c8e1e6 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Tue, 8 Jun 2021 21:58:48 -0400 Subject: [PATCH 432/464] Convert TextForEvent to TypeScript Signed-off-by: Robin Townsend --- src/{TextForEvent.js => TextForEvent.ts} | 54 +++++++++++++----------- 1 file changed, 29 insertions(+), 25 deletions(-) rename src/{TextForEvent.js => TextForEvent.ts} (94%) diff --git a/src/TextForEvent.js b/src/TextForEvent.ts similarity index 94% rename from src/TextForEvent.js rename to src/TextForEvent.ts index 22ce0dd9cf..649c53664e 100644 --- a/src/TextForEvent.js +++ b/src/TextForEvent.ts @@ -25,7 +25,7 @@ import {WIDGET_LAYOUT_EVENT_TYPE} from "./stores/widgets/WidgetLayoutStore"; // 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) { +function textForMemberEvent(ev): () => string | null { // XXX: SYJS-16 "sender is sometimes null for join messages" const senderName = ev.sender ? ev.sender.name : ev.getSender(); const targetName = ev.target ? ev.target.name : ev.getStateKey(); @@ -107,7 +107,7 @@ function textForMemberEvent(ev) { } } -function textForTopicEvent(ev) { +function textForTopicEvent(ev): () => string | null { const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); return () => _t('%(senderDisplayName)s changed the topic to "%(topic)s".', { senderDisplayName, @@ -115,7 +115,7 @@ function textForTopicEvent(ev) { }); } -function textForRoomNameEvent(ev) { +function textForRoomNameEvent(ev): () => string | null { const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); if (!ev.getContent().name || ev.getContent().name.trim().length === 0) { @@ -134,12 +134,12 @@ function textForRoomNameEvent(ev) { }); } -function textForTombstoneEvent(ev) { +function textForTombstoneEvent(ev): () => string | null { const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); return () => _t('%(senderDisplayName)s upgraded this room.', {senderDisplayName}); } -function textForJoinRulesEvent(ev) { +function textForJoinRulesEvent(ev): () => string | null { const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); switch (ev.getContent().join_rule) { case "public": @@ -159,7 +159,7 @@ function textForJoinRulesEvent(ev) { } } -function textForGuestAccessEvent(ev) { +function textForGuestAccessEvent(ev): () => string | null { const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); switch (ev.getContent().guest_access) { case "can_join": @@ -175,7 +175,7 @@ function textForGuestAccessEvent(ev) { } } -function textForRelatedGroupsEvent(ev) { +function textForRelatedGroupsEvent(ev): () => string | null { const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); const groups = ev.getContent().groups || []; const prevGroups = ev.getPrevContent().groups || []; @@ -205,7 +205,7 @@ function textForRelatedGroupsEvent(ev) { } } -function textForServerACLEvent(ev) { +function textForServerACLEvent(ev): () => string | null { const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); const prevContent = ev.getPrevContent(); const current = ev.getContent(); @@ -235,7 +235,7 @@ function textForServerACLEvent(ev) { return getText; } -function textForMessageEvent(ev) { +function textForMessageEvent(ev): () => string | null { return () => { const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); let message = senderDisplayName + ': ' + ev.getContent().body; @@ -248,7 +248,7 @@ function textForMessageEvent(ev) { }; } -function textForCanonicalAliasEvent(ev) { +function textForCanonicalAliasEvent(ev): () => string | null { const senderName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); const oldAlias = ev.getPrevContent().alias; const oldAltAliases = ev.getPrevContent().alt_aliases || []; @@ -299,7 +299,7 @@ function textForCanonicalAliasEvent(ev) { }); } -function textForCallAnswerEvent(event) { +function textForCallAnswerEvent(event): () => string | null { return () => { const senderName = event.sender ? event.sender.name : _t('Someone'); const supported = MatrixClientPeg.get().supportsVoip() ? '' : _t('(not supported by this browser)'); @@ -307,7 +307,7 @@ function textForCallAnswerEvent(event) { }; } -function textForCallHangupEvent(event) { +function textForCallHangupEvent(event): () => string | null { const getSenderName = () => event.sender ? event.sender.name : _t('Someone'); const eventContent = event.getContent(); let getReason = () => ""; @@ -344,14 +344,14 @@ function textForCallHangupEvent(event) { return () => _t('%(senderName)s ended the call.', {senderName: getSenderName()}) + ' ' + getReason(); } -function textForCallRejectEvent(event) { +function textForCallRejectEvent(event): () => string | null { return () => { const senderName = event.sender ? event.sender.name : _t('Someone'); return _t('%(senderName)s declined the call.', {senderName}); }; } -function textForCallInviteEvent(event) { +function textForCallInviteEvent(event): () => string | null { const getSenderName = () => event.sender ? event.sender.name : _t('Someone'); // FIXME: Find a better way to determine this from the event? let isVoice = true; @@ -383,7 +383,7 @@ function textForCallInviteEvent(event) { } } -function textForThreePidInviteEvent(event) { +function textForThreePidInviteEvent(event): () => string | null { const senderName = event.sender ? event.sender.name : event.getSender(); if (!isValid3pidInvite(event)) { @@ -399,7 +399,7 @@ function textForThreePidInviteEvent(event) { }); } -function textForHistoryVisibilityEvent(event) { +function textForHistoryVisibilityEvent(event): () => string | null { const senderName = event.sender ? event.sender.name : event.getSender(); switch (event.getContent().history_visibility) { case 'invited': @@ -421,7 +421,7 @@ function textForHistoryVisibilityEvent(event) { } // Currently will only display a change if a user's power level is changed -function textForPowerEvent(event) { +function textForPowerEvent(event): () => string | null { const senderName = event.sender ? event.sender.name : event.getSender(); if (!event.getPrevContent() || !event.getPrevContent().users || !event.getContent() || !event.getContent().users) { @@ -466,12 +466,12 @@ function textForPowerEvent(event) { }); } -function textForPinnedEvent(event) { +function textForPinnedEvent(event): () => string | null { const senderName = event.sender ? event.sender.name : event.getSender(); return () => _t("%(senderName)s changed the pinned messages for the room.", {senderName}); } -function textForWidgetEvent(event) { +function textForWidgetEvent(event): () => string | null { const senderName = event.getSender(); const {name: prevName, type: prevType, url: prevUrl} = event.getPrevContent(); const {name, type, url} = event.getContent() || {}; @@ -501,12 +501,12 @@ function textForWidgetEvent(event) { } } -function textForWidgetLayoutEvent(event) { +function textForWidgetLayoutEvent(event): () => string | null { const senderName = event.sender?.name || event.getSender(); return () => _t("%(senderName)s has updated the widget layout", {senderName}); } -function textForMjolnirEvent(event) { +function textForMjolnirEvent(event): () => string | null { const senderName = event.getSender(); const {entity: prevEntity} = event.getPrevContent(); const {entity, recommendation, reason} = event.getContent(); @@ -593,7 +593,11 @@ function textForMjolnirEvent(event) { "for %(reason)s", {senderName, oldGlob: prevEntity, newGlob: entity, reason}); } -const handlers = { +interface IHandlers { + [type: string]: (ev: any) => (() => string | null); +} + +const handlers: IHandlers = { 'm.room.message': textForMessageEvent, 'm.call.invite': textForCallInviteEvent, 'm.call.answer': textForCallAnswerEvent, @@ -601,7 +605,7 @@ const handlers = { 'm.call.reject': textForCallRejectEvent, }; -const stateHandlers = { +const stateHandlers: IHandlers = { 'm.room.canonical_alias': textForCanonicalAliasEvent, 'm.room.name': textForRoomNameEvent, 'm.room.topic': textForTopicEvent, @@ -626,12 +630,12 @@ for (const evType of ALL_RULE_TYPES) { stateHandlers[evType] = textForMjolnirEvent; } -export function hasText(ev) { +export function hasText(ev): boolean { const handler = (ev.isState() ? stateHandlers : handlers)[ev.getType()]; return Boolean(handler?.(ev)); } -export function textForEvent(ev) { +export function textForEvent(ev): string { const handler = (ev.isState() ? stateHandlers : handlers)[ev.getType()]; return handler?.(ev)?.() || ''; } From fccd71f82e9101ebbcab23d3ec04e6521958359f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 9 Jun 2021 06:23:51 +0200 Subject: [PATCH 433/464] Update styling based on review 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 | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/res/css/views/messages/_SenderProfile.scss b/res/css/views/messages/_SenderProfile.scss index 378d9e6f1a..7aaafa6ac3 100644 --- a/res/css/views/messages/_SenderProfile.scss +++ b/res/css/views/messages/_SenderProfile.scss @@ -20,8 +20,7 @@ limitations under the License. .mx_SenderProfile_mxid { font-weight: 600; - font-family: monospace; - font-size: 1rem; + font-size: 1.1rem; color: $event-timestamp-color; margin-left: 5px; } From c9b1d1aef06ece68109c46871bf633f823d41c7c Mon Sep 17 00:00:00 2001 From: Germain Date: Wed, 9 Jun 2021 10:38:28 +0100 Subject: [PATCH 434/464] Only run e2e tests on develop and PR targetting develop Co-authored-by: J. Ryan Stinnett --- .github/workflows/end-to-end-tests.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/end-to-end-tests.yml b/.github/workflows/end-to-end-tests.yml index 271ea06529..abf7813bbc 100644 --- a/.github/workflows/end-to-end-tests.yml +++ b/.github/workflows/end-to-end-tests.yml @@ -1,5 +1,9 @@ name: "end-to-end_tests" -on: [push] +on: + push: + branches: [develop] + pull_request: + branches: [develop] jobs: "end-to-end_tests": runs-on: ubuntu-latest From 85d1bb65c6046330c2fd732621d9e94ced82a072 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Wed, 9 Jun 2021 11:40:20 +0100 Subject: [PATCH 435/464] Upgrade to React@17 --- package.json | 8 +- yarn.lock | 288 +++++++++++++++++++++++++++++++++------------------ 2 files changed, 193 insertions(+), 103 deletions(-) diff --git a/package.json b/package.json index 13047b69cf..ab855b84be 100644 --- a/package.json +++ b/package.json @@ -90,9 +90,9 @@ "qrcode": "^1.4.4", "qs": "^6.9.6", "re-resizable": "^6.9.0", - "react": "^16.14.0", + "react": "^17.0.2", "react-beautiful-dnd": "^4.0.1", - "react-dom": "^16.14.0", + "react-dom": "^17.0.2", "react-focus-lock": "^2.5.0", "react-transition-group": "^4.4.1", "resize-observer-polyfill": "^1.5.1", @@ -147,7 +147,7 @@ "chokidar": "^3.5.1", "concurrently": "^5.3.0", "enzyme": "^3.11.0", - "enzyme-adapter-react-16": "^1.15.6", + "@wojtekmaj/enzyme-adapter-react-17": "^0.6.1", "eslint": "7.18.0", "eslint-config-matrix-org": "^0.2.0", "eslint-plugin-babel": "^5.3.1", @@ -162,7 +162,7 @@ "matrix-mock-request": "^1.2.3", "matrix-react-test-utils": "^0.2.2", "matrix-web-i18n": "github:matrix-org/matrix-web-i18n", - "react-test-renderer": "^16.14.0", + "react-test-renderer": "^17.0.2", "rimraf": "^3.0.2", "stylelint": "^13.9.0", "stylelint-config-standard": "^20.0.0", diff --git a/yarn.lock b/yarn.lock index 0ff235a660..32480a156f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1752,6 +1752,31 @@ "@typescript-eslint/types" "4.14.0" eslint-visitor-keys "^2.0.0" +"@wojtekmaj/enzyme-adapter-react-17@^0.6.1": + version "0.6.1" + resolved "https://registry.yarnpkg.com/@wojtekmaj/enzyme-adapter-react-17/-/enzyme-adapter-react-17-0.6.1.tgz#28caa37118c183e5f13c4dfb68cc32cde828ecbc" + integrity sha512-xgPfzLVpN0epIHeZofahwr5qwpukEDNAbrufgeDWN6vZPtfblGCC+OZG5TlfK+A6ePVy8sBkD8S2X4tO17JKjg== + dependencies: + "@wojtekmaj/enzyme-adapter-utils" "^0.1.0" + enzyme-shallow-equal "^1.0.0" + has "^1.0.0" + object.assign "^4.1.0" + object.values "^1.1.0" + prop-types "^15.7.0" + react-is "^17.0.0" + react-test-renderer "^17.0.0" + +"@wojtekmaj/enzyme-adapter-utils@^0.1.0": + version "0.1.0" + resolved "https://registry.yarnpkg.com/@wojtekmaj/enzyme-adapter-utils/-/enzyme-adapter-utils-0.1.0.tgz#3a2a3db756111d53357e2f119a1612a969ab8c38" + integrity sha512-EYK/Vy0Y1ap0jH2UNQjOKtR/7HWkbEq8N+cwC5+yDf+Mwp5uu7j4Qg70RmWuzsA35DGGwgkop6m4pQsGwNOF2A== + dependencies: + function.prototype.name "^1.1.0" + has "^1.0.0" + object.assign "^4.1.0" + object.fromentries "^2.0.0" + prop-types "^15.7.0" + abab@^2.0.3: version "2.0.5" resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.5.tgz#c0b678fb32d60fc1219c784d6a826fe385aeb79a" @@ -1780,21 +1805,6 @@ acorn@^7.1.1, acorn@^7.4.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== -airbnb-prop-types@^2.16.0: - version "2.16.0" - resolved "https://registry.yarnpkg.com/airbnb-prop-types/-/airbnb-prop-types-2.16.0.tgz#b96274cefa1abb14f623f804173ee97c13971dc2" - integrity sha512-7WHOFolP/6cS96PhKNrslCLMYAI8yB1Pp6u6XmxozQOiZbsI5ycglZr5cHhBFfuRcQQjzCMith5ZPZdYiJCxUg== - dependencies: - array.prototype.find "^2.1.1" - function.prototype.name "^1.1.2" - is-regex "^1.1.0" - object-is "^1.1.2" - object.assign "^4.1.0" - object.entries "^1.1.2" - prop-types "^15.7.2" - prop-types-exact "^1.2.0" - react-is "^16.13.1" - ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.3, ajv@^6.12.4: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" @@ -1920,14 +1930,6 @@ array-unique@^0.3.2: resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= -array.prototype.find@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/array.prototype.find/-/array.prototype.find-2.1.1.tgz#3baca26108ca7affb08db06bf0be6cb3115a969c" - integrity sha512-mi+MYNJYLTx2eNYy+Yh6raoQacCsNeeMUaspFPh9Y141lFSsWxxB8V9mM2ye+eqiRs917J6/pJ4M9ZPzenWckA== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.4" - array.prototype.flat@^1.2.3: version "1.2.4" resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.2.4.tgz#6ef638b43312bd401b4c6199fdec7e2dc9e9a123" @@ -3075,35 +3077,7 @@ entities@~2.0: resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.3.tgz#5c487e5742ab93c15abb5da22759b8590ec03b7f" integrity sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ== -enzyme-adapter-react-16@^1.15.6: - version "1.15.6" - resolved "https://registry.yarnpkg.com/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.15.6.tgz#fd677a658d62661ac5afd7f7f541f141f8085901" - integrity sha512-yFlVJCXh8T+mcQo8M6my9sPgeGzj85HSHi6Apgf1Cvq/7EL/J9+1JoJmJsRxZgyTvPMAqOEpRSu/Ii/ZpyOk0g== - dependencies: - enzyme-adapter-utils "^1.14.0" - enzyme-shallow-equal "^1.0.4" - has "^1.0.3" - object.assign "^4.1.2" - object.values "^1.1.2" - prop-types "^15.7.2" - react-is "^16.13.1" - react-test-renderer "^16.0.0-0" - semver "^5.7.0" - -enzyme-adapter-utils@^1.14.0: - version "1.14.0" - resolved "https://registry.yarnpkg.com/enzyme-adapter-utils/-/enzyme-adapter-utils-1.14.0.tgz#afbb0485e8033aa50c744efb5f5711e64fbf1ad0" - integrity sha512-F/z/7SeLt+reKFcb7597IThpDp0bmzcH1E9Oabqv+o01cID2/YInlqHbFl7HzWBl4h3OdZYedtwNDOmSKkk0bg== - dependencies: - airbnb-prop-types "^2.16.0" - function.prototype.name "^1.1.3" - has "^1.0.3" - object.assign "^4.1.2" - object.fromentries "^2.0.3" - prop-types "^15.7.2" - semver "^5.7.1" - -enzyme-shallow-equal@^1.0.1, enzyme-shallow-equal@^1.0.4: +enzyme-shallow-equal@^1.0.0, enzyme-shallow-equal@^1.0.1: version "1.0.4" resolved "https://registry.yarnpkg.com/enzyme-shallow-equal/-/enzyme-shallow-equal-1.0.4.tgz#b9256cb25a5f430f9bfe073a84808c1d74fced2e" integrity sha512-MttIwB8kKxypwHvRynuC3ahyNc+cFbR8mjVIltnmzQ0uKGqmsfO4bfBuLxb0beLNPhjblUEYvEbsg+VSygvF1Q== @@ -3146,7 +3120,7 @@ error-ex@^1.2.0, error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" -es-abstract@^1.17.0-next.1, es-abstract@^1.17.4: +es-abstract@^1.17.0-next.1: version "1.17.7" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.7.tgz#a4de61b2f66989fc7421676c1cb9787573ace54c" integrity sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g== @@ -3183,6 +3157,28 @@ es-abstract@^1.18.0-next.1: string.prototype.trimend "^1.0.3" string.prototype.trimstart "^1.0.3" +es-abstract@^1.18.0-next.2, es-abstract@^1.18.2: + version "1.18.3" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.3.tgz#25c4c3380a27aa203c44b2b685bba94da31b63e0" + integrity sha512-nQIr12dxV7SSxE6r6f1l3DtAeEYdsGpps13dR0TwJg1S8gyp4ZPgy3FZcHBgbiQqnoqSTb+oC+kO4UQ0C/J8vw== + dependencies: + call-bind "^1.0.2" + es-to-primitive "^1.2.1" + function-bind "^1.1.1" + get-intrinsic "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.2" + is-callable "^1.2.3" + is-negative-zero "^2.0.1" + is-regex "^1.1.3" + is-string "^1.0.6" + object-inspect "^1.10.3" + object-keys "^1.1.1" + object.assign "^4.1.2" + string.prototype.trimend "^1.0.4" + string.prototype.trimstart "^1.0.4" + unbox-primitive "^1.0.1" + es-get-iterator@^1.0.1: version "1.1.1" resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.1.tgz#b93ddd867af16d5118e00881396533c1c6647ad9" @@ -3965,7 +3961,17 @@ function-bind@^1.1.1: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== -function.prototype.name@^1.1.2, function.prototype.name@^1.1.3: +function.prototype.name@^1.1.0: + version "1.1.4" + resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.4.tgz#e4ea839b9d3672ae99d0efd9f38d9191c5eaac83" + integrity sha512-iqy1pIotY/RmhdFZygSSlW0wko2yxkSCKqsuv4pr8QESohpYyG/Z7B/XXvPRKTJS//960rgguE5mSRUsDdaJrQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + es-abstract "^1.18.0-next.2" + functions-have-names "^1.2.2" + +function.prototype.name@^1.1.2: version "1.1.3" resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.3.tgz#0bb034bb308e7682826f215eb6b2ae64918847fe" integrity sha512-H51qkbNSp8mtkJt+nyW1gyStBiKZxfRqySNUR99ylq6BPXHKI4SEvIlTKp4odLfjRKJV04DFWMU3G/YRlQOsag== @@ -3980,7 +3986,7 @@ functional-red-black-tree@^1.0.1: resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= -functions-have-names@^1.2.0, functions-have-names@^1.2.1: +functions-have-names@^1.2.0, functions-have-names@^1.2.1, functions-have-names@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.2.tgz#98d93991c39da9361f8e50b337c4f6e41f120e21" integrity sha512-bLgc3asbWdwPbx2mNk2S49kmJCuQeu0nfmaOgbs8WIyzzkw3r4htszdIi9Q9EMezDPTYuJx2wvjZ/EwgAthpnA== @@ -4004,6 +4010,15 @@ get-intrinsic@^1.0.1, get-intrinsic@^1.0.2: has "^1.0.3" has-symbols "^1.0.1" +get-intrinsic@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.1.tgz#15f59f376f855c446963948f0d24cd3637b4abc6" + integrity sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q== + dependencies: + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.1" + get-package-type@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" @@ -4157,6 +4172,11 @@ hard-rejection@^2.1.0: resolved "https://registry.yarnpkg.com/hard-rejection/-/hard-rejection-2.1.0.tgz#1c6eda5c1685c63942766d79bb40ae773cecd883" integrity sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA== +has-bigints@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.1.tgz#64fe6acb020673e3b78db035a5af69aa9d07b113" + integrity sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA== + has-flag@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" @@ -4172,6 +4192,11 @@ has-symbols@^1.0.1: resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8" integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg== +has-symbols@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.2.tgz#165d3070c00309752a1236a479331e3ac56f1423" + integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw== + has-value@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f" @@ -4203,7 +4228,7 @@ has-values@^1.0.0: is-number "^3.0.0" kind-of "^4.0.0" -has@^1.0.1, has@^1.0.3: +has@^1.0.0, has@^1.0.1, has@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== @@ -4534,6 +4559,11 @@ is-callable@^1.0.4, is-callable@^1.1.4, is-callable@^1.1.5, is-callable@^1.2.2: resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.2.tgz#c7c6715cd22d4ddb48d3e19970223aceabb080d9" integrity sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA== +is-callable@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.3.tgz#8b1e0500b73a1d76c70487636f368e519de8db8e" + integrity sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ== + is-ci@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c" @@ -4736,13 +4766,21 @@ is-potential-custom-element-name@^1.0.0: resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.0.tgz#0c52e54bcca391bb2c494b21e8626d7336c6e397" integrity sha1-DFLlS8yjkbssSUsh6GJtczbG45c= -is-regex@^1.0.3, is-regex@^1.0.4, is-regex@^1.0.5, is-regex@^1.1.0, is-regex@^1.1.1: +is-regex@^1.0.3, is-regex@^1.0.4, is-regex@^1.0.5, is-regex@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.1.tgz#c6f98aacc546f6cec5468a07b7b153ab564a57b9" integrity sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg== dependencies: has-symbols "^1.0.1" +is-regex@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.3.tgz#d029f9aff6448b93ebbe3f33dac71511fdcbef9f" + integrity sha512-qSVXFz28HM7y+IWX6vLCsexdlvzT1PJNFSBuaQLQ5o0IEw8UDYW6/2+eCMVyIsbM8CNLX2a/QWmSpyxYEHY7CQ== + dependencies: + call-bind "^1.0.2" + has-symbols "^1.0.2" + is-regexp@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-regexp/-/is-regexp-2.1.0.tgz#cd734a56864e23b956bf4e7c66c396a4c0b22c2d" @@ -4768,6 +4806,11 @@ is-string@^1.0.4, is-string@^1.0.5: resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.5.tgz#40493ed198ef3ff477b8c7f92f644ec82a5cd3a6" integrity sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ== +is-string@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.6.tgz#3fe5d5992fb0d93404f32584d4b0179a71b54a5f" + integrity sha512-2gdzbKUuqtQ3lYNrUTQYoClPhm7oQu4UdpSZMp1/DGgkHBT8E2Z1l0yMdb6D4zNAxwDiMv8MdulKROJGNl0Q0w== + is-subset@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/is-subset/-/is-subset-0.1.1.tgz#8a59117d932de1de00f245fcdd39ce43f1e939a6" @@ -6081,6 +6124,11 @@ object-inspect@^1.1.0, object-inspect@^1.7.0, object-inspect@^1.8.0, object-insp resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.9.0.tgz#c90521d74e1127b67266ded3394ad6116986533a" integrity sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw== +object-inspect@^1.10.3: + version "1.10.3" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.10.3.tgz#c2aa7d2d09f50c99375704f7a0adf24c5782d369" + integrity sha512-e5mCJlSH7poANfC8z8S9s9S2IN5/4Zb3aZ33f5s8YqoazCFzNLloLU8r5VCG+G7WoqLvAAZoVMcy3tp/3X0Plw== + object-is@^1.0.2, object-is@^1.1.2: version "1.1.4" resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.4.tgz#63d6c83c00a43f4cbc9434eb9757c8a5b8565068" @@ -6121,7 +6169,17 @@ object.entries@^1.1.0, object.entries@^1.1.1, object.entries@^1.1.2: es-abstract "^1.18.0-next.1" has "^1.0.3" -object.fromentries@^2.0.2, object.fromentries@^2.0.3: +object.fromentries@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.4.tgz#26e1ba5c4571c5c6f0890cef4473066456a120b8" + integrity sha512-EsFBshs5RUUpQEY1D4q/m59kMfz4YJvxuNCJcv/jWwOJr34EaVnG11ZrZa0UHB3wnzV1wx8m58T4hQL8IuNXlQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + es-abstract "^1.18.0-next.2" + has "^1.0.3" + +object.fromentries@^2.0.2: version "2.0.3" resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.3.tgz#13cefcffa702dc67750314a3305e8cb3fad1d072" integrity sha512-IDUSMXs6LOSJBWE++L0lzIbSqHl9KDCfff2x/JSEIDtEUavUnyMYC2ZGay/04Zq4UT8lvd4xNhU4/YHKibAOlw== @@ -6138,7 +6196,16 @@ object.pick@^1.3.0: dependencies: isobject "^3.0.1" -object.values@^1.1.1, object.values@^1.1.2: +object.values@^1.1.0: + version "1.1.4" + resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.4.tgz#0d273762833e816b693a637d30073e7051535b30" + integrity sha512-TnGo7j4XSnKQoK3MfvkzqKCi0nVe/D9I9IjwTNYdb/fxYHpjrluHVOgw0AF6jrRFGMPHdfuidR09tIDiIvnaSg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + es-abstract "^1.18.2" + +object.values@^1.1.1: version "1.1.2" resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.2.tgz#7a2015e06fcb0f546bd652486ce8583a4731c731" integrity sha512-MYC0jvJopr8EK6dPBiO8Nb9mvjdypOachO5REGk6MXzujbBrAisKo3HmdEI6kZDL6fC31Mwee/5YbtMebixeag== @@ -6588,16 +6655,7 @@ prompts@^2.0.1: kleur "^3.0.3" sisteransi "^1.0.5" -prop-types-exact@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/prop-types-exact/-/prop-types-exact-1.2.0.tgz#825d6be46094663848237e3925a98c6e944e9869" - integrity sha512-K+Tk3Kd9V0odiXFP9fwDHUYRyvK3Nun3GVyPapSIs5OBkITAm15W0CPFD/YKTkMUAbc0b9CUwRQp2ybiBIq+eA== - dependencies: - has "^1.0.3" - 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.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.0, 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== @@ -6729,15 +6787,14 @@ react-clientside-effect@^1.2.2: dependencies: "@babel/runtime" "^7.0.0" -react-dom@^16.14.0: - version "16.14.0" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.14.0.tgz#7ad838ec29a777fb3c75c3a190f661cf92ab8b89" - integrity sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw== +react-dom@^17.0.2: + version "17.0.2" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23" + integrity sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA== dependencies: loose-envify "^1.1.0" object-assign "^4.1.1" - prop-types "^15.6.2" - scheduler "^0.19.1" + scheduler "^0.20.2" react-focus-lock@^2.5.0: version "2.5.0" @@ -6751,7 +6808,12 @@ 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.12.0 || ^17.0.0", react-is@^17.0.0, react-is@^17.0.2: + version "17.0.2" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" + integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== + +react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== @@ -6788,15 +6850,23 @@ react-redux@^5.0.6: 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" - integrity sha512-L8yPjqPE5CZO6rKsKXRO/rVPiaCOy0tQQJbC+UjPNlobl5mad59lvPjwFsQHTvL03caVDIVr9x9/OSgDe6I5Eg== +react-shallow-renderer@^16.13.1: + version "16.14.1" + resolved "https://registry.yarnpkg.com/react-shallow-renderer/-/react-shallow-renderer-16.14.1.tgz#bf0d02df8a519a558fd9b8215442efa5c840e124" + integrity sha512-rkIMcQi01/+kxiTE9D3fdS959U1g7gs+/rborw++42m1O9FAQiNI/UNRZExVUoAOprn4umcXf+pFRou8i4zuBg== dependencies: object-assign "^4.1.1" - prop-types "^15.6.2" - react-is "^16.8.6" - scheduler "^0.19.1" + react-is "^16.12.0 || ^17.0.0" + +react-test-renderer@^17.0.0, react-test-renderer@^17.0.2: + version "17.0.2" + resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-17.0.2.tgz#4cd4ae5ef1ad5670fc0ef776e8cc7e1231d9866c" + integrity sha512-yaQ9cB89c17PUb0x6UfWRs7kQCorVdHlutU1boVPEsB8IDZH6n9tHxMacc3y0JoXOJUsZb/t/Mb8FUWMKaM7iQ== + dependencies: + object-assign "^4.1.1" + react-is "^17.0.2" + react-shallow-renderer "^16.13.1" + scheduler "^0.20.2" react-transition-group@^4.4.1: version "4.4.1" @@ -6808,14 +6878,13 @@ react-transition-group@^4.4.1: loose-envify "^1.4.0" prop-types "^15.6.2" -react@^16.14.0: - version "16.14.0" - resolved "https://registry.yarnpkg.com/react/-/react-16.14.0.tgz#94d776ddd0aaa37da3eda8fc5b6b18a4c9a3114d" - integrity sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g== +react@^17.0.2: + version "17.0.2" + resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037" + integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA== dependencies: loose-envify "^1.1.0" object-assign "^4.1.1" - prop-types "^15.6.2" read-pkg-up@^2.0.0: version "2.0.0" @@ -6923,11 +6992,6 @@ redux@^3.7.2: 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" - integrity sha1-dJrO7H8/34tj+SegSAnpDFwLNGA= - regenerate-unicode-properties@^8.2.0: version "8.2.0" resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.2.0.tgz#e5de7111d655e7ba60c057dbe9ff37c87e65cdec" @@ -7266,15 +7330,15 @@ saxes@^5.0.0: dependencies: xmlchars "^2.2.0" -scheduler@^0.19.1: - version "0.19.1" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.19.1.tgz#4f3e2ed2c1a7d65681f4c854fa8c5a1ccb40f196" - integrity sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA== +scheduler@^0.20.2: + version "0.20.2" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.2.tgz#4baee39436e34aa93b4874bddcbf0fe8b8b50e91" + integrity sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ== dependencies: loose-envify "^1.1.0" object-assign "^4.1.1" -"semver@2 || 3 || 4 || 5", semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0, semver@^5.7.0, semver@^5.7.1: +"semver@2 || 3 || 4 || 5", semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== @@ -7622,6 +7686,14 @@ string.prototype.trimend@^1.0.1, string.prototype.trimend@^1.0.3: call-bind "^1.0.0" define-properties "^1.1.3" +string.prototype.trimend@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz#e75ae90c2942c63504686c18b287b4a0b1a45f80" + integrity sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + string.prototype.trimstart@^1.0.1, string.prototype.trimstart@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.3.tgz#9b4cb590e123bb36564401d59824298de50fd5aa" @@ -7630,6 +7702,14 @@ string.prototype.trimstart@^1.0.1, string.prototype.trimstart@^1.0.3: call-bind "^1.0.0" define-properties "^1.1.3" +string.prototype.trimstart@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz#b36399af4ab2999b4c9c648bd7a3fb2bb26feeed" + integrity sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + string_decoder@^1.1.1: version "1.3.0" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" @@ -8078,6 +8158,16 @@ ua-parser-js@^0.7.18: resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.28.tgz#8ba04e653f35ce210239c64661685bf9121dec31" integrity sha512-6Gurc1n//gjp9eQNXjD9O3M/sMwVtN5S8Lv9bvOYBfKfDNiIIhqiyi01vMBO45u4zkDE420w/e0se7Vs+sIg+g== +unbox-primitive@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.1.tgz#085e215625ec3162574dc8859abee78a59b14471" + integrity sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw== + dependencies: + function-bind "^1.1.1" + has-bigints "^1.0.1" + has-symbols "^1.0.2" + which-boxed-primitive "^1.0.2" + unhomoglyph@^1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/unhomoglyph/-/unhomoglyph-1.0.6.tgz#ea41f926d0fcf598e3b8bb2980c2ddac66b081d3" @@ -8352,7 +8442,7 @@ whatwg-url@^8.0.0: tr46 "^2.0.2" webidl-conversions "^6.1.0" -which-boxed-primitive@^1.0.1: +which-boxed-primitive@^1.0.1, which-boxed-primitive@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg== From d492ee4d8acbf0156492cbd97e3f2f3e8153a234 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Wed, 9 Jun 2021 11:57:29 +0100 Subject: [PATCH 436/464] Update Enzyme adapter name --- test/accessibility/RovingTabIndex-test.js | 2 +- .../structures/MessagePanel-test.js | 32 +++++++++---------- .../views/messages/TextualBody-test.js | 2 +- .../views/rooms/SendMessageComposer-test.js | 2 +- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/test/accessibility/RovingTabIndex-test.js b/test/accessibility/RovingTabIndex-test.js index 5aa93f99f3..7af28043e0 100644 --- a/test/accessibility/RovingTabIndex-test.js +++ b/test/accessibility/RovingTabIndex-test.js @@ -16,7 +16,7 @@ limitations under the License. import '../skinned-sdk'; // Must be first for skinning to work import React from "react"; -import Adapter from "enzyme-adapter-react-16"; +import Adapter from "@wojtekmaj/enzyme-adapter-react-17"; import { configure, mount } from "enzyme"; import { diff --git a/test/components/structures/MessagePanel-test.js b/test/components/structures/MessagePanel-test.js index 5b466b4bb0..9426998160 100644 --- a/test/components/structures/MessagePanel-test.js +++ b/test/components/structures/MessagePanel-test.js @@ -32,7 +32,7 @@ import Matrix from 'matrix-js-sdk'; const test_utils = require('../../test-utils'); const mockclock = require('../../mock-clock'); -import Adapter from "enzyme-adapter-react-16"; +import Adapter from "@wojtekmaj/enzyme-adapter-react-17"; import { configure, mount } from "enzyme"; import MatrixClientContext from "../../../src/contexts/MatrixClientContext"; @@ -77,7 +77,7 @@ describe('MessagePanel', function() { DMRoomMap.makeShared(); }); - afterEach(function () { + afterEach(function() { clock.uninstall(); }); @@ -270,7 +270,7 @@ describe('MessagePanel', function() { it('should show the events', function() { const res = TestUtils.renderIntoDocument( - , + , ); // just check we have the right number of tiles for now @@ -298,8 +298,8 @@ describe('MessagePanel', function() { it('should insert the read-marker in the right place', function() { const res = TestUtils.renderIntoDocument( - , + , ); const tiles = TestUtils.scryRenderedComponentsWithType( @@ -316,8 +316,8 @@ describe('MessagePanel', function() { it('should show the read-marker that fall in summarised events after the summary', function() { const melsEvents = mkMelsEvents(); const res = TestUtils.renderIntoDocument( - , + , ); const summary = TestUtils.findRenderedDOMComponentWithClass(res, 'mx_EventListSummary'); @@ -334,8 +334,8 @@ describe('MessagePanel', function() { it('should hide the read-marker at the end of summarised events', function() { const melsEvents = mkMelsEventsOnly(); const res = TestUtils.renderIntoDocument( - , + , ); const summary = TestUtils.findRenderedDOMComponentWithClass(res, 'mx_EventListSummary'); @@ -358,9 +358,9 @@ describe('MessagePanel', function() { // first render with the RM in one place let mp = ReactDOM.render( - , parentDiv); + , parentDiv); const tiles = TestUtils.scryRenderedComponentsWithType( mp, sdk.getComponent('rooms.EventTile')); @@ -374,9 +374,9 @@ describe('MessagePanel', function() { // now move the RM mp = ReactDOM.render( - , parentDiv); + , parentDiv); // now there should be two RM containers const found = TestUtils.scryRenderedDOMComponentsWithClass(mp, 'mx_RoomView_myReadMarker_container'); @@ -451,7 +451,7 @@ describe('MessagePanel', function() { expect(isReadMarkerVisible(rm)).toBeFalsy(); }); - it('should render Date separators for the events', function () { + it('should render Date separators for the events', function() { const events = mkOneDayEvents(); const res = mount( Date: Wed, 9 Jun 2021 12:17:12 +0100 Subject: [PATCH 437/464] rename workflow --- .github/workflows/{end-to-end-tests.yml => develop.yml} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename .github/workflows/{end-to-end-tests.yml => develop.yml} (96%) diff --git a/.github/workflows/end-to-end-tests.yml b/.github/workflows/develop.yml similarity index 96% rename from .github/workflows/end-to-end-tests.yml rename to .github/workflows/develop.yml index abf7813bbc..346dcbaaba 100644 --- a/.github/workflows/end-to-end-tests.yml +++ b/.github/workflows/develop.yml @@ -1,11 +1,11 @@ -name: "end-to-end_tests" +name: Develop jobs on: push: branches: [develop] pull_request: branches: [develop] jobs: - "end-to-end_tests": + end-to-end: runs-on: ubuntu-latest defaults: run: From 4e14c612b78eec426ce88e4b9f880f9535d66365 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Wed, 9 Jun 2021 12:17:49 +0100 Subject: [PATCH 438/464] Remove extraenous instructions --- .github/workflows/develop.yml | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/.github/workflows/develop.yml b/.github/workflows/develop.yml index 346dcbaaba..3f82e61280 100644 --- a/.github/workflows/develop.yml +++ b/.github/workflows/develop.yml @@ -7,16 +7,7 @@ on: jobs: end-to-end: 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 }} + container: vectorim/element-web-ci-e2etests-env:latest steps: - name: Checkout code uses: actions/checkout@v2 From 15132074e2bd899e55da8be97650e129a05a5b64 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Wed, 9 Jun 2021 13:44:31 +0100 Subject: [PATCH 439/464] Restore copy button icon when sharing permalink --- res/css/views/dialogs/_ShareDialog.scss | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/res/css/views/dialogs/_ShareDialog.scss b/res/css/views/dialogs/_ShareDialog.scss index ce3fdd021f..4d5e1409db 100644 --- a/res/css/views/dialogs/_ShareDialog.scss +++ b/res/css/views/dialogs/_ShareDialog.scss @@ -50,7 +50,8 @@ limitations under the License. margin-left: 20px; display: inherit; } -.mx_ShareDialog_matrixto_copy > div { +.mx_ShareDialog_matrixto_copy::after { + content: ""; mask-image: url($copy-button-url); background-color: $message-action-bar-fg-color; margin-left: 5px; From ceed6ecbe87f743ddd87252a673b16fd6a7bf96a Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Wed, 9 Jun 2021 14:03:31 +0100 Subject: [PATCH 440/464] Restore Page Up/Down key bindings when focusing the composer --- src/components/structures/RoomView.tsx | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 463b71922f..fe90d2f873 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -1669,6 +1669,24 @@ 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 927de02a8e8e7a86eafeac59aecc779fecb9e986 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Wed, 9 Jun 2021 14:59:55 +0100 Subject: [PATCH 441/464] Revert refreshStickyHeaders optimisations --- src/components/structures/LeftPanel.tsx | 1 + src/components/views/rooms/RoomList.tsx | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/structures/LeftPanel.tsx b/src/components/structures/LeftPanel.tsx index 5b6b9c3717..af22db1350 100644 --- a/src/components/structures/LeftPanel.tsx +++ b/src/components/structures/LeftPanel.tsx @@ -439,6 +439,7 @@ export default class LeftPanel extends React.Component { onBlur={this.onBlur} isMinimized={this.props.isMinimized} activeSpace={this.state.activeSpace} + onResize={this.refreshStickyHeaders} onListCollapse={this.refreshStickyHeaders} />; diff --git a/src/components/views/rooms/RoomList.tsx b/src/components/views/rooms/RoomList.tsx index 8b36341ed0..5a1c3a24b3 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; + onResize: () => void; onListCollapse?: (isExpanded: boolean) => void; resizeNotifier: ResizeNotifier; isMinimized: boolean; @@ -404,7 +405,9 @@ 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.setState({sublists, isNameFiltering}, () => { + this.props.onResize(); + }); } }; From b43315c6c1c555ebce7a032dcc2f7950e999ed83 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Wed, 9 Jun 2021 15:46:10 +0100 Subject: [PATCH 442/464] Restore read receipt animation from event to event This restores expected read receipt animation by always including the positioned parent of read receipts. I imagine there's something smarter we could be doing, but for now, at least least get back to expected behaviour. Fixes https://github.com/vector-im/element-web/issues/17561 --- src/components/views/rooms/EventTile.tsx | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 9e9d7ef53d..354e84bcae 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -644,7 +644,18 @@ 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 null; + // We currently must include `mx_EventTile_readAvatars` in the DOM + // of all events, as it is the positioned parent of the animated + // read receipts. We can't let it unmount when a receipt moves + // events, so for now we mount it for all events. With out it, the + // animation will start from the top of the timeline (because it + // lost its container). + // See also https://github.com/vector-im/element-web/issues/17561 + return ( +
    + +
    + ); } const ReadReceiptMarker = sdk.getComponent('rooms.ReadReceiptMarker'); From e566704bdf1baf5abc81601401d6f00422117923 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Wed, 9 Jun 2021 15:48:30 +0100 Subject: [PATCH 443/464] Remove redundant early return --- src/components/views/rooms/EventTile.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 354e84bcae..48118444a5 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -663,11 +663,7 @@ export default class EventTile extends React.Component { const receiptOffset = 15; let left = 0; - const receipts = this.props.readReceipts || []; - - if (receipts.length === 0) { - return null; - } + const receipts = this.props.readReceipts; for (let i = 0; i < receipts.length; ++i) { const receipt = receipts[i]; From c6972c4535c09e914a7aaa2c980c5e2f3fe78eb3 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Wed, 9 Jun 2021 15:53:38 +0100 Subject: [PATCH 444/464] Fix spelling --- 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 48118444a5..0861f7fd5d 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -647,7 +647,7 @@ export default class EventTile extends React.Component { // We currently must include `mx_EventTile_readAvatars` in the DOM // of all events, as it is the positioned parent of the animated // read receipts. We can't let it unmount when a receipt moves - // events, so for now we mount it for all events. With out it, the + // events, so for now we mount it for all events. Without it, the // animation will start from the top of the timeline (because it // lost its container). // See also https://github.com/vector-im/element-web/issues/17561 From bd8fd246c67a4b79f7374e1cad18001080ff4741 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 9 Jun 2021 16:04:03 +0100 Subject: [PATCH 445/464] Add logging for which rooms calls are in --- src/CallHandler.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx index 0d87451b5f..bf7cb3473d 100644 --- a/src/CallHandler.tsx +++ b/src/CallHandler.tsx @@ -542,6 +542,7 @@ export default class CallHandler extends EventEmitter { if (newMappedRoomId !== mappedRoomId) { this.removeCallForRoom(mappedRoomId); mappedRoomId = newMappedRoomId; + console.log("Moving call to room " + mappedRoomId); this.calls.set(mappedRoomId, call); this.emit(CallHandlerEvent.CallChangeRoom, call); } @@ -607,6 +608,7 @@ export default class CallHandler extends EventEmitter { } private removeCallForRoom(roomId: string) { + console.log("Removing call for room ", roomId); this.calls.delete(roomId); this.emit(CallHandlerEvent.CallsChanged, this.calls); } @@ -680,6 +682,7 @@ export default class CallHandler extends EventEmitter { console.log("Current turn creds expire in " + timeUntilTurnCresExpire + " ms"); const call = MatrixClientPeg.get().createCall(mappedRoomId); + console.log("Adding call for room ", roomId); this.calls.set(roomId, call); this.emit(CallHandlerEvent.CallsChanged, this.calls); if (transferee) { @@ -812,6 +815,7 @@ export default class CallHandler extends EventEmitter { } Analytics.trackEvent('voip', 'receiveCall', 'type', call.type); + console.log("Adding call for room ", mappedRoomId); this.calls.set(mappedRoomId, call) this.emit(CallHandlerEvent.CallsChanged, this.calls); this.setCallListeners(call); From b4cb275ca1e3e5b5d7d068b8cacfed85ee375ba7 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Wed, 9 Jun 2021 17:59:41 +0100 Subject: [PATCH 446/464] Fix expanding last collapsed sticky session when zoomed in --- src/components/views/rooms/RoomSublist.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/RoomSublist.tsx b/src/components/views/rooms/RoomSublist.tsx index 0bb7381dbc..ba8bbffbcc 100644 --- a/src/components/views/rooms/RoomSublist.tsx +++ b/src/components/views/rooms/RoomSublist.tsx @@ -454,8 +454,9 @@ export default class RoomSublist extends React.Component { const sublist = possibleSticky.parentElement.parentElement; const list = sublist.parentElement.parentElement; // the scrollTop is capped at the height of the header in LeftPanel, the top header is always sticky - const isAtTop = list.scrollTop <= HEADER_HEIGHT; - const isAtBottom = list.scrollTop >= list.scrollHeight - list.offsetHeight; + const listScrollTop = Math.round(list.scrollTop); + const isAtTop = listScrollTop <= Math.round(HEADER_HEIGHT); + const isAtBottom = listScrollTop >= Math.round(list.scrollHeight - list.offsetHeight); const isStickyTop = possibleSticky.classList.contains('mx_RoomSublist_headerContainer_stickyTop'); const isStickyBottom = possibleSticky.classList.contains('mx_RoomSublist_headerContainer_stickyBottom'); From 4f649f290cafbe49c9bc7ed8ac8c20e1ae77ab35 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Thu, 10 Jun 2021 10:46:21 +0100 Subject: [PATCH 447/464] Migrate RoomSettingsDialog to TypeScript --- ...ttingsDialog.js => RoomSettingsDialog.tsx} | 33 ++++++++++--------- 1 file changed, 18 insertions(+), 15 deletions(-) rename src/components/views/dialogs/{RoomSettingsDialog.js => RoomSettingsDialog.tsx} (87%) diff --git a/src/components/views/dialogs/RoomSettingsDialog.js b/src/components/views/dialogs/RoomSettingsDialog.tsx similarity index 87% rename from src/components/views/dialogs/RoomSettingsDialog.js rename to src/components/views/dialogs/RoomSettingsDialog.tsx index b6c4d42243..9d1608c9e4 100644 --- a/src/components/views/dialogs/RoomSettingsDialog.js +++ b/src/components/views/dialogs/RoomSettingsDialog.tsx @@ -16,7 +16,6 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import TabbedView, {Tab} from "../../structures/TabbedView"; import {_t, _td} from "../../../languageHandler"; import AdvancedRoomSettingsTab from "../settings/tabs/room/AdvancedRoomSettingsTab"; @@ -39,31 +38,35 @@ export const ROOM_NOTIFICATIONS_TAB = "ROOM_NOTIFICATIONS_TAB"; export const ROOM_BRIDGES_TAB = "ROOM_BRIDGES_TAB"; export const ROOM_ADVANCED_TAB = "ROOM_ADVANCED_TAB"; +interface IProps { + roomId: string; + onFinished: (success: boolean) => void; +} + @replaceableComponent("views.dialogs.RoomSettingsDialog") -export default class RoomSettingsDialog extends React.Component { - static propTypes = { - roomId: PropTypes.string.isRequired, - onFinished: PropTypes.func.isRequired, - }; +export default class RoomSettingsDialog extends React.Component { + private dispatcherRef: string; - componentDidMount() { - this._dispatcherRef = dis.register(this._onAction); + public componentDidMount() { + this.dispatcherRef = dis.register(this.onAction); } - componentWillUnmount() { - if (this._dispatcherRef) dis.unregister(this._dispatcherRef); + public componentWillUnmount() { + if (this.dispatcherRef) { + dis.unregister(this.dispatcherRef); + } } - _onAction = (payload) => { + private onAction = (payload): void => { // When view changes below us, close the room settings // whilst the modal is open this can only be triggered when someone hits Leave Room if (payload.action === 'view_home_page') { - this.props.onFinished(); + this.props.onFinished(true); } }; - _getTabs() { - const tabs = []; + private getTabs(): Tab[] { + const tabs: Tab[] = []; tabs.push(new Tab( ROOM_GENERAL_TAB, @@ -123,7 +126,7 @@ export default class RoomSettingsDialog extends React.Component { title={_t("Room Settings - %(roomName)s", {roomName})} >
    - +
    ); From 7f3173f17085ee598e78c20f94db916b7f297fa7 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Thu, 10 Jun 2021 11:14:43 +0100 Subject: [PATCH 448/464] Implement unencrypted warning slate in rooms --- .../views/dialogs/RoomSettingsDialog.tsx | 6 ++- .../views/messages/EventTileBubble.tsx | 3 +- src/components/views/rooms/NewRoomIntro.tsx | 37 +++++++++++++------ src/i18n/strings/en_EN.json | 3 +- src/stores/RoomViewStore.tsx | 1 + 5 files changed, 35 insertions(+), 15 deletions(-) diff --git a/src/components/views/dialogs/RoomSettingsDialog.tsx b/src/components/views/dialogs/RoomSettingsDialog.tsx index 9d1608c9e4..1a664951c5 100644 --- a/src/components/views/dialogs/RoomSettingsDialog.tsx +++ b/src/components/views/dialogs/RoomSettingsDialog.tsx @@ -41,6 +41,7 @@ export const ROOM_ADVANCED_TAB = "ROOM_ADVANCED_TAB"; interface IProps { roomId: string; onFinished: (success: boolean) => void; + initialTabId?: string; } @replaceableComponent("views.dialogs.RoomSettingsDialog") @@ -126,7 +127,10 @@ export default class RoomSettingsDialog extends React.Component { title={_t("Room Settings - %(roomName)s", {roomName})} >
    - +
    ); diff --git a/src/components/views/messages/EventTileBubble.tsx b/src/components/views/messages/EventTileBubble.tsx index f797a97a3d..88913ac2d4 100644 --- a/src/components/views/messages/EventTileBubble.tsx +++ b/src/components/views/messages/EventTileBubble.tsx @@ -14,13 +14,14 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, {forwardRef, ReactNode} from "react"; +import React, {forwardRef, ReactNode, ReactChildren} from "react"; import classNames from "classnames"; interface IProps { className: string; title: string; subtitle?: ReactNode; + children?: ReactChildren; } const EventTileBubble = forwardRef(({ className, title, subtitle, children }, ref) => { diff --git a/src/components/views/rooms/NewRoomIntro.tsx b/src/components/views/rooms/NewRoomIntro.tsx index 9b003ade89..21282c415a 100644 --- a/src/components/views/rooms/NewRoomIntro.tsx +++ b/src/components/views/rooms/NewRoomIntro.tsx @@ -33,6 +33,9 @@ import {showSpaceInvite} from "../../../utils/space"; import { privateShouldBeEncrypted } from "../../../createRoom"; +import EventTileBubble from "../messages/EventTileBubble"; +import { ROOM_SECURITY_TAB } from "../dialogs/RoomSettingsDialog"; + function hasExpectedEncryptionSettings(room): boolean { const isEncrypted: boolean = room._client.isRoomEncrypted(room.roomId); const isPublic: boolean = room.getJoinRule() === "public"; @@ -174,20 +177,30 @@ const NewRoomIntro = () => { ; } + function openRoomSettings(event) { + event.preventDefault(); + dis.dispatch({ + action: "open_room_settings", + initial_tab_id: ROOM_SECURITY_TAB, + }); + } + + const sub2 = _t("Your private messages are normally encrypted, but this room isn't." + + "Usually this is because the room was created with a device or method that doesn't support " + + "encryption, like email invites. Enable encryption in settings.", {}, + { a: sub => {sub} }); + return
    + + { !hasExpectedEncryptionSettings(room) && ( + + )} + { 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 8508d55eba..3ab69cdb11 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1509,7 +1509,8 @@ "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.", + "Your private messages are normally encrypted, but this room isn't.Usually this is because the room was created with a device or method that doesn't support encryption, like email invites. Enable encryption in settings.": "Your private messages are normally encrypted, but this room isn't.Usually this is because the room was created with a device or method that doesn't support encryption, like email invites. Enable encryption in settings.", + "This room isn't end to end encrypted": "This room isn't end to end encrypted", "Unpin": "Unpin", "View message": "View message", "%(duration)ss": "%(duration)ss", diff --git a/src/stores/RoomViewStore.tsx b/src/stores/RoomViewStore.tsx index 8da565f603..cc3eafffcd 100644 --- a/src/stores/RoomViewStore.tsx +++ b/src/stores/RoomViewStore.tsx @@ -167,6 +167,7 @@ class RoomViewStore extends Store { const RoomSettingsDialog = sdk.getComponent("dialogs.RoomSettingsDialog"); Modal.createTrackedDialog('Room settings', '', RoomSettingsDialog, { roomId: payload.room_id || this.state.roomId, + initialTabId: payload.initial_tab_id, }, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true); break; } From 0e2e327c4650d835abf8b2b0d9cee39982383144 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Thu, 10 Jun 2021 11:19:25 +0100 Subject: [PATCH 449/464] Delete new room intro stale CSS --- res/css/views/rooms/_NewRoomIntro.scss | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/res/css/views/rooms/_NewRoomIntro.scss b/res/css/views/rooms/_NewRoomIntro.scss index 357273b597..e0cccfa885 100644 --- a/res/css/views/rooms/_NewRoomIntro.scss +++ b/res/css/views/rooms/_NewRoomIntro.scss @@ -69,24 +69,4 @@ 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); - } } From 6606a1142ded38fe85953d1cc881599491aa3054 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Thu, 10 Jun 2021 11:29:10 +0100 Subject: [PATCH 450/464] Update unencrypted room warning copy --- src/components/views/rooms/NewRoomIntro.tsx | 8 ++++---- src/i18n/strings/en_EN.json | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/views/rooms/NewRoomIntro.tsx b/src/components/views/rooms/NewRoomIntro.tsx index 21282c415a..76240f6a2f 100644 --- a/src/components/views/rooms/NewRoomIntro.tsx +++ b/src/components/views/rooms/NewRoomIntro.tsx @@ -185,9 +185,9 @@ const NewRoomIntro = () => { }); } - const sub2 = _t("Your private messages are normally encrypted, but this room isn't." + - "Usually this is because the room was created with a device or method that doesn't support " + - "encryption, like email invites. Enable encryption in settings.", {}, + const sub2 = _t("Your private messages are normally encrypted, but this room isn't. "+ + "Usually this is due to an unsupported device or method being used, " + + "like email invites. Enable encryption in settings.", {}, { a: sub => {sub} }); return
    @@ -195,7 +195,7 @@ const NewRoomIntro = () => { { !hasExpectedEncryptionSettings(room) && ( )} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 3ab69cdb11..874dc11bd2 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1509,8 +1509,8 @@ "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 .", - "Your private messages are normally encrypted, but this room isn't.Usually this is because the room was created with a device or method that doesn't support encryption, like email invites. Enable encryption in settings.": "Your private messages are normally encrypted, but this room isn't.Usually this is because the room was created with a device or method that doesn't support encryption, like email invites. Enable encryption in settings.", - "This room isn't end to end encrypted": "This room isn't end to end encrypted", + "Your private messages are normally encrypted, but this room isn't. Usually this is due to an unsupported device or method being used, like email invites. Enable encryption in settings.": "Your private messages are normally encrypted, but this room isn't. Usually this is due to an unsupported device or method being used, like email invites. Enable encryption in settings.", + "End-to-end encryption isn't enabled": "End-to-end encryption isn't enabled", "Unpin": "Unpin", "View message": "View message", "%(duration)ss": "%(duration)ss", From 9731e275f8665b59b8a50746603ad4754392848d Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Thu, 10 Jun 2021 12:15:55 +0100 Subject: [PATCH 451/464] cater for an undefined MatrixClient on rooms --- src/components/views/rooms/NewRoomIntro.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/NewRoomIntro.tsx b/src/components/views/rooms/NewRoomIntro.tsx index 76240f6a2f..390a570b26 100644 --- a/src/components/views/rooms/NewRoomIntro.tsx +++ b/src/components/views/rooms/NewRoomIntro.tsx @@ -37,7 +37,7 @@ import EventTileBubble from "../messages/EventTileBubble"; import { ROOM_SECURITY_TAB } from "../dialogs/RoomSettingsDialog"; function hasExpectedEncryptionSettings(room): boolean { - const isEncrypted: boolean = room._client.isRoomEncrypted(room.roomId); + const isEncrypted: boolean = room._client?.isRoomEncrypted(room.roomId); const isPublic: boolean = room.getJoinRule() === "public"; return isPublic || !privateShouldBeEncrypted() || isEncrypted; } From 79b8fbc2a909364972043ebe06019406247fb9b5 Mon Sep 17 00:00:00 2001 From: Germain Date: Thu, 10 Jun 2021 14:21:48 +0100 Subject: [PATCH 452/464] Add indentation for unencrypted warning text Co-authored-by: J. Ryan Stinnett --- src/components/views/rooms/NewRoomIntro.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/components/views/rooms/NewRoomIntro.tsx b/src/components/views/rooms/NewRoomIntro.tsx index 390a570b26..3bf9a9db33 100644 --- a/src/components/views/rooms/NewRoomIntro.tsx +++ b/src/components/views/rooms/NewRoomIntro.tsx @@ -185,10 +185,12 @@ const NewRoomIntro = () => { }); } - const sub2 = _t("Your private messages are normally encrypted, but this room isn't. "+ - "Usually this is due to an unsupported device or method being used, " + - "like email invites. Enable encryption in settings.", {}, - { a: sub => {sub} }); + const sub2 = _t( + "Your private messages are normally encrypted, but this room isn't. "+ + "Usually this is due to an unsupported device or method being used, " + + "like email invites. Enable encryption in settings.", {}, + { a: sub => {sub} }, + ); return
    From 8174973cfed821249db3c3db61a489c79d776039 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 10 Jun 2021 15:31:27 +0200 Subject: [PATCH 453/464] Use the same styling as for mx_TextualEvent 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 7aaafa6ac3..08644b14e3 100644 --- a/res/css/views/messages/_SenderProfile.scss +++ b/res/css/views/messages/_SenderProfile.scss @@ -21,6 +21,6 @@ limitations under the License. .mx_SenderProfile_mxid { font-weight: 600; font-size: 1.1rem; - color: $event-timestamp-color; margin-left: 5px; + opacity: 0.5; // Match mx_TextualEvent } From 5343be7814e9fd120c3779aa8a33951716cb2d0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 10 Jun 2021 18:53:56 +0200 Subject: [PATCH 454/464] Fix buggy hovering/selecting of event tiles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/rooms/_EventTile.scss | 18 +++++++++++------- res/css/views/rooms/_GroupLayout.scss | 8 -------- 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index c8b4138f27..3af266caee 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -16,6 +16,7 @@ limitations under the License. */ $left-gutter: 64px; +$hover-select-border: 4px; .mx_EventTile { max-width: 100%; @@ -142,8 +143,7 @@ $left-gutter: 64px; } .mx_EventTile_selected > div > a > .mx_MessageTimestamp { - left: 3px; - width: auto; + left: calc(-$hover-select-border); } .mx_EventTile:hover .mx_MessageActionBar, @@ -158,7 +158,7 @@ $left-gutter: 64px; */ .mx_EventTile_selected > .mx_EventTile_line { border-left: $accent-color 4px solid; - padding-left: 60px; + padding-left: calc($left-gutter - $hover-select-border); background-color: $event-selected-color; } @@ -171,8 +171,12 @@ $left-gutter: 64px; } } +.mx_EventTile_info .mx_EventTile_line { + padding-left: calc($left-gutter + 18px); +} + .mx_EventTile_selected.mx_EventTile_info .mx_EventTile_line { - padding-left: 78px; + padding-left: calc($left-gutter + 18px - $hover-select-border); } .mx_EventTile:hover .mx_EventTile_line, @@ -408,7 +412,7 @@ $left-gutter: 64px; .mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line, .mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line, .mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line { - padding-left: 60px; + padding-left: calc($left-gutter - $hover-select-border); } .mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line { @@ -426,7 +430,7 @@ $left-gutter: 64px; .mx_EventTile:hover.mx_EventTile_verified.mx_EventTile_info .mx_EventTile_line, .mx_EventTile:hover.mx_EventTile_unverified.mx_EventTile_info .mx_EventTile_line, .mx_EventTile:hover.mx_EventTile_unknown.mx_EventTile_info .mx_EventTile_line { - padding-left: 78px; + padding-left: calc($left-gutter + 18px - $hover-select-border); } /* End to end encryption stuff */ @@ -438,7 +442,7 @@ $left-gutter: 64px; .mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line > a > .mx_MessageTimestamp, .mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line > a > .mx_MessageTimestamp, .mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line > a > .mx_MessageTimestamp { - width: $MessageTimestamp_width_hover; + left: calc(-$hover-select-border); } // Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies) diff --git a/res/css/views/rooms/_GroupLayout.scss b/res/css/views/rooms/_GroupLayout.scss index 818509785b..ddee81a914 100644 --- a/res/css/views/rooms/_GroupLayout.scss +++ b/res/css/views/rooms/_GroupLayout.scss @@ -24,10 +24,6 @@ $left-gutter: 64px; margin-left: $left-gutter; } - > .mx_EventTile_line { - padding-left: $left-gutter; - } - > .mx_EventTile_avatar { position: absolute; } @@ -43,10 +39,6 @@ $left-gutter: 64px; line-height: $font-22px; } } - - .mx_EventTile_info .mx_EventTile_line { - padding-left: calc($left-gutter + 18px); - } } /* Compact layout overrides */ From b1824ce579c805c8c293aee739808b71bc030614 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Fri, 11 Jun 2021 11:34:53 +0100 Subject: [PATCH 455/464] fix HTML tag for Event Tile when not rendered in a list --- .../views/elements/EventTilePreview.tsx | 1 + src/components/views/rooms/EventTile.tsx | 150 ++++++++++-------- src/components/views/rooms/ReplyPreview.js | 1 + 3 files changed, 82 insertions(+), 70 deletions(-) diff --git a/src/components/views/elements/EventTilePreview.tsx b/src/components/views/elements/EventTilePreview.tsx index b15fbbed2b..cf3b7a6e61 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" />
    ; } diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 0861f7fd5d..85b9cac2c4 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -1059,58 +1059,65 @@ export default class EventTile extends React.Component { switch (this.props.tileShape) { case 'notif': { const room = this.context.getRoom(this.props.mxEvent.getRoomId()); - return ( -
  • - - -
    - -
    -
  • - ); + return React.createElement(this.props.as || "li", { + "className": classes, + "aria-live": ariaLive, + "aria-atomic": true, + "data-scroll-tokens": scrollToken, + }, [ + , + , +
    + +
    , + ]); } case 'file_grid': { - return ( -
  • -
    - + return React.createElement(this.props.as || "li", { + "className": classes, + "aria-live": ariaLive, + "aria-atomic": true, + "data-scroll-tokens": scrollToken, + }, [ +
    + +
    , + +
    + { sender } + { timestamp }
    -
    -
    - { sender } - { timestamp } -
    -
    -
  • - ); + , + ]); } case 'reply': @@ -1126,27 +1133,30 @@ export default class EventTile extends React.Component { this.props.alwaysShowTimestamps || this.state.hover, ); } - return ( -
  • - { ircTimestamp } - { avatar } - { sender } - { ircPadlock } -
    - { groupTimestamp } - { groupPadlock } - { thread } - -
    -
  • - ); + return React.createElement(this.props.as || "li", { + "className": classes, + "aria-live": ariaLive, + "aria-atomic": true, + "data-scroll-tokens": scrollToken, + }, [ + ircTimestamp, + avatar, + sender, + ircPadlock, +
    + { groupTimestamp } + { groupPadlock } + { thread } + +
    , + ]); } default: { const thread = ReplyThread.makeThread( diff --git a/src/components/views/rooms/ReplyPreview.js b/src/components/views/rooms/ReplyPreview.js index 5835eb9f36..c61424a059 100644 --- a/src/components/views/rooms/ReplyPreview.js +++ b/src/components/views/rooms/ReplyPreview.js @@ -95,6 +95,7 @@ export default class ReplyPreview extends React.Component { permalinkCreator={this.props.permalinkCreator} isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")} enableFlair={SettingsStore.getValue(UIFeature.Flair)} + as="div" />
    ; From 84679cf8ec5bfe764cda2743f69a9099fe79c38a Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Fri, 11 Jun 2021 12:19:14 +0100 Subject: [PATCH 456/464] remove legacy polyfills and unused dependencies --- package.json | 3 --- src/ContentMessages.tsx | 2 -- src/CountlyAnalytics.ts | 7 ------- src/rageshake/submit-rageshake.ts | 6 ------ src/utils/MegolmExportEncryption.js | 11 ----------- yarn.lock | 10 ---------- 6 files changed, 39 deletions(-) diff --git a/package.json b/package.json index dbb6b29b53..4dc4bda3b2 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,6 @@ "dependencies": { "@babel/runtime": "^7.12.5", "await-lock": "^2.1.0", - "blueimp-canvas-to-blob": "^3.28.0", "browser-encrypt-attachment": "^0.3.0", "browser-request": "^0.3.3", "cheerio": "^1.0.0-rc.9", @@ -88,7 +87,6 @@ "png-chunks-extract": "^1.0.0", "prop-types": "^15.7.2", "qrcode": "^1.4.4", - "qs": "^6.9.6", "re-resizable": "^6.9.0", "react": "^17.0.2", "react-beautiful-dnd": "^4.0.1", @@ -99,7 +97,6 @@ "rfc4648": "^1.4.0", "sanitize-html": "^2.3.2", "tar-js": "^0.3.0", - "text-encoding-utf-8": "^1.0.2", "url": "^0.11.0", "what-input": "^5.2.10", "zxcvbn": "^4.4.2" diff --git a/src/ContentMessages.tsx b/src/ContentMessages.tsx index b21829ac63..9042d47243 100644 --- a/src/ContentMessages.tsx +++ b/src/ContentMessages.tsx @@ -28,8 +28,6 @@ import encrypt from "browser-encrypt-attachment"; import extractPngChunks from "png-chunks-extract"; import Spinner from "./components/views/elements/Spinner"; -// Polyfill for Canvas.toBlob API using Canvas.toDataURL -import "blueimp-canvas-to-blob"; import { Action } from "./dispatcher/actions"; import CountlyAnalytics from "./CountlyAnalytics"; import { diff --git a/src/CountlyAnalytics.ts b/src/CountlyAnalytics.ts index 5545ed8483..f494c1eb22 100644 --- a/src/CountlyAnalytics.ts +++ b/src/CountlyAnalytics.ts @@ -24,13 +24,6 @@ import {sleep} from "./utils/promise"; import RoomViewStore from "./stores/RoomViewStore"; import { Action } from "./dispatcher/actions"; -// polyfill textencoder if necessary -import * as TextEncodingUtf8 from 'text-encoding-utf-8'; -let TextEncoder = window.TextEncoder; -if (!TextEncoder) { - TextEncoder = TextEncodingUtf8.TextEncoder; -} - const INACTIVITY_TIME = 20; // seconds const HEARTBEAT_INTERVAL = 5_000; // ms const SESSION_UPDATE_INTERVAL = 60; // seconds diff --git a/src/rageshake/submit-rageshake.ts b/src/rageshake/submit-rageshake.ts index b2ad9fe6f6..08d8ccfd13 100644 --- a/src/rageshake/submit-rageshake.ts +++ b/src/rageshake/submit-rageshake.ts @@ -25,14 +25,8 @@ import Tar from "tar-js"; import * as rageshake from './rageshake'; -// polyfill textencoder if necessary -import * as TextEncodingUtf8 from 'text-encoding-utf-8'; import SettingsStore from "../settings/SettingsStore"; import SdkConfig from "../SdkConfig"; -let TextEncoder = window.TextEncoder; -if (!TextEncoder) { - TextEncoder = TextEncodingUtf8.TextEncoder; -} interface IOpts { label?: string; diff --git a/src/utils/MegolmExportEncryption.js b/src/utils/MegolmExportEncryption.js index 6f5c7104b1..ae6b2999fa 100644 --- a/src/utils/MegolmExportEncryption.js +++ b/src/utils/MegolmExportEncryption.js @@ -15,17 +15,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -// polyfill textencoder if necessary -import * as TextEncodingUtf8 from 'text-encoding-utf-8'; -let TextEncoder = window.TextEncoder; -if (!TextEncoder) { - TextEncoder = TextEncodingUtf8.TextEncoder; -} -let TextDecoder = window.TextDecoder; -if (!TextDecoder) { - TextDecoder = TextEncodingUtf8.TextDecoder; -} - import { _t } from '../languageHandler'; import SdkConfig from '../SdkConfig'; diff --git a/yarn.lock b/yarn.lock index 2b8667c7a6..f98cb5845e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2181,11 +2181,6 @@ bluebird@^3.5.0: resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== -blueimp-canvas-to-blob@^3.28.0: - version "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== - boolbase@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" @@ -7945,11 +7940,6 @@ test-exclude@^6.0.0: glob "^7.1.4" minimatch "^3.0.4" -text-encoding-utf-8@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/text-encoding-utf-8/-/text-encoding-utf-8-1.0.2.tgz#585b62197b0ae437e3c7b5d0af27ac1021e10d13" - integrity sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg== - text-table@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" From 37484e3fc465ef5a05a5001580facbea37d10bb5 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Fri, 11 Jun 2021 12:33:02 +0100 Subject: [PATCH 457/464] Add TextEncoder polyfill for test run --- test/setupTests.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/setupTests.js b/test/setupTests.js index 6d37d48987..9da412a7e8 100644 --- a/test/setupTests.js +++ b/test/setupTests.js @@ -1,6 +1,12 @@ import * as languageHandler from "../src/languageHandler"; +import { TextEncoder, TextDecoder } from 'util'; languageHandler.setLanguage('en'); languageHandler.setMissingEntryGenerator(key => key.split("|", 2)[1]); require('jest-fetch-mock').enableMocks(); + +// polyfilling TextEncoder as it is not available on JSDOM +// view https://github.com/facebook/jest/issues/9983 +global.TextEncoder = TextEncoder; +global.TextDecoder = TextDecoder; From e260e8671fab08da34f8c9abc30ec39e1aa486d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 11 Jun 2021 15:14:16 +0200 Subject: [PATCH 458/464] Inline display name into return 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, 3 insertions(+), 7 deletions(-) diff --git a/src/components/views/messages/SenderProfile.tsx b/src/components/views/messages/SenderProfile.tsx index 872dc132b9..805f842fbc 100644 --- a/src/components/views/messages/SenderProfile.tsx +++ b/src/components/views/messages/SenderProfile.tsx @@ -118,12 +118,6 @@ export default class SenderProfile extends React.Component { return null; // emote message must include the name so don't duplicate it } - const displayNameElement = ( - - { displayName } - - ); - let mxidElement; if (disambiguate) { mxidElement = ( @@ -147,7 +141,9 @@ export default class SenderProfile extends React.Component { return (
    - { displayNameElement } + + { displayName } + { mxidElement } { flair }
    From 2eafe132b8b0650b058faa4be3bbdc4e5167ff38 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 14 Jun 2021 13:31:58 -0600 Subject: [PATCH 459/464] Move various createRoom types to the js-sdk --- src/components/structures/SpaceRoomView.tsx | 3 +- .../views/dialogs/CreateRoomDialog.tsx | 21 ++++---- .../views/spaces/SpaceCreateMenu.tsx | 6 ++- src/createRoom.ts | 52 ++----------------- src/mjolnir/Mjolnir.ts | 3 +- 5 files changed, 23 insertions(+), 62 deletions(-) diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx index 276f4ae6ca..3ccf2e5424 100644 --- a/src/components/structures/SpaceRoomView.tsx +++ b/src/components/structures/SpaceRoomView.tsx @@ -28,7 +28,7 @@ import RoomTopic from "../views/elements/RoomTopic"; import InlineSpinner from "../views/elements/InlineSpinner"; import {inviteMultipleToRoom, showRoomInviteDialog} from "../../RoomInvite"; import {useRoomMembers} from "../../hooks/useRoomMembers"; -import createRoom, {IOpts, Preset} from "../../createRoom"; +import createRoom, {IOpts} from "../../createRoom"; import Field from "../views/elements/Field"; import {useEventEmitter} from "../../hooks/useEventEmitter"; import withValidation from "../views/elements/Validation"; @@ -65,6 +65,7 @@ import dis from "../../dispatcher/dispatcher"; import Modal from "../../Modal"; import BetaFeedbackDialog from "../views/dialogs/BetaFeedbackDialog"; import SdkConfig from "../../SdkConfig"; +import { Preset } from "matrix-js-sdk/src/@types/partials"; interface IProps { space: Room; diff --git a/src/components/views/dialogs/CreateRoomDialog.tsx b/src/components/views/dialogs/CreateRoomDialog.tsx index cce6b6c34c..544f593708 100644 --- a/src/components/views/dialogs/CreateRoomDialog.tsx +++ b/src/components/views/dialogs/CreateRoomDialog.tsx @@ -15,22 +15,23 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, {ChangeEvent, createRef, KeyboardEvent, SyntheticEvent} from "react"; -import {Room} from "matrix-js-sdk/src/models/room"; +import React, { ChangeEvent, createRef, KeyboardEvent, SyntheticEvent } from "react"; +import { Room } from "matrix-js-sdk/src/models/room"; import SdkConfig from '../../../SdkConfig'; -import withValidation, {IFieldState} from '../elements/Validation'; -import {_t} from '../../../languageHandler'; -import {MatrixClientPeg} from '../../../MatrixClientPeg'; -import {Key} from "../../../Keyboard"; -import {IOpts, Preset, privateShouldBeEncrypted, Visibility} from "../../../createRoom"; -import {CommunityPrototypeStore} from "../../../stores/CommunityPrototypeStore"; -import {replaceableComponent} from "../../../utils/replaceableComponent"; +import withValidation, { IFieldState } from '../elements/Validation'; +import { _t } from '../../../languageHandler'; +import { MatrixClientPeg } from '../../../MatrixClientPeg'; +import { Key } from "../../../Keyboard"; +import { IOpts, privateShouldBeEncrypted } 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"; +import { Preset, Visibility } from "matrix-js-sdk/src/@types/partials"; interface IProps { defaultPublic?: boolean; @@ -72,7 +73,7 @@ export default class CreateRoomDialog extends React.Component { canChangeEncryption: true, }; - MatrixClientPeg.get().doesServerForceEncryptionForPreset("private") + MatrixClientPeg.get().doesServerForceEncryptionForPreset(Preset.PrivateChat) .then(isForced => this.setState({ canChangeEncryption: !isForced })); } diff --git a/src/components/views/spaces/SpaceCreateMenu.tsx b/src/components/views/spaces/SpaceCreateMenu.tsx index 0ebf511018..6a935ab276 100644 --- a/src/components/views/spaces/SpaceCreateMenu.tsx +++ b/src/components/views/spaces/SpaceCreateMenu.tsx @@ -22,7 +22,7 @@ import FocusLock from "react-focus-lock"; import {_t} from "../../../languageHandler"; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import {ChevronFace, ContextMenu} from "../../structures/ContextMenu"; -import createRoom, {IStateEvent, Preset} from "../../../createRoom"; +import createRoom from "../../../createRoom"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import {SpaceAvatar} from "./SpaceBasicSettings"; import AccessibleButton from "../elements/AccessibleButton"; @@ -33,6 +33,8 @@ import {USER_LABS_TAB} from "../dialogs/UserSettingsDialog"; import Field from "../elements/Field"; import withValidation from "../elements/Validation"; import {SpaceFeedbackPrompt} from "../../structures/SpaceRoomView"; +import { Preset } from "matrix-js-sdk/src/@types/partials"; +import { ICreateRoomStateEvent } from "matrix-js-sdk/src/@types/requests"; const SpaceCreateMenuType = ({ title, description, className, onClick }) => { return ( @@ -81,7 +83,7 @@ const SpaceCreateMenu = ({ onFinished }) => { return; } - const initialState: IStateEvent[] = [ + const initialState: ICreateRoomStateEvent[] = [ { type: EventType.RoomHistoryVisibility, content: { diff --git a/src/createRoom.ts b/src/createRoom.ts index c6507b1380..2641492588 100644 --- a/src/createRoom.ts +++ b/src/createRoom.ts @@ -35,53 +35,15 @@ import { VIRTUAL_ROOM_EVENT_TYPE } from "./CallHandler"; import SpaceStore from "./stores/SpaceStore"; import { makeSpaceParentEvent } from "./utils/space"; import { Action } from "./dispatcher/actions" +import { ICreateRoomOpts } from "matrix-js-sdk/src/@types/requests"; +import { Preset, Visibility } from "matrix-js-sdk/src/@types/partials"; // we define a number of interfaces which take their names from the js-sdk /* eslint-disable camelcase */ -// TODO move these interfaces over to js-sdk once it has been typescripted enough to accept them -export enum Visibility { - Public = "public", - Private = "private", -} - -export enum Preset { - PrivateChat = "private_chat", - TrustedPrivateChat = "trusted_private_chat", - PublicChat = "public_chat", -} - -interface Invite3PID { - id_server: string; - id_access_token?: string; // this gets injected by the js-sdk - medium: string; - address: string; -} - -export interface IStateEvent { - type: string; - state_key?: string; // defaults to an empty string - content: object; -} - -interface ICreateOpts { - visibility?: Visibility; - room_alias_name?: string; - name?: string; - topic?: string; - invite?: string[]; - invite_3pid?: Invite3PID[]; - room_version?: string; - creation_content?: object; - initial_state?: IStateEvent[]; - preset?: Preset; - is_direct?: boolean; - power_level_content_override?: object; -} - export interface IOpts { dmUserId?: string; - createOpts?: ICreateOpts; + createOpts?: ICreateRoomOpts; spinner?: boolean; guestAccess?: boolean; encryption?: boolean; @@ -91,12 +53,6 @@ export interface IOpts { parentSpace?: Room; } -export interface IInvite3PID { - id_server: string, - medium: 'email', - address: string, -} - /** * Create a new room, and switch to it. * @@ -136,7 +92,7 @@ export default function createRoom(opts: IOpts): Promise { const defaultPreset = opts.dmUserId ? Preset.TrustedPrivateChat : Preset.PrivateChat; // set some defaults for the creation - const createOpts = opts.createOpts || {}; + const createOpts: ICreateRoomOpts = opts.createOpts || {}; createOpts.preset = createOpts.preset || defaultPreset; createOpts.visibility = createOpts.visibility || Visibility.Private; if (opts.dmUserId && createOpts.invite === undefined) { diff --git a/src/mjolnir/Mjolnir.ts b/src/mjolnir/Mjolnir.ts index 891438bbb9..f1fe816642 100644 --- a/src/mjolnir/Mjolnir.ts +++ b/src/mjolnir/Mjolnir.ts @@ -20,6 +20,7 @@ import SettingsStore from "../settings/SettingsStore"; import {_t} from "../languageHandler"; import dis from "../dispatcher/dispatcher"; import {SettingLevel} from "../settings/SettingLevel"; +import { Preset } from "matrix-js-sdk/src/@types/partials"; // TODO: Move this and related files to the js-sdk or something once finalized. @@ -86,7 +87,7 @@ export class Mjolnir { const resp = await MatrixClientPeg.get().createRoom({ name: _t("My Ban List"), topic: _t("This is your list of users/servers you have blocked - don't leave the room!"), - preset: "private_chat", + preset: Preset.PrivateChat, }); personalRoomId = resp['room_id']; await SettingsStore.setValue( From 951fbf4a1a06f52d40b2eefc21937a3383e983ea Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 14 Jun 2021 13:38:43 -0600 Subject: [PATCH 460/464] fix import --- src/components/views/dialogs/InviteDialog.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/dialogs/InviteDialog.tsx b/src/components/views/dialogs/InviteDialog.tsx index b006205f11..d90214110f 100644 --- a/src/components/views/dialogs/InviteDialog.tsx +++ b/src/components/views/dialogs/InviteDialog.tsx @@ -31,7 +31,6 @@ import Modal from "../../../Modal"; import {humanizeTime} from "../../../utils/humanize"; import createRoom, { canEncryptToAllUsers, ensureDMExists, findDMForUser, privateShouldBeEncrypted, - IInvite3PID, } from "../../../createRoom"; import {inviteMultipleToRoom, showCommunityInviteDialog} from "../../../RoomInvite"; import {Key} from "../../../Keyboard"; @@ -50,6 +49,7 @@ import {getAddressType} from "../../../UserAddress"; import BaseAvatar from '../avatars/BaseAvatar'; import AccessibleButton from '../elements/AccessibleButton'; import { compare } from '../../../utils/strings'; +import { IInvite3PID } from "matrix-js-sdk/src/@types/requests"; // we have a number of types defined from the Matrix spec which can't reasonably be altered here. /* eslint-disable camelcase */ From f8eea2ae23ccf9bc58af983d236249d1ed878831 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 14 Jun 2021 22:18:02 +0100 Subject: [PATCH 461/464] Fix word wrap in space descriptions --- res/css/structures/_SpaceRoomView.scss | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/res/css/structures/_SpaceRoomView.scss b/res/css/structures/_SpaceRoomView.scss index 12762cbc9d..48b565be7f 100644 --- a/res/css/structures/_SpaceRoomView.scss +++ b/res/css/structures/_SpaceRoomView.scss @@ -328,7 +328,8 @@ $SpaceRoomViewInnerWidth: 428px; font-size: $font-15px; margin-top: 12px; margin-bottom: 16px; - white-space: pre; + white-space: pre-wrap; + word-wrap: break-word; } > hr { From 430ec8cecd002cbc5996ec313bb0f7322a6eee14 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 14 Jun 2021 22:18:18 +0100 Subject: [PATCH 462/464] Clear selected rooms after operation in manage space rooms --- src/components/structures/SpaceRoomDirectory.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/structures/SpaceRoomDirectory.tsx b/src/components/structures/SpaceRoomDirectory.tsx index 8d59fe6c68..acbde0b097 100644 --- a/src/components/structures/SpaceRoomDirectory.tsx +++ b/src/components/structures/SpaceRoomDirectory.tsx @@ -520,6 +520,7 @@ export const SpaceHierarchy: React.FC = ({ setError("Failed to update some suggestions. Try again later"); } setSaving(false); + setSelected(new Map()); }} kind="primary_outline" disabled={disabled} From d08495bde68f206ba39d3941950a3a34f279abbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 15 Jun 2021 06:30:22 +0200 Subject: [PATCH 463/464] Fix display name overlap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/rooms/_IRCLayout.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/res/css/views/rooms/_IRCLayout.scss b/res/css/views/rooms/_IRCLayout.scss index cf61ce569d..ae8d22a462 100644 --- a/res/css/views/rooms/_IRCLayout.scss +++ b/res/css/views/rooms/_IRCLayout.scss @@ -178,7 +178,7 @@ $irc-line-height: $font-18px; overflow: hidden; display: flex; - > .mx_SenderProfile_name { + > .mx_SenderProfile_displayName { overflow: hidden; text-overflow: ellipsis; min-width: var(--name-width); @@ -207,7 +207,7 @@ $irc-line-height: $font-18px; background: transparent; > span { - > .mx_SenderProfile_name { + > .mx_SenderProfile_displayName { min-width: inherit; } } From 4a23ebae1e45c572e07cde7eee68d7738b302292 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Tue, 15 Jun 2021 12:00:44 +0100 Subject: [PATCH 464/464] upgrade matrix-react-test-utils for react 17 peer deps --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 4dc4bda3b2..d8c26098ca 100644 --- a/package.json +++ b/package.json @@ -157,7 +157,7 @@ "jest-environment-jsdom-sixteen": "^1.0.3", "jest-fetch-mock": "^3.0.3", "matrix-mock-request": "^1.2.3", - "matrix-react-test-utils": "^0.2.2", + "matrix-react-test-utils": "^0.2.3", "matrix-web-i18n": "github:matrix-org/matrix-web-i18n", "react-test-renderer": "^17.0.2", "rimraf": "^3.0.2", diff --git a/yarn.lock b/yarn.lock index f98cb5845e..7c232d2aa1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5733,10 +5733,10 @@ matrix-mock-request@^1.2.3: bluebird "^3.5.0" expect "^1.20.2" -matrix-react-test-utils@^0.2.2: - version "0.2.2" - resolved "https://registry.yarnpkg.com/matrix-react-test-utils/-/matrix-react-test-utils-0.2.2.tgz#c87144d3b910c7edc544a6699d13c7c2bf02f853" - integrity sha512-49+7gfV6smvBIVbeloql+37IeWMTD+fiywalwCqk8Dnz53zAFjKSltB3rmWHso1uecLtQEcPtCijfhzcLXAxTQ== +matrix-react-test-utils@^0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/matrix-react-test-utils/-/matrix-react-test-utils-0.2.3.tgz#27653f9d6bbfddd1856e51860fad1503b039d617" + integrity sha512-NKZDlMEQzDZDQhBYyKBUtqidRvpkww3n9/GmGICkxtU2D6NetyBIfvm1Lf9o7167KSkPHJUVvDS9dzaS55jUnA== "matrix-web-i18n@github:matrix-org/matrix-web-i18n": version "1.1.2"