From a308a5418323f9529986791a59e27e37ce2eebae Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Wed, 31 Mar 2021 12:28:24 +0100 Subject: [PATCH 001/147] 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 002/147] 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 003/147] 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 004/147] 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 005/147] 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 21c1179f8d2726a909c5540f8e25a92fa0070dc3 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 20 May 2021 17:54:42 +0100 Subject: [PATCH 006/147] 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 007/147] 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 008/147] 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 009/147] 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 431b4607a4c7489e91f4886c6f5b8d8e9beeb855 Mon Sep 17 00:00:00 2001 From: c-cal Date: Thu, 20 May 2021 15:07:41 +0000 Subject: [PATCH 010/147] 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 d0da4b2a2578688dc4892ecd68f0f6c0c9317e90 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 21 May 2021 12:37:34 +0100 Subject: [PATCH 011/147] 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 012/147] 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 013/147] 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 054/147] 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 055/147] 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 056/147] 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 0bbfb1a6d953344083fb7554a303ee38039adc7d Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 24 May 2021 16:18:55 +0100 Subject: [PATCH 057/147] 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 fdc22bfdf747e618dd510c8a257f74ffd8dcca6d Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 24 May 2021 16:40:55 +0100 Subject: [PATCH 058/147] 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 059/147] 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 cdecc156df1800179a619e2a8063295ece3d63b0 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 24 May 2021 17:30:37 +0100 Subject: [PATCH 060/147] 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 061/147] 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 062/147] 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 063/147] 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 064/147] 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 065/147] 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 066/147] 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 067/147] 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 068/147] 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 069/147] 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 070/147] 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 071/147] 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 cb88f37bbd146c62c4ead8043b10b1c3496be2e7 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Tue, 25 May 2021 12:28:16 +0100 Subject: [PATCH 072/147] 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 073/147] 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 7303166924b0c6ea492811f768354e27b35cd974 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Tue, 25 May 2021 13:53:20 +0100 Subject: [PATCH 074/147] 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 a803e33ffe3ae4b17548a69161ae9c0cb2c160f8 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Tue, 25 May 2021 14:10:16 +0100 Subject: [PATCH 075/147] 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 076/147] 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 077/147] 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 078/147] 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 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 079/147] 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 e934f81521dbe50dbdfca807fce003fb1f816573 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Tue, 25 May 2021 16:34:52 +0100 Subject: [PATCH 080/147] 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 081/147] 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:26:43 +0100 Subject: [PATCH 082/147] 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 083/147] 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 084/147] 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 085/147] 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 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 086/147] 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 087/147] 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 088/147] 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 091/147] 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 092/147] 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 093/147] 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 094/147] 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 095/147] 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 096/147] 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 097/147] 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 098/147] 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 099/147] 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}