From 066f72fcdb74cd5df17af6864c152ac5e8bad09e Mon Sep 17 00:00:00 2001 From: Panagiotis <27917356+panoschal@users.noreply.github.com> Date: Thu, 11 Mar 2021 00:14:55 +0200 Subject: [PATCH 01/25] feat: require strong password in forgot password form --- .../structures/auth/ForgotPassword.js | 22 ++++++++++++++++--- src/i18n/strings/en_EN.json | 1 + 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/components/structures/auth/ForgotPassword.js b/src/components/structures/auth/ForgotPassword.js index 5a39fe9fd9..45270323b0 100644 --- a/src/components/structures/auth/ForgotPassword.js +++ b/src/components/structures/auth/ForgotPassword.js @@ -18,7 +18,7 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; -import { _t } from '../../../languageHandler'; +import { _t, _td } from '../../../languageHandler'; import * as sdk from '../../../index'; import Modal from "../../../Modal"; import PasswordReset from "../../../PasswordReset"; @@ -27,6 +27,9 @@ import classNames from 'classnames'; import AuthPage from "../../views/auth/AuthPage"; import CountlyAnalytics from "../../../CountlyAnalytics"; import ServerPicker from "../../views/elements/ServerPicker"; +import PassphraseField from '../../views/auth/PassphraseField'; + +const PASSWORD_MIN_SCORE = 3; // safely unguessable: moderate protection from offline slow-hash scenario. // Phases // Show the forgot password inputs @@ -135,10 +138,14 @@ export default class ForgotPassword extends React.Component { // refresh the server errors, just in case the server came back online await this._checkServerLiveliness(this.props.serverConfig); + await this['password_field'].validate({ allowEmpty: false }); + if (!this.state.email) { this.showErrorDialog(_t('The email address linked to your account must be entered.')); } else if (!this.state.password || !this.state.password2) { this.showErrorDialog(_t('A new password must be entered.')); + } else if (!this.state.passwordFieldValid) { + this.showErrorDialog(_t('Please choose a strong password')); } else if (this.state.password !== this.state.password2) { this.showErrorDialog(_t('New passwords must match each other.')); } else { @@ -184,6 +191,12 @@ export default class ForgotPassword extends React.Component { }); } + onPasswordValidate(result) { + this.setState({ + passwordFieldValid: result.valid, + }); + } + renderForgot() { const Field = sdk.getComponent('elements.Field'); @@ -228,12 +241,15 @@ export default class ForgotPassword extends React.Component { />
- this['password_field'] = field} + onValidate={(result) => this.onPasswordValidate(result)} onFocus={() => CountlyAnalytics.instance.track("onboarding_forgot_password_newPassword_focus")} onBlur={() => CountlyAnalytics.instance.track("onboarding_forgot_password_newPassword_blur")} autoComplete="new-password" diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 833a8c7838..9539b3096e 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2662,6 +2662,7 @@ "Failed to send email": "Failed to send email", "The email address linked to your account must be entered.": "The email address linked to your account must be entered.", "A new password must be entered.": "A new password must be entered.", + "Please choose a strong password": "Please choose a strong password", "New passwords must match each other.": "New passwords must match each other.", "Changing your password will reset any end-to-end encryption keys on all of your sessions, making encrypted chat history unreadable. Set up Key Backup or export your room keys from another session before resetting your password.": "Changing your password will reset any end-to-end encryption keys on all of your sessions, making encrypted chat history unreadable. Set up Key Backup or export your room keys from another session before resetting your password.", "New Password": "New Password", From 82ba546142abebc34ccf920e467460cfd3b3ad6d Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 25 Mar 2021 19:56:21 +0000 Subject: [PATCH 02/25] WIP attended transfer --- res/css/views/voip/_CallView.scss | 8 +-- src/CallHandler.tsx | 16 ++++- src/components/views/dialogs/InviteDialog.tsx | 48 ++++++++++--- src/components/views/voip/CallView.tsx | 67 +++++++++++++------ src/i18n/strings/en_EN.json | 3 + 5 files changed, 108 insertions(+), 34 deletions(-) diff --git a/res/css/views/voip/_CallView.scss b/res/css/views/voip/_CallView.scss index 7eb329594a..e032e4582d 100644 --- a/res/css/views/voip/_CallView.scss +++ b/res/css/views/voip/_CallView.scss @@ -55,7 +55,7 @@ limitations under the License. } } - .mx_CallView_voice_holdText { + .mx_CallView_holdTransferContent { padding-top: 10px; padding-bottom: 25px; } @@ -82,7 +82,7 @@ limitations under the License. } } -.mx_CallView_voice_hold { +.mx_CallView_voice .mx_CallView_holdTransferContent { // This masks the avatar image so when it's blurred, the edge is still crisp .mx_CallView_voice_avatarContainer { border-radius: 2000px; @@ -91,7 +91,7 @@ limitations under the License. } } -.mx_CallView_voice_holdText { +.mx_CallView_holdTransferContent { height: 20px; padding-top: 20px; padding-bottom: 15px; @@ -142,7 +142,7 @@ limitations under the License. } } -.mx_CallView_video_holdContent { +.mx_CallView_video .mx_CallView_holdTransferContent { position: absolute; top: 50%; left: 50%; diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx index ce779f12a5..270853865a 100644 --- a/src/CallHandler.tsx +++ b/src/CallHandler.tsx @@ -154,6 +154,9 @@ function getRemoteAudioElement(): HTMLAudioElement { export default class CallHandler { private calls = new Map(); // roomId -> call + // Calls started as an attended transfer, ie. with the intention of transferring another + // call with a different party to this one. + private transferees = new Map(); // callId (target) -> call (transferee) private audioPromises = new Map>(); private dispatcherRef: string = null; private supportsPstnProtocol = null; @@ -325,6 +328,10 @@ export default class CallHandler { return callsNotInThatRoom; } + getTransfereeForCallId(callId: string): MatrixCall { + return this.transferees[callId]; + } + play(audioId: AudioID) { // TODO: Attach an invisible element for this instead // which listens? @@ -622,6 +629,7 @@ export default class CallHandler { private async placeCall( roomId: string, type: PlaceCallType, localElement: HTMLVideoElement, remoteElement: HTMLVideoElement, + transferee: MatrixCall, ) { Analytics.trackEvent('voip', 'placeCall', 'type', type); CountlyAnalytics.instance.trackStartCall(roomId, type === PlaceCallType.Video, false); @@ -634,6 +642,9 @@ export default class CallHandler { const call = createNewMatrixCall(MatrixClientPeg.get(), mappedRoomId); this.calls.set(roomId, call); + if (transferee) { + this.transferees[transferee.callId] = call; + } this.setCallListeners(call); this.setCallAudioElement(call); @@ -723,7 +734,10 @@ export default class CallHandler { } else if (members.length === 2) { console.info(`Place ${payload.type} call in ${payload.room_id}`); - this.placeCall(payload.room_id, payload.type, payload.local_element, payload.remote_element); + this.placeCall( + payload.room_id, payload.type, payload.local_element, payload.remote_element, + payload.transferee, + ); } else { // > 2 dis.dispatch({ action: "place_conference_call", diff --git a/src/components/views/dialogs/InviteDialog.tsx b/src/components/views/dialogs/InviteDialog.tsx index de0b5b237b..ad74e7bb02 100644 --- a/src/components/views/dialogs/InviteDialog.tsx +++ b/src/components/views/dialogs/InviteDialog.tsx @@ -29,7 +29,9 @@ import dis from "../../../dispatcher/dispatcher"; import IdentityAuthClient from "../../../IdentityAuthClient"; import Modal from "../../../Modal"; import {humanizeTime} from "../../../utils/humanize"; -import createRoom, {canEncryptToAllUsers, findDMForUser, privateShouldBeEncrypted} from "../../../createRoom"; +import createRoom, { + canEncryptToAllUsers, ensureDMExists, findDMForUser, privateShouldBeEncrypted, +} from "../../../createRoom"; import {inviteMultipleToRoom, showCommunityInviteDialog} from "../../../RoomInvite"; import {Key} from "../../../Keyboard"; import {Action} from "../../../dispatcher/actions"; @@ -331,6 +333,7 @@ interface IInviteDialogState { threepidResultsMixin: { user: Member, userId: string}[]; canUseIdentityServer: boolean; tryingIdentityServer: boolean; + consultFirst: boolean; // These two flags are used for the 'Go' button to communicate what is going on. busy: boolean, @@ -379,6 +382,7 @@ export default class InviteDialog extends React.PureComponent { + this.setState({consultFirst: ev.target.checked}); + } + static buildRecents(excludedTargetIds: Set): {userId: string, user: RoomMember, lastActive: number}[] { const rooms = DMRoomMap.shared().getUniqueRoomsWithIndividuals(); // map of userId => js-sdk Room @@ -721,16 +729,28 @@ export default class InviteDialog extends React.PureComponent + +
; } else { console.error("Unknown kind of InviteDialog: " + this.props.kind); } @@ -1327,6 +1354,7 @@ export default class InviteDialog extends React.PureComponent + {consultSection} ); diff --git a/src/components/views/voip/CallView.tsx b/src/components/views/voip/CallView.tsx index 9bdc8fb11d..0a5d028069 100644 --- a/src/components/views/voip/CallView.tsx +++ b/src/components/views/voip/CallView.tsx @@ -364,6 +364,11 @@ export default class CallView extends React.Component { CallHandler.sharedInstance().setActiveCallRoomId(userFacingRoomId); } + private onTransferClick = () => { + const transfereeCall = CallHandler.sharedInstance().getTransfereeForCallId(this.props.call.callId); + this.props.call.transferToCall(transfereeCall); + } + public render() { const client = MatrixClientPeg.get(); const callRoomId = CallHandler.roomIdForCall(this.props.call); @@ -479,25 +484,52 @@ export default class CallView extends React.Component { // for voice calls (fills the bg) let contentView: React.ReactNode; + const transfereeCall = CallHandler.sharedInstance().getTransfereeForCallId(this.props.call.callId); const isOnHold = this.state.isLocalOnHold || this.state.isRemoteOnHold; - let onHoldText = null; - if (this.state.isRemoteOnHold) { - const holdString = CallHandler.sharedInstance().hasAnyUnheldCall() ? - _td("You held the call Switch") : _td("You held the call Resume"); - onHoldText = _t(holdString, {}, { - a: sub => - {sub} - , - }); - } else if (this.state.isLocalOnHold) { - onHoldText = _t("%(peerName)s held the call", { - peerName: this.props.call.getOpponentMember().name, - }); + let holdTransferContent; + if (transfereeCall) { + const transferTargetRoom = MatrixClientPeg.get().getRoom(CallHandler.roomIdForCall(this.props.call)); + const transferTargetName = transferTargetRoom ? transferTargetRoom.name : _t("unknown person"); + + const transfereeRoom = MatrixClientPeg.get().getRoom( + CallHandler.roomIdForCall(transfereeCall), + ); + const transfereeName = transfereeRoom ? transfereeRoom.name : _t("unknown person"); + + holdTransferContent =
+ {_t( + "Consulting with %(transferTarget)s. Transfer to %(transferee)s", + { + transferTarget: transferTargetName, + transferee: transfereeName, + }, + { + a: sub => {sub}, + }, + )} +
; + } else if (isOnHold) { + let onHoldText = null; + if (this.state.isRemoteOnHold) { + const holdString = CallHandler.sharedInstance().hasAnyUnheldCall() ? + _td("You held the call Switch") : _td("You held the call Resume"); + onHoldText = _t(holdString, {}, { + a: sub => + {sub} + , + }); + } else if (this.state.isLocalOnHold) { + onHoldText = _t("%(peerName)s held the call", { + peerName: this.props.call.getOpponentMember().name, + }); + } + holdTransferContent =
+ {onHoldText} +
; } if (this.props.call.type === CallType.Video) { let localVideoFeed = null; - let onHoldContent = null; let onHoldBackground = null; const backgroundStyle: CSSProperties = {}; const containerClasses = classNames({ @@ -505,9 +537,6 @@ export default class CallView extends React.Component { mx_CallView_video_hold: isOnHold, }); if (isOnHold) { - onHoldContent =
- {onHoldText} -
; const backgroundAvatarUrl = avatarUrlForMember( // is it worth getting the size of the div to pass here? this.props.call.getOpponentMember(), 1024, 1024, 'crop', @@ -534,7 +563,7 @@ export default class CallView extends React.Component { maxHeight={maxVideoHeight} /> {localVideoFeed} - {onHoldContent} + {holdTransferContent} {callControls} ; } else { @@ -554,7 +583,7 @@ export default class CallView extends React.Component { /> -
{onHoldText}
+ {holdTransferContent} {callControls} ; } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 5f1003bf29..5ee799466d 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -876,6 +876,8 @@ "sends fireworks": "sends fireworks", "Sends the given message with snowfall": "Sends the given message with snowfall", "sends snowfall": "sends snowfall", + "unknown person": "unknown person", + "Consulting with %(transferTarget)s. Transfer to %(transferee)s": "Consulting with %(transferTarget)s. Transfer to %(transferee)s", "You held the call Switch": "You held the call Switch", "You held the call Resume": "You held the call Resume", "%(peerName)s held the call": "%(peerName)s held the call", @@ -2208,6 +2210,7 @@ "Invite someone using their name, email address, username (like ) or share this room.": "Invite someone using their name, email address, username (like ) or share this room.", "Invite someone using their name, username (like ) or share this room.": "Invite someone using their name, username (like ) or share this room.", "Transfer": "Transfer", + "Consult first": "Consult first", "a new master key signature": "a new master key signature", "a new cross-signing key signature": "a new cross-signing key signature", "a device cross-signing signature": "a device cross-signing signature", From c40f97fa2595a2565f17cc61a6843cbc8dbcbef6 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Fri, 26 Mar 2021 09:44:52 +0000 Subject: [PATCH 03/25] Add reset option for corrupted event index store --- .../views/dialogs/SeshatResetDialog.js | 55 +++++++++++++++++++ .../views/settings/EventIndexPanel.js | 16 ++++++ src/i18n/strings/en_EN.json | 3 + src/indexing/BaseEventIndexManager.ts | 10 ++++ src/indexing/EventIndexPeg.js | 5 ++ 5 files changed, 89 insertions(+) create mode 100644 src/components/views/dialogs/SeshatResetDialog.js diff --git a/src/components/views/dialogs/SeshatResetDialog.js b/src/components/views/dialogs/SeshatResetDialog.js new file mode 100644 index 0000000000..751af72383 --- /dev/null +++ b/src/components/views/dialogs/SeshatResetDialog.js @@ -0,0 +1,55 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; +import PropTypes from 'prop-types'; +import {_t} from "../../../languageHandler"; +import * as sdk from "../../../index"; +import {replaceableComponent} from "../../../utils/replaceableComponent"; + +@replaceableComponent("views.dialogs.SeshatResetDialog") +export default class SeshatResetDialog extends React.Component { + static propTypes = { + onFinished: PropTypes.func.isRequired, + }; + render() { + const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); + + return ( + +
+

+ {_t( + "Your event store appears corrupted. " + + "This action will restart this application.", + )} +

+
+ +
+ ); + } +} diff --git a/src/components/views/settings/EventIndexPanel.js b/src/components/views/settings/EventIndexPanel.js index a48583b61d..773f687524 100644 --- a/src/components/views/settings/EventIndexPanel.js +++ b/src/components/views/settings/EventIndexPanel.js @@ -26,6 +26,7 @@ import {formatBytes, formatCountLong} from "../../../utils/FormattingUtils"; import EventIndexPeg from "../../../indexing/EventIndexPeg"; import {SettingLevel} from "../../../settings/SettingLevel"; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import SeshatResetDialog from '../dialogs/SeshatResetDialog'; @replaceableComponent("views.settings.EventIndexPanel") export default class EventIndexPanel extends React.Component { @@ -122,6 +123,16 @@ export default class EventIndexPanel extends React.Component { await this.updateState(); } + _confirmEventStoreReset() { + Modal.createDialog(SeshatResetDialog, { + onFinished: (success) => { + if (success) { + EventIndexPeg.resetEventStore(); + } + }, + }); + } + render() { let eventIndexingSettings = null; const InlineSpinner = sdk.getComponent('elements.InlineSpinner'); @@ -220,6 +231,11 @@ export default class EventIndexPanel extends React.Component { {EventIndexPeg.error.message} +

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

)} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index b5edf31d01..9c72f1c1c5 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2300,6 +2300,9 @@ "Use your preferred Matrix homeserver if you have one, or host your own.": "Use your preferred Matrix homeserver if you have one, or host your own.", "Learn more": "Learn more", "About homeservers": "About homeservers", + "Reset event index store?": "Reset event index store?", + "Your event store appears corrupted. This action will restart this application.": "Your event store appears corrupted. This action will restart this application.", + "Reset event store": "Reset event store", "Sign out and remove encryption keys?": "Sign out and remove encryption keys?", "Clear Storage and Sign Out": "Clear Storage and Sign Out", "Send Logs": "Send Logs", diff --git a/src/indexing/BaseEventIndexManager.ts b/src/indexing/BaseEventIndexManager.ts index 2474406618..2ae47c6d86 100644 --- a/src/indexing/BaseEventIndexManager.ts +++ b/src/indexing/BaseEventIndexManager.ts @@ -309,4 +309,14 @@ export default abstract class BaseEventIndexManager { async deleteEventIndex(): Promise { throw new Error("Unimplemented"); } + + /** + * Reset a potentially corrupted event store + * + * @return {Promise} A promise that will resolve once the event store has + * been deleted. + */ + async resetEventStore(): Promise { + throw new Error("Unimplemented"); + } } diff --git a/src/indexing/EventIndexPeg.js b/src/indexing/EventIndexPeg.js index 7004efc554..c339ca4209 100644 --- a/src/indexing/EventIndexPeg.js +++ b/src/indexing/EventIndexPeg.js @@ -179,6 +179,11 @@ class EventIndexPeg { await indexManager.deleteEventIndex(); } } + + resetEventStore() { + const indexManager = PlatformPeg.get().getEventIndexingManager(); + return indexManager.resetEventStore(); + } } if (!global.mxEventIndexPeg) { From 989d69ba16069929de1102f58905b1f8228ef4f5 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 26 Mar 2021 14:21:58 +0000 Subject: [PATCH 04/25] Get tbe transfer target / transferee the right way around and also switch to the transfer target's room when we call them --- src/CallHandler.tsx | 2 +- src/components/views/dialogs/InviteDialog.tsx | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx index 270853865a..be687a4474 100644 --- a/src/CallHandler.tsx +++ b/src/CallHandler.tsx @@ -643,7 +643,7 @@ export default class CallHandler { this.calls.set(roomId, call); if (transferee) { - this.transferees[transferee.callId] = call; + this.transferees[call.callId] = transferee; } this.setCallListeners(call); diff --git a/src/components/views/dialogs/InviteDialog.tsx b/src/components/views/dialogs/InviteDialog.tsx index ad74e7bb02..0f38cb130f 100644 --- a/src/components/views/dialogs/InviteDialog.tsx +++ b/src/components/views/dialogs/InviteDialog.tsx @@ -738,6 +738,12 @@ export default class InviteDialog extends React.PureComponent Date: Sun, 28 Mar 2021 19:28:48 +1300 Subject: [PATCH 05/25] Make use of the KeyBindingManager in LeftPanel LeftPanel was making key action decisions based on the forwarded event. Use the KeyBindingManager now. Signed-off-by: Clemens Zeidler --- src/components/structures/LeftPanel.tsx | 17 +++++++++-------- src/components/structures/RoomSearch.tsx | 12 ++++++++---- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/src/components/structures/LeftPanel.tsx b/src/components/structures/LeftPanel.tsx index 2861cfd7e7..cbfc7b476b 100644 --- a/src/components/structures/LeftPanel.tsx +++ b/src/components/structures/LeftPanel.tsx @@ -34,7 +34,6 @@ import { UPDATE_EVENT } from "../../stores/AsyncStore"; import ResizeNotifier from "../../utils/ResizeNotifier"; import SettingsStore from "../../settings/SettingsStore"; import RoomListStore, { LISTS_UPDATE_EVENT } from "../../stores/room-list/RoomListStore"; -import {Key} from "../../Keyboard"; import IndicatorScrollbar from "../structures/IndicatorScrollbar"; import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton"; import { OwnProfileStore } from "../../stores/OwnProfileStore"; @@ -43,6 +42,7 @@ import LeftPanelWidget from "./LeftPanelWidget"; import {replaceableComponent} from "../../utils/replaceableComponent"; import {mediaFromMxc} from "../../customisations/Media"; import SpaceStore, {UPDATE_SELECTED_SPACE} from "../../stores/SpaceStore"; +import { getKeyBindingsManager, RoomListAction } from "../../KeyBindingsManager"; interface IProps { isMinimized: boolean; @@ -297,17 +297,18 @@ export default class LeftPanel extends React.Component { private onKeyDown = (ev: React.KeyboardEvent) => { if (!this.focusedElement) return; - switch (ev.key) { - case Key.ARROW_UP: - case Key.ARROW_DOWN: + const action = getKeyBindingsManager().getRoomListAction(ev); + switch (action) { + case RoomListAction.NextRoom: + case RoomListAction.PrevRoom: ev.stopPropagation(); ev.preventDefault(); - this.onMoveFocus(ev.key === Key.ARROW_UP); + this.onMoveFocus(action === RoomListAction.PrevRoom); break; } }; - private onEnter = () => { + private selectRoom = () => { const firstRoom = this.listContainerRef.current.querySelector(".mx_RoomTile"); if (firstRoom) { firstRoom.click(); @@ -388,8 +389,8 @@ export default class LeftPanel extends React.Component { > { break; case RoomListAction.NextRoom: case RoomListAction.PrevRoom: - this.props.onVerticalArrow(ev); + // we don't handle these actions here put pass the event on to the interested party (LeftPanel) + this.props.onKeyDown(ev); break; case RoomListAction.SelectRoom: { - const shouldClear = this.props.onEnter(ev); + const shouldClear = this.props.onSelectRoom(); if (shouldClear) { // wrap in set immediate to delay it so that we don't clear the filter & then change room setImmediate(() => { From 57cd8afbc49d84a2bd999ee6df8b5cd67ad0b6eb Mon Sep 17 00:00:00 2001 From: Clemens Zeidler Date: Sun, 28 Mar 2021 19:59:33 +1300 Subject: [PATCH 06/25] Split ApplySelection into CompleteOrPrevSelection and CompleteOrNextSelection When moving through the autocomplete selection list distinguish between the following cases: 1) When there is no autocomplete window open, only open one and select the first item when the CompleteOrPrevSelection / CompleteOrNextSelection actions are emitted (e.g. by pressing SHIFT + TAB, TAB) 2) Otherwise navigate through the selection list (e.g. SHIFT + TAB, TAB, UP, DOWN) - Remove references to raw keyboard events in autocomplete.ts - Clarify the purpose of startSelection (previously onTab) Signed-off-by: Clemens Zeidler --- src/KeyBindingsDefaults.ts | 8 ++++---- src/KeyBindingsManager.ts | 13 ++++++++---- .../views/rooms/BasicMessageComposer.tsx | 20 +++++++++---------- src/editor/autocomplete.ts | 12 +++++------ 4 files changed, 29 insertions(+), 24 deletions(-) diff --git a/src/KeyBindingsDefaults.ts b/src/KeyBindingsDefaults.ts index 0e9d14ea8f..ac9ef1f8cc 100644 --- a/src/KeyBindingsDefaults.ts +++ b/src/KeyBindingsDefaults.ts @@ -161,27 +161,27 @@ const messageComposerBindings = (): KeyBinding[] => { const autocompleteBindings = (): KeyBinding[] => { return [ { - action: AutocompleteAction.ApplySelection, + action: AutocompleteAction.CompleteOrNextSelection, keyCombo: { key: Key.TAB, }, }, { - action: AutocompleteAction.ApplySelection, + action: AutocompleteAction.CompleteOrNextSelection, keyCombo: { key: Key.TAB, ctrlKey: true, }, }, { - action: AutocompleteAction.ApplySelection, + action: AutocompleteAction.CompleteOrPrevSelection, keyCombo: { key: Key.TAB, shiftKey: true, }, }, { - action: AutocompleteAction.ApplySelection, + action: AutocompleteAction.CompleteOrPrevSelection, keyCombo: { key: Key.TAB, ctrlKey: true, diff --git a/src/KeyBindingsManager.ts b/src/KeyBindingsManager.ts index 45ef97b121..d862f10c02 100644 --- a/src/KeyBindingsManager.ts +++ b/src/KeyBindingsManager.ts @@ -52,14 +52,19 @@ export enum MessageComposerAction { /** Actions for text editing autocompletion */ export enum AutocompleteAction { - /** Apply the current autocomplete selection */ - ApplySelection = 'ApplySelection', - /** Cancel autocompletion */ - Cancel = 'Cancel', + /** + * Select previous selection or, if the autocompletion window is not shown, open the window and select the first + * selection. + */ + CompleteOrPrevSelection = 'ApplySelection', + /** Select next selection or, if the autocompletion window is not shown, open it and select the first selection */ + CompleteOrNextSelection = 'CompleteOrNextSelection', /** Move to the previous autocomplete selection */ PrevSelection = 'PrevSelection', /** Move to the next autocomplete selection */ NextSelection = 'NextSelection', + /** Close the autocompletion window */ + Cancel = 'Cancel', } /** Actions for the room list sidebar */ diff --git a/src/components/views/rooms/BasicMessageComposer.tsx b/src/components/views/rooms/BasicMessageComposer.tsx index 5dabd80399..9d9e3a1ba0 100644 --- a/src/components/views/rooms/BasicMessageComposer.tsx +++ b/src/components/views/rooms/BasicMessageComposer.tsx @@ -485,16 +485,14 @@ export default class BasicMessageEditor extends React.Component if (model.autoComplete && model.autoComplete.hasCompletions()) { const autoComplete = model.autoComplete; switch (autocompleteAction) { + case AutocompleteAction.CompleteOrPrevSelection: case AutocompleteAction.PrevSelection: - autoComplete.onUpArrow(event); + autoComplete.selectPreviousSelection(); handled = true; break; + case AutocompleteAction.CompleteOrNextSelection: case AutocompleteAction.NextSelection: - autoComplete.onDownArrow(event); - handled = true; - break; - case AutocompleteAction.ApplySelection: - autoComplete.onTab(event); + autoComplete.selectNextSelection(); handled = true; break; case AutocompleteAction.Cancel: @@ -504,8 +502,10 @@ export default class BasicMessageEditor extends React.Component default: return; // don't preventDefault on anything else } - } else if (autocompleteAction === AutocompleteAction.ApplySelection) { - this.tabCompleteName(event); + } else if (autocompleteAction === AutocompleteAction.CompleteOrPrevSelection + || autocompleteAction === AutocompleteAction.CompleteOrNextSelection) { + // there is no current autocomplete window, try to open it + this.tabCompleteName(); handled = true; } else if (event.key === Key.BACKSPACE || event.key === Key.DELETE) { this.formatBarRef.current.hide(); @@ -517,7 +517,7 @@ export default class BasicMessageEditor extends React.Component } }; - private async tabCompleteName(event: React.KeyboardEvent) { + private async tabCompleteName() { try { await new Promise(resolve => this.setState({showVisualBell: false}, resolve)); const {model} = this.props; @@ -540,7 +540,7 @@ export default class BasicMessageEditor extends React.Component // Don't try to do things with the autocomplete if there is none shown if (model.autoComplete) { - await model.autoComplete.onTab(event); + await model.autoComplete.startSelection(); if (!model.autoComplete.hasSelection()) { this.setState({showVisualBell: true}); model.autoComplete.close(); diff --git a/src/editor/autocomplete.ts b/src/editor/autocomplete.ts index d8cea961d4..2f56494ea0 100644 --- a/src/editor/autocomplete.ts +++ b/src/editor/autocomplete.ts @@ -68,24 +68,24 @@ export default class AutocompleteWrapperModel { this.updateCallback({close: true}); } - public async onTab(e: KeyboardEvent) { + /** + * If there is no current autocompletion, start one and move to the first selection. + */ + public async startSelection() { const acComponent = this.getAutocompleterComponent(); - if (acComponent.countCompletions() === 0) { // Force completions to show for the text currently entered await acComponent.forceComplete(); // Select the first item by moving "down" await acComponent.moveSelection(+1); - } else { - await acComponent.moveSelection(e.shiftKey ? -1 : +1); } } - public onUpArrow(e: KeyboardEvent) { + public selectPreviousSelection() { this.getAutocompleterComponent().moveSelection(-1); } - public onDownArrow(e: KeyboardEvent) { + public selectNextSelection() { this.getAutocompleterComponent().moveSelection(+1); } From be00320def83fa1f00b7daefd82956803dcc8a3a Mon Sep 17 00:00:00 2001 From: Clemens Zeidler Date: Sun, 28 Mar 2021 22:26:05 +1300 Subject: [PATCH 07/25] Make use of the KeyBindingsManager in the ScrollPanel Signed-off-by: Clemens Zeidler --- src/components/structures/LoggedInView.tsx | 1 + src/components/structures/ScrollPanel.js | 32 ++++++++-------------- 2 files changed, 12 insertions(+), 21 deletions(-) diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx index 5634c1a0c8..0255a3bf35 100644 --- a/src/components/structures/LoggedInView.tsx +++ b/src/components/structures/LoggedInView.tsx @@ -444,6 +444,7 @@ class LoggedInView extends React.Component { case RoomAction.RoomScrollDown: case RoomAction.JumpToFirstMessage: case RoomAction.JumpToLatestMessage: + // pass the event down to the scroll panel this._onScrollKeyPressed(ev); handled = true; break; diff --git a/src/components/structures/ScrollPanel.js b/src/components/structures/ScrollPanel.js index 3a9b2b8a77..976734680c 100644 --- a/src/components/structures/ScrollPanel.js +++ b/src/components/structures/ScrollPanel.js @@ -16,10 +16,10 @@ limitations under the License. import React, {createRef} from "react"; import PropTypes from 'prop-types'; -import { Key } from '../../Keyboard'; import Timer from '../../utils/Timer'; import AutoHideScrollbar from "./AutoHideScrollbar"; import {replaceableComponent} from "../../utils/replaceableComponent"; +import {getKeyBindingsManager, RoomAction} from "../../KeyBindingsManager"; const DEBUG_SCROLL = false; @@ -535,29 +535,19 @@ export default class ScrollPanel extends React.Component { * @param {object} ev the keyboard event */ handleScrollKey = ev => { - switch (ev.key) { - case Key.PAGE_UP: - if (!ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) { - this.scrollRelative(-1); - } + const roomAction = getKeyBindingsManager().getRoomAction(ev); + switch (roomAction) { + case RoomAction.ScrollUp: + this.scrollRelative(-1); break; - - case Key.PAGE_DOWN: - if (!ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) { - this.scrollRelative(1); - } + case RoomAction.RoomScrollDown: + this.scrollRelative(1); break; - - case Key.HOME: - if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) { - this.scrollToTop(); - } + case RoomAction.JumpToFirstMessage: + this.scrollToTop(); break; - - case Key.END: - if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) { - this.scrollToBottom(); - } + case RoomAction.JumpToLatestMessage: + this.scrollToBottom(); break; } }; From 4974cb43afcd168f70960d4563ce1f8481d9b9a5 Mon Sep 17 00:00:00 2001 From: Aaron Raimist Date: Sun, 28 Mar 2021 22:35:08 -0500 Subject: [PATCH 08/25] Prevent Re-request encryption keys from appearing under redacted messages Signed-off-by: Aaron Raimist --- src/components/views/rooms/EventTile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index 644d64d322..d51f4c00f1 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -936,7 +936,7 @@ export default class EventTile extends React.Component { ); const TooltipButton = sdk.getComponent('elements.TooltipButton'); - const keyRequestInfo = isEncryptionFailure ? + const keyRequestInfo = isEncryptionFailure && !isRedacted ?
{ keyRequestInfoContent } From e53a8ad992b593290230e7ca5631f5a3bd1f62b5 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 29 Mar 2021 08:44:10 +0100 Subject: [PATCH 09/25] Refactor SeshatResetDialog in TypeScript --- ...atResetDialog.js => SeshatResetDialog.tsx} | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) rename src/components/views/dialogs/{SeshatResetDialog.js => SeshatResetDialog.tsx} (71%) diff --git a/src/components/views/dialogs/SeshatResetDialog.js b/src/components/views/dialogs/SeshatResetDialog.tsx similarity index 71% rename from src/components/views/dialogs/SeshatResetDialog.js rename to src/components/views/dialogs/SeshatResetDialog.tsx index 751af72383..a351b9f15d 100644 --- a/src/components/views/dialogs/SeshatResetDialog.js +++ b/src/components/views/dialogs/SeshatResetDialog.tsx @@ -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. @@ -15,25 +15,24 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import {_t} from "../../../languageHandler"; -import * as sdk from "../../../index"; import {replaceableComponent} from "../../../utils/replaceableComponent"; -@replaceableComponent("views.dialogs.SeshatResetDialog") -export default class SeshatResetDialog extends React.Component { - static propTypes = { - onFinished: PropTypes.func.isRequired, - }; - render() { - const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); - const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); +import BaseDialog from "./BaseDialog"; +import DialogButtons from "../elements/DialogButtons"; +interface IProps { + onFinished(): void; +} + +@replaceableComponent("views.dialogs.SeshatResetDialog") +export default class SeshatResetDialog extends React.PureComponent { + render() { return ( + hasCancel={true} + onFinished={this.props.onFinished.bind(null, false)} + title={_t("Reset event index store?")}>

{_t( From b38f5c945920d98aa1964a6685e14ff647148468 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 29 Mar 2021 15:46:58 +0100 Subject: [PATCH 10/25] Simplify event index store reset flow --- .../views/dialogs/SeshatResetDialog.tsx | 4 ++-- .../views/settings/EventIndexPanel.js | 17 ++++++++++++----- src/indexing/BaseEventIndexManager.ts | 10 ---------- src/indexing/EventIndexPeg.js | 5 ----- 4 files changed, 14 insertions(+), 22 deletions(-) diff --git a/src/components/views/dialogs/SeshatResetDialog.tsx b/src/components/views/dialogs/SeshatResetDialog.tsx index a351b9f15d..09bf25f5cc 100644 --- a/src/components/views/dialogs/SeshatResetDialog.tsx +++ b/src/components/views/dialogs/SeshatResetDialog.tsx @@ -36,8 +36,8 @@ export default class SeshatResetDialog extends React.PureComponent {

{_t( - "Your event store appears corrupted. " + - "This action will restart this application.", + "Your event store appears to be corrupted. " + + "Your messages will be re-indexed as soon as the store is initialised.", )}

diff --git a/src/components/views/settings/EventIndexPanel.js b/src/components/views/settings/EventIndexPanel.js index 773f687524..049ba72d2f 100644 --- a/src/components/views/settings/EventIndexPanel.js +++ b/src/components/views/settings/EventIndexPanel.js @@ -123,11 +123,15 @@ export default class EventIndexPanel extends React.Component { await this.updateState(); } - _confirmEventStoreReset() { - Modal.createDialog(SeshatResetDialog, { - onFinished: (success) => { + _confirmEventStoreReset = () => { + const self = this; + const { close } = Modal.createDialog(SeshatResetDialog, { + onFinished: async (success) => { if (success) { - EventIndexPeg.resetEventStore(); + await SettingsStore.setValue('enableEventIndexing', null, SettingLevel.DEVICE, false); + await EventIndexPeg.deleteEventIndex(); + await self._onEnable(); + close(); } }, }); @@ -223,7 +227,10 @@ export default class EventIndexPanel extends React.Component { eventIndexingSettings = (

- {_t("Message search initilisation failed")} + {this.state.enabling + ? + : _t("Message search initilisation failed") + }

{EventIndexPeg.error && (
diff --git a/src/indexing/BaseEventIndexManager.ts b/src/indexing/BaseEventIndexManager.ts index 2ae47c6d86..2474406618 100644 --- a/src/indexing/BaseEventIndexManager.ts +++ b/src/indexing/BaseEventIndexManager.ts @@ -309,14 +309,4 @@ export default abstract class BaseEventIndexManager { async deleteEventIndex(): Promise { throw new Error("Unimplemented"); } - - /** - * Reset a potentially corrupted event store - * - * @return {Promise} A promise that will resolve once the event store has - * been deleted. - */ - async resetEventStore(): Promise { - throw new Error("Unimplemented"); - } } diff --git a/src/indexing/EventIndexPeg.js b/src/indexing/EventIndexPeg.js index c339ca4209..7004efc554 100644 --- a/src/indexing/EventIndexPeg.js +++ b/src/indexing/EventIndexPeg.js @@ -179,11 +179,6 @@ class EventIndexPeg { await indexManager.deleteEventIndex(); } } - - resetEventStore() { - const indexManager = PlatformPeg.get().getEventIndexingManager(); - return indexManager.resetEventStore(); - } } if (!global.mxEventIndexPeg) { From 2a4e327dbfd1a66edbc5e60de1826abf63bed841 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 29 Mar 2021 16:03:06 +0100 Subject: [PATCH 11/25] Change copy to point to native node modules docs in element desktop --- src/components/views/settings/EventIndexPanel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/settings/EventIndexPanel.js b/src/components/views/settings/EventIndexPanel.js index a48583b61d..f932d44933 100644 --- a/src/components/views/settings/EventIndexPanel.js +++ b/src/components/views/settings/EventIndexPanel.js @@ -167,7 +167,7 @@ export default class EventIndexPanel extends React.Component { ); } else if (EventIndexPeg.platformHasSupport() && !EventIndexPeg.supportIsInstalled()) { const nativeLink = ( - "https://github.com/vector-im/element-web/blob/develop/" + + "https://github.com/vector-im/element-desktop/blob/develop/" + "docs/native-node-modules.md#" + "adding-seshat-for-search-in-e2e-encrypted-rooms" ); From 3f33060cddb150a3b17cf381d51ccaa036581ebd Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Wed, 31 Mar 2021 11:15:16 +0100 Subject: [PATCH 12/25] increase default visible tiles for room sublists --- src/stores/room-list/ListLayout.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stores/room-list/ListLayout.ts b/src/stores/room-list/ListLayout.ts index caf2e92bd1..41887970ab 100644 --- a/src/stores/room-list/ListLayout.ts +++ b/src/stores/room-list/ListLayout.ts @@ -82,7 +82,7 @@ export class ListLayout { public get defaultVisibleTiles(): number { // This number is what "feels right", and mostly subject to design's opinion. - return 5; + return 8; } public tilesWithPadding(n: number, paddingPx: number): number { From 377b6c8a0537e0a92e09996d61670f1e398366a7 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Wed, 31 Mar 2021 11:41:22 +0100 Subject: [PATCH 13/25] Make user autocomplete query search beyond prefix --- src/autocomplete/UserProvider.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/autocomplete/UserProvider.tsx b/src/autocomplete/UserProvider.tsx index 7fc01daef9..eeff1497c2 100644 --- a/src/autocomplete/UserProvider.tsx +++ b/src/autocomplete/UserProvider.tsx @@ -56,7 +56,7 @@ export default class UserProvider extends AutocompleteProvider { this.matcher = new QueryMatcher([], { keys: ['name'], funcs: [obj => obj.userId.slice(1)], // index by user id minus the leading '@' - shouldMatchPrefix: true, + shouldMatchPrefix: false, shouldMatchWordsOnly: false, }); From c81847689a2147a758d88626cbae32b520a32678 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Wed, 31 Mar 2021 18:21:02 +0100 Subject: [PATCH 14/25] Remove query matcher shouldMatchPrefix support --- src/autocomplete/QueryMatcher.ts | 9 +-------- src/autocomplete/UserProvider.tsx | 1 - test/autocomplete/QueryMatcher-test.js | 14 -------------- 3 files changed, 1 insertion(+), 23 deletions(-) diff --git a/src/autocomplete/QueryMatcher.ts b/src/autocomplete/QueryMatcher.ts index a07ed29c7e..91fbea4d6a 100644 --- a/src/autocomplete/QueryMatcher.ts +++ b/src/autocomplete/QueryMatcher.ts @@ -23,7 +23,6 @@ interface IOptions { keys: Array; funcs?: Array<(T) => string>; shouldMatchWordsOnly?: boolean; - shouldMatchPrefix?: boolean; // whether to apply unhomoglyph and strip diacritics to fuzz up the search. Defaults to true fuzzy?: boolean; } @@ -56,12 +55,6 @@ export default class QueryMatcher { if (this._options.shouldMatchWordsOnly === undefined) { this._options.shouldMatchWordsOnly = true; } - - // By default, match anywhere in the string being searched. If enabled, only return - // matches that are prefixed with the query. - if (this._options.shouldMatchPrefix === undefined) { - this._options.shouldMatchPrefix = false; - } } setObjects(objects: T[]) { @@ -112,7 +105,7 @@ export default class QueryMatcher { resultKey = resultKey.replace(/[^\w]/g, ''); } const index = resultKey.indexOf(query); - if (index !== -1 && (!this._options.shouldMatchPrefix || index === 0)) { + if (index !== -1) { matches.push( ...candidates.map((candidate) => ({index, ...candidate})), ); diff --git a/src/autocomplete/UserProvider.tsx b/src/autocomplete/UserProvider.tsx index eeff1497c2..5f0cfc2df1 100644 --- a/src/autocomplete/UserProvider.tsx +++ b/src/autocomplete/UserProvider.tsx @@ -56,7 +56,6 @@ export default class UserProvider extends AutocompleteProvider { this.matcher = new QueryMatcher([], { keys: ['name'], funcs: [obj => obj.userId.slice(1)], // index by user id minus the leading '@' - shouldMatchPrefix: false, shouldMatchWordsOnly: false, }); diff --git a/test/autocomplete/QueryMatcher-test.js b/test/autocomplete/QueryMatcher-test.js index 2d0e10563b..3d383f08d7 100644 --- a/test/autocomplete/QueryMatcher-test.js +++ b/test/autocomplete/QueryMatcher-test.js @@ -183,18 +183,4 @@ describe('QueryMatcher', function() { expect(results.length).toBe(1); expect(results[0].name).toBe('bob'); }); - - it('Matches only by prefix with shouldMatchPrefix on', function() { - const qm = new QueryMatcher([ - {name: "Victoria"}, - {name: "Tori"}, - ], { - keys: ["name"], - shouldMatchPrefix: true, - }); - - const results = qm.match('tori'); - expect(results.length).toBe(1); - expect(results[0].name).toBe('Tori'); - }); }); From f0333b5b1caf7ccf2458646dc74fee31bf246365 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Fri, 26 Feb 2021 16:02:46 -0500 Subject: [PATCH 15/25] Show invite reasons Displays the reason for invitation in the invitation dialog, requiring a click to reveal the message. Signed-off-by: Robin Townsend --- res/css/views/rooms/_RoomPreviewBar.scss | 29 +++++++ res/themes/dark/css/_dark.scss | 2 + res/themes/legacy-dark/css/_legacy-dark.scss | 2 + .../legacy-light/css/_legacy-light.scss | 2 + res/themes/light/css/_light.scss | 2 + .../views/elements/EventTilePreview.tsx | 86 ++++++++++++------- src/components/views/rooms/RoomPreviewBar.js | 27 ++++++ .../tabs/user/AppearanceUserSettingsTab.tsx | 24 ++++++ src/i18n/strings/en_EN.json | 1 + 9 files changed, 144 insertions(+), 31 deletions(-) diff --git a/res/css/views/rooms/_RoomPreviewBar.scss b/res/css/views/rooms/_RoomPreviewBar.scss index 0b1da7a41c..b340080837 100644 --- a/res/css/views/rooms/_RoomPreviewBar.scss +++ b/res/css/views/rooms/_RoomPreviewBar.scss @@ -40,6 +40,35 @@ limitations under the License. word-break: break-word; } + .mx_RoomPreviewBar_reason { + text-align: left; + background-color: $primary-bg-color; + border: 1px solid $invite-reason-border-color; + border-radius: 10px; + padding: 0 16px 12px 16px; + margin: 5px 0 20px 0; + + div { + pointer-events: none; + } + + .mx_EventTile_msgOption { + display: none; + } + + .mx_MatrixChat_useCompactLayout & { + padding-top: 9px; + } + + &.mx_EventTilePreview_faded { + cursor: pointer; + + .mx_SenderProfile, .mx_EventTile_avatar { + opacity: 0.3; + } + } + } + .mx_Spinner { width: auto; height: auto; diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index a878aa3cdd..94463a41a4 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -205,6 +205,8 @@ $user-tile-hover-bg-color: $header-panel-bg-color; // Appearance tab colors $appearance-tab-border-color: $room-highlight-color; +$invite-reason-border-color: $room-highlight-color; + // blur amounts for left left panel (only for element theme, used in _mods.scss) $roomlist-background-blur-amount: 60px; $groupFilterPanel-background-blur-amount: 30px; diff --git a/res/themes/legacy-dark/css/_legacy-dark.scss b/res/themes/legacy-dark/css/_legacy-dark.scss index 3e3c299af9..8a938b7006 100644 --- a/res/themes/legacy-dark/css/_legacy-dark.scss +++ b/res/themes/legacy-dark/css/_legacy-dark.scss @@ -200,6 +200,8 @@ $user-tile-hover-bg-color: $header-panel-bg-color; // Appearance tab colors $appearance-tab-border-color: $room-highlight-color; +$invite-reason-border-color: $room-highlight-color; + $composer-shadow-color: tranparent; // ***** Mixins! ***** diff --git a/res/themes/legacy-light/css/_legacy-light.scss b/res/themes/legacy-light/css/_legacy-light.scss index a740ba155c..a107617c15 100644 --- a/res/themes/legacy-light/css/_legacy-light.scss +++ b/res/themes/legacy-light/css/_legacy-light.scss @@ -324,6 +324,8 @@ $user-tile-hover-bg-color: $header-panel-bg-color; // FontSlider colors $appearance-tab-border-color: $input-darker-bg-color; +$invite-reason-border-color: $input-darker-bg-color; + $composer-shadow-color: tranparent; // ***** Mixins! ***** diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index 1c89d83c01..6409b73351 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -325,6 +325,8 @@ $user-tile-hover-bg-color: $header-panel-bg-color; // FontSlider colors $appearance-tab-border-color: $input-darker-bg-color; +$invite-reason-border-color: $input-darker-bg-color; + // blur amounts for left left panel (only for element theme, used in _mods.scss) $roomlist-background-blur-amount: 40px; $groupFilterPanel-background-blur-amount: 20px; diff --git a/src/components/views/elements/EventTilePreview.tsx b/src/components/views/elements/EventTilePreview.tsx index 49c97831bc..d1f5ba9d62 100644 --- a/src/components/views/elements/EventTilePreview.tsx +++ b/src/components/views/elements/EventTilePreview.tsx @@ -19,7 +19,6 @@ import classnames from 'classnames'; import { MatrixEvent } from 'matrix-js-sdk/src/models/event'; import * as Avatar from '../../../Avatar'; -import { MatrixClientPeg } from '../../../MatrixClientPeg'; import EventTile from '../rooms/EventTile'; import SettingsStore from "../../../settings/SettingsStore"; import {Layout} from "../../../settings/Layout"; @@ -40,61 +39,84 @@ interface IProps { * classnames to apply to the wrapper of the preview */ className: string; + + /** + * The ID of the displayed user + */ + userId: string; + + /** + * The display name of the displayed user + */ + displayName?: string; + + /** + * The mxc:// avatar URL of the displayed user + */ + avatarUrl?: string; + + /** + * Whether the EventTile should appear faded + */ + faded?: boolean; + + /** + * Callback for when the component is clicked + */ + onClick?: () => void; } -/* eslint-disable camelcase */ interface IState { - userId: string; - displayname: string; - avatar_url: string; + message: string; + faded: boolean; + eventTileKey: number; } -/* eslint-enable camelcase */ const AVATAR_SIZE = 32; export default class EventTilePreview extends React.Component { constructor(props: IProps) { super(props); - this.state = { - userId: "@erim:fink.fink", - displayname: "Erimayas Fink", - avatar_url: null, + message: props.message, + faded: !!props.faded, + eventTileKey: 0, }; } - async componentDidMount() { - // Fetch current user data - const client = MatrixClientPeg.get(); - const userId = client.getUserId(); - const profileInfo = await client.getProfileInfo(userId); - const avatarUrl = Avatar.avatarUrlForUser( - {avatarUrl: profileInfo.avatar_url}, - AVATAR_SIZE, AVATAR_SIZE, "crop"); - + changeMessage(message: string) { this.setState({ - userId, - displayname: profileInfo.displayname, - avatar_url: avatarUrl, + message, + // Change the EventTile key to force React to create a new instance + eventTileKey: this.state.eventTileKey + 1, }); } - private fakeEvent({userId, displayname, avatar_url: avatarUrl}: IState) { + unfade() { + this.setState({ faded: false }); + } + + private fakeEvent({message}: IState) { + const avatarUrl = Avatar.avatarUrlForUser( + { avatarUrl: this.props.avatarUrl }, + AVATAR_SIZE, AVATAR_SIZE, "crop", + ); + // Fake it till we make it /* eslint-disable quote-props */ const rawEvent = { type: "m.room.message", - sender: userId, + sender: this.props.userId, content: { "m.new_content": { msgtype: "m.text", - body: this.props.message, - displayname: displayname, + body: message, + displayname: this.props.displayName, avatar_url: avatarUrl, }, msgtype: "m.text", - body: this.props.message, - displayname: displayname, + body: message, + displayname: this.props.displayName, avatar_url: avatarUrl, }, unsigned: { @@ -108,8 +130,8 @@ export default class EventTilePreview extends React.Component { // Fake it more event.sender = { - name: displayname, - userId: userId, + name: this.props.displayName, + userId: this.props.userId, getAvatarUrl: (..._) => { return avatarUrl; }, @@ -124,10 +146,12 @@ export default class EventTilePreview extends React.Component { const className = classnames(this.props.className, { "mx_IRCLayout": this.props.layout == Layout.IRC, "mx_GroupLayout": this.props.layout == Layout.Group, + "mx_EventTilePreview_faded": this.state.faded, }); - return
+ return
{ + this.reasonElement.current.unfade(); + this.reasonElement.current.changeMessage(reason); + }; + reasonElement =
+ { reasonElement }
{ secondaryButton } { extraComponents } diff --git a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx index 80a20d8afa..fa4983f15c 100644 --- a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx @@ -18,6 +18,7 @@ limitations under the License. import React from 'react'; import {_t} from "../../../../../languageHandler"; import SdkConfig from "../../../../../SdkConfig"; +import { MatrixClientPeg } from '../../../../../MatrixClientPeg'; import SettingsStore from "../../../../../settings/SettingsStore"; import { enumerateThemes } from "../../../../../theme"; import ThemeWatcher from "../../../../../settings/watchers/ThemeWatcher"; @@ -62,6 +63,10 @@ interface IState extends IThemeState { systemFont: string; showAdvanced: boolean; layout: Layout; + // User profile data for the message preview + userId: string; + displayName: string; + avatarUrl: string; } @@ -83,9 +88,25 @@ export default class AppearanceUserSettingsTab extends React.Component
Aa
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index e8a4b86c77..14db2fb6e6 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1525,6 +1525,7 @@ "Start chatting": "Start chatting", "Do you want to join %(roomName)s?": "Do you want to join %(roomName)s?", " invited you": " invited you", + "Invite messages are hidden by default. Click to show the message.": "Invite messages are hidden by default. Click to show the message.", "Reject": "Reject", "Reject & Ignore user": "Reject & Ignore user", "You're previewing %(roomName)s. Want to join it?": "You're previewing %(roomName)s. Want to join it?", From 70db749430dbffa9f2e74f961b7b5fbc6ada10e3 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 31 Mar 2021 23:36:36 -0600 Subject: [PATCH 16/25] Restabilize room list ordering with prefiltering on spaces/communities Fixes https://github.com/vector-im/element-web/issues/16799 This change replaces the "relative priority" system for filters with a kind model. The kind is used to differentiate and optimize when/where a filter condition is applied, resulting in a more stable ordering of the room list. The included documentation describes what this means in detail. This also introduces a way to inhibit updates being emitted from the Algorithm class given what we're doing to the poor thing will cause it to do a bunch of recalculation. Inhibiting the update and implicitly applying it (as part of our updateFn.mark()/trigger steps) results in much better performance. This has been tested on my own account with both communities and spaces of varying complexity: it feels faster, though the measurements appear to be within an error tolerance of each other (read: there's no performance impact of this). --- docs/room-list-store.md | 66 +++++---- src/stores/room-list/RoomListStore.ts | 132 ++++++++++++++---- src/stores/room-list/algorithms/Algorithm.ts | 42 +++--- .../filters/CommunityFilterCondition.ts | 9 +- .../room-list/filters/IFilterCondition.ts | 24 ++-- .../room-list/filters/NameFilterCondition.ts | 9 +- .../room-list/filters/SpaceFilterCondition.ts | 12 +- 7 files changed, 192 insertions(+), 102 deletions(-) diff --git a/docs/room-list-store.md b/docs/room-list-store.md index fa849e2505..f6330f5722 100644 --- a/docs/room-list-store.md +++ b/docs/room-list-store.md @@ -6,7 +6,7 @@ It's so complicated it needs its own README. Legend: * Orange = External event. -* Purple = Deterministic flow. +* Purple = Deterministic flow. * Green = Algorithm definition. * Red = Exit condition/point. * Blue = Process definition. @@ -24,8 +24,8 @@ algorithm to call, instead of having all the logic in the room list store itself Tag sorting is effectively the comparator supplied to the list algorithm. This gives the list algorithm -the power to decide when and how to apply the tag sorting, if at all. For example, the importance algorithm, -later described in this document, heavily uses the list ordering behaviour to break the tag into categories. +the power to decide when and how to apply the tag sorting, if at all. For example, the importance algorithm, +later described in this document, heavily uses the list ordering behaviour to break the tag into categories. Each category then gets sorted by the appropriate tag sorting algorithm. ### Tag sorting algorithm: Alphabetical @@ -36,7 +36,7 @@ useful. ### Tag sorting algorithm: Manual -Manual sorting makes use of the `order` property present on all tags for a room, per the +Manual sorting makes use of the `order` property present on all tags for a room, per the [Matrix specification](https://matrix.org/docs/spec/client_server/r0.6.0#room-tagging). Smaller values of `order` cause rooms to appear closer to the top of the list. @@ -74,7 +74,7 @@ relative (perceived) importance to the user: set to 'All Messages'. * **Bold**: The room has unread messages waiting for the user. Essentially this is a grey room without a badge/notification count (or 'Mentions Only'/'Muted'). -* **Idle**: No useful (see definition of useful above) activity has occurred in the room since the user +* **Idle**: No useful (see definition of useful above) activity has occurred in the room since the user last read it. Conveniently, each tag gets ordered by those categories as presented: red rooms appear above grey, grey @@ -82,7 +82,7 @@ above bold, etc. Once the algorithm has determined which rooms belong in which categories, the tag sorting algorithm gets applied to each category in a sub-list fashion. This should result in the red rooms (for example) -being sorted alphabetically amongst each other as well as the grey rooms sorted amongst each other, but +being sorted alphabetically amongst each other as well as the grey rooms sorted amongst each other, but collectively the tag will be sorted into categories with red being at the top. ## Sticky rooms @@ -103,48 +103,62 @@ receive another notification which causes the room to move into the topmost posi above the sticky room will move underneath to allow for the new room to take the top slot, maintaining the sticky room's position. -Though only applicable to the importance algorithm, the sticky room is not aware of category boundaries -and thus the user can see a shift in what kinds of rooms move around their selection. An example would -be the user having 4 red rooms, the user selecting the third room (leaving 2 above it), and then having -the rooms above it read on another device. This would result in 1 red room and 1 other kind of room +Though only applicable to the importance algorithm, the sticky room is not aware of category boundaries +and thus the user can see a shift in what kinds of rooms move around their selection. An example would +be the user having 4 red rooms, the user selecting the third room (leaving 2 above it), and then having +the rooms above it read on another device. This would result in 1 red room and 1 other kind of room above the sticky room as it will try to maintain 2 rooms above the sticky room. An exception for the sticky room placement is when there's suddenly not enough rooms to maintain the placement exactly. This typically happens if the user selects a room and leaves enough rooms where it cannot maintain the N required rooms above the sticky room. In this case, the sticky room will simply decrease N as needed. -The N value will never increase while selection remains unchanged: adding a bunch of rooms after having +The N value will never increase while selection remains unchanged: adding a bunch of rooms after having put the sticky room in a position where it's had to decrease N will not increase N. ## Responsibilities of the store -The store is responsible for the ordering, upkeep, and tracking of all rooms. The room list component simply gets -an object containing the tags it needs to worry about and the rooms within. The room list component will -decide which tags need rendering (as it commonly filters out empty tags in most cases), and will deal with +The store is responsible for the ordering, upkeep, and tracking of all rooms. The room list component simply gets +an object containing the tags it needs to worry about and the rooms within. The room list component will +decide which tags need rendering (as it commonly filters out empty tags in most cases), and will deal with all kinds of filtering. ## Filtering -Filters are provided to the store as condition classes, which are then passed along to the algorithm -implementations. The implementations then get to decide how to actually filter the rooms, however in -practice the base `Algorithm` class deals with the filtering in a more optimized/generic way. +Filters are provided to the store as condition classes and have two major kinds: Prefilters and Runtime. -The results of filters get cached to avoid needlessly iterating over potentially thousands of rooms, -as the old room list store does. When a filter condition changes, it emits an update which (in this -case) the `Algorithm` class will pick up and act accordingly. Typically, this also means filtering a +Prefilters flush out rooms which shouldn't appear to the algorithm implementations. Typically this is +due to some higher order room list filtering (such as spaces or tags) deliberately exposing a subset of +rooms to the user. The algorithm implementations will not see a room being prefiltered out. + +Runtime filters are used for more dynamic filtering, such as the user filtering by room name. These +filters are passed along to the algorithm implementations where those implementations decide how and +when to apply the filter. In practice, the base `Algorithm` class ends up doing the heavy lifting for +optimization reasons. + +The results of runtime filters get cached to avoid needlessly iterating over potentially thousands of +rooms, as the old room list store does. When a filter condition changes, it emits an update which (in this +case) the `Algorithm` class will pick up and act accordingly. Typically, this also means filtering a minor subset where possible to avoid over-iterating rooms. All filter conditions are considered "stable" by the consumers, meaning that the consumer does not expect a change in the condition unless the condition says it has changed. This is intentional to maintain the caching behaviour described above. +One might ask why we don't just use prefilter conditions for everything, and the answer is one of slight +subtly: in the cases of prefilters we are knowingly exposing the user to a workspace-style UX where +room notifications are self-contained within that workspace. Runtime filters tend to not want to affect +visible notification counts (as it doesn't want the room header to suddenly be confusing to the user as +they type), and occasionally UX like "found 2/12 rooms" is desirable. If prefiltering were used instead, +the notification counts would vary while the user was typing and "found 2/12" UX would not be possible. + ## Class breakdowns -The `RoomListStore` is the major coordinator of various algorithm implementations, which take care -of the various `ListAlgorithm` and `SortingAlgorithm` options. The `Algorithm` class is responsible -for figuring out which tags get which rooms, as Matrix specifies them as a reverse map: tags get -defined on rooms and are not defined as a collection of rooms (unlike how they are presented to the -user). Various list-specific utilities are also included, though they are expected to move somewhere -more general when needed. For example, the `membership` utilities could easily be moved elsewhere +The `RoomListStore` is the major coordinator of various algorithm implementations, which take care +of the various `ListAlgorithm` and `SortingAlgorithm` options. The `Algorithm` class is responsible +for figuring out which tags get which rooms, as Matrix specifies them as a reverse map: tags get +defined on rooms and are not defined as a collection of rooms (unlike how they are presented to the +user). Various list-specific utilities are also included, though they are expected to move somewhere +more general when needed. For example, the `membership` utilities could easily be moved elsewhere as needed. The various bits throughout the room list store should also have jsdoc of some kind to help describe diff --git a/src/stores/room-list/RoomListStore.ts b/src/stores/room-list/RoomListStore.ts index 074c2e569d..70ce6c1263 100644 --- a/src/stores/room-list/RoomListStore.ts +++ b/src/stores/room-list/RoomListStore.ts @@ -1,6 +1,5 @@ /* -Copyright 2018, 2019 New Vector Ltd -Copyright 2020 The Matrix.org Foundation C.I.C. +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. @@ -15,27 +14,27 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { MatrixClient } from "matrix-js-sdk/src/client"; +import {MatrixClient} from "matrix-js-sdk/src/client"; import SettingsStore from "../../settings/SettingsStore"; -import { DefaultTagID, isCustomTag, OrderedDefaultTagIDs, RoomUpdateCause, TagID } from "./models"; -import { Room } from "matrix-js-sdk/src/models/room"; -import { IListOrderingMap, ITagMap, ITagSortingMap, ListAlgorithm, SortAlgorithm } from "./algorithms/models"; -import { ActionPayload } from "../../dispatcher/payloads"; +import {DefaultTagID, isCustomTag, OrderedDefaultTagIDs, RoomUpdateCause, TagID} from "./models"; +import {Room} from "matrix-js-sdk/src/models/room"; +import {IListOrderingMap, ITagMap, ITagSortingMap, ListAlgorithm, SortAlgorithm} from "./algorithms/models"; +import {ActionPayload} from "../../dispatcher/payloads"; import defaultDispatcher from "../../dispatcher/dispatcher"; -import { readReceiptChangeIsFor } from "../../utils/read-receipts"; -import { FILTER_CHANGED, IFilterCondition } from "./filters/IFilterCondition"; -import { TagWatcher } from "./TagWatcher"; +import {readReceiptChangeIsFor} from "../../utils/read-receipts"; +import {FILTER_CHANGED, FilterKind, IFilterCondition} from "./filters/IFilterCondition"; +import {TagWatcher} from "./TagWatcher"; import RoomViewStore from "../RoomViewStore"; -import { Algorithm, LIST_UPDATED_EVENT } from "./algorithms/Algorithm"; -import { EffectiveMembership, getEffectiveMembership } from "../../utils/membership"; -import { isNullOrUndefined } from "matrix-js-sdk/src/utils"; +import {Algorithm, LIST_UPDATED_EVENT} from "./algorithms/Algorithm"; +import {EffectiveMembership, getEffectiveMembership} from "../../utils/membership"; +import {isNullOrUndefined} from "matrix-js-sdk/src/utils"; import RoomListLayoutStore from "./RoomListLayoutStore"; -import { MarkedExecution } from "../../utils/MarkedExecution"; -import { AsyncStoreWithClient } from "../AsyncStoreWithClient"; -import { NameFilterCondition } from "./filters/NameFilterCondition"; -import { RoomNotificationStateStore } from "../notifications/RoomNotificationStateStore"; -import { VisibilityProvider } from "./filters/VisibilityProvider"; -import { SpaceWatcher } from "./SpaceWatcher"; +import {MarkedExecution} from "../../utils/MarkedExecution"; +import {AsyncStoreWithClient} from "../AsyncStoreWithClient"; +import {NameFilterCondition} from "./filters/NameFilterCondition"; +import {RoomNotificationStateStore} from "../notifications/RoomNotificationStateStore"; +import {VisibilityProvider} from "./filters/VisibilityProvider"; +import {SpaceWatcher} from "./SpaceWatcher"; interface IState { tagsEnabled?: boolean; @@ -57,6 +56,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient { private initialListsGenerated = false; private algorithm = new Algorithm(); private filterConditions: IFilterCondition[] = []; + private prefilterConditions: IFilterCondition[] = []; private tagWatcher: TagWatcher; private spaceWatcher: SpaceWatcher; private updateFn = new MarkedExecution(() => { @@ -104,6 +104,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient { public async resetStore() { await this.reset(); this.filterConditions = []; + this.prefilterConditions = []; this.initialListsGenerated = false; this.setupWatchers(); @@ -435,6 +436,39 @@ export class RoomListStoreClass extends AsyncStoreWithClient { } } + private async recalculatePrefiltering() { + if (!this.algorithm) return; + if (!this.algorithm.hasTagSortingMap) return; // we're still loading + + if (SettingsStore.getValue("advancedRoomListLogging")) { + // TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602 + console.log("Calculating new prefiltered room list"); + } + + // Inhibit updates because we're about to lie heavily to the algorithm + this.algorithm.updatesInhibited = true; + + // Figure out which rooms are about to be valid, and the state of affairs + const rooms = this.getPlausibleRooms(); + const currentSticky = this.algorithm.stickyRoom; + const stickyIsStillPresent = currentSticky && rooms.includes(currentSticky); + + // Reset the sticky room before resetting the known rooms so the algorithm + // doesn't freak out. + await this.algorithm.setStickyRoom(null); + await this.algorithm.setKnownRooms(rooms); + + // Set the sticky room back, if needed, now that we have updated the store. + // This will use relative stickyness to the new room set. + if (stickyIsStillPresent) { + await this.algorithm.setStickyRoom(currentSticky); + } + + // Finally, mark an update and resume updates from the algorithm + this.updateFn.mark(); + this.algorithm.updatesInhibited = false; + } + public async setTagSorting(tagId: TagID, sort: SortAlgorithm) { await this.setAndPersistTagSorting(tagId, sort); this.updateFn.trigger(); @@ -557,6 +591,34 @@ export class RoomListStoreClass extends AsyncStoreWithClient { this.updateFn.trigger(); }; + private onPrefilterUpdated = async () => { + await this.recalculatePrefiltering(); + this.updateFn.trigger(); + }; + + private getPlausibleRooms(): Room[] { + if (!this.matrixClient) return []; + + let rooms = [ + ...this.matrixClient.getVisibleRooms(), + // also show space invites in the room list + ...this.matrixClient.getRooms().filter(r => r.isSpaceRoom() && r.getMyMembership() === "invite"), + ].filter(r => VisibilityProvider.instance.isRoomVisible(r)); + + if (this.prefilterConditions.length > 0) { + rooms = rooms.filter(r => { + for (const filter of this.prefilterConditions) { + if (!filter.isVisible(r)) { + return false; + } + } + return true; + }); + } + + return rooms; + } + /** * Regenerates the room whole room list, discarding any previous results. * @@ -568,11 +630,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient { public async regenerateAllLists({trigger = true}) { console.warn("Regenerating all room lists"); - const rooms = [ - ...this.matrixClient.getVisibleRooms(), - // also show space invites in the room list - ...this.matrixClient.getRooms().filter(r => r.isSpaceRoom() && r.getMyMembership() === "invite"), - ].filter(r => VisibilityProvider.instance.isRoomVisible(r)); + const rooms = this.getPlausibleRooms(); const customTags = new Set(); if (this.state.tagsEnabled) { @@ -606,11 +664,18 @@ export class RoomListStoreClass extends AsyncStoreWithClient { // TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602 console.log("Adding filter condition:", filter); } - this.filterConditions.push(filter); - if (this.algorithm) { - this.algorithm.addFilterCondition(filter); + let promise = Promise.resolve(); // use a promise to maintain sync API contract + if (filter.kind === FilterKind.Prefilter) { + filter.on(FILTER_CHANGED, this.onPrefilterUpdated); + this.prefilterConditions.push(filter); + promise = this.recalculatePrefiltering(); + } else { + this.filterConditions.push(filter); + if (this.algorithm) { + this.algorithm.addFilterCondition(filter); + } } - this.updateFn.trigger(); + promise.then(() => this.updateFn.trigger()); } public removeFilter(filter: IFilterCondition): void { @@ -618,7 +683,8 @@ export class RoomListStoreClass extends AsyncStoreWithClient { // TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602 console.log("Removing filter condition:", filter); } - const idx = this.filterConditions.indexOf(filter); + let promise = Promise.resolve(); // use a promise to maintain sync API contract + let idx = this.filterConditions.indexOf(filter); if (idx >= 0) { this.filterConditions.splice(idx, 1); @@ -626,7 +692,13 @@ export class RoomListStoreClass extends AsyncStoreWithClient { this.algorithm.removeFilterCondition(filter); } } - this.updateFn.trigger(); + idx = this.prefilterConditions.indexOf(filter); + if (idx >= 0) { + filter.off(FILTER_CHANGED, this.onPrefilterUpdated); + this.prefilterConditions.splice(idx, 1); + promise = this.recalculatePrefiltering(); + } + promise.then(() => this.updateFn.trigger()); } /** diff --git a/src/stores/room-list/algorithms/Algorithm.ts b/src/stores/room-list/algorithms/Algorithm.ts index fed3099325..83f333585d 100644 --- a/src/stores/room-list/algorithms/Algorithm.ts +++ b/src/stores/room-list/algorithms/Algorithm.ts @@ -1,5 +1,5 @@ /* -Copyright 2020 The Matrix.org Foundation C.I.C. +Copyright 2020, 2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -18,8 +18,7 @@ import { Room } from "matrix-js-sdk/src/models/room"; import { isNullOrUndefined } from "matrix-js-sdk/src/utils"; import DMRoomMap from "../../../utils/DMRoomMap"; import { EventEmitter } from "events"; -import { arrayDiff, arrayHasDiff, ArrayUtil } from "../../../utils/arrays"; -import { getEnumValues } from "../../../utils/enums"; +import { arrayDiff, arrayHasDiff } from "../../../utils/arrays"; import { DefaultTagID, RoomUpdateCause, TagID } from "../models"; import { IListOrderingMap, @@ -29,7 +28,7 @@ import { ListAlgorithm, SortAlgorithm, } from "./models"; -import { FILTER_CHANGED, FilterPriority, IFilterCondition } from "../filters/IFilterCondition"; +import { FILTER_CHANGED, FilterKind, IFilterCondition } from "../filters/IFilterCondition"; import { EffectiveMembership, getEffectiveMembership, splitRoomsByMembership } from "../../../utils/membership"; import { OrderingAlgorithm } from "./list-ordering/OrderingAlgorithm"; import { getListAlgorithmInstance } from "./list-ordering"; @@ -79,6 +78,11 @@ export class Algorithm extends EventEmitter { private allowedByFilter: Map = new Map(); private allowedRoomsByFilters: Set = new Set(); + /** + * Set to true to suspend emissions of algorithm updates. + */ + public updatesInhibited = false; + public constructor() { super(); } @@ -87,6 +91,14 @@ export class Algorithm extends EventEmitter { return this._stickyRoom ? this._stickyRoom.room : null; } + public get knownRooms(): Room[] { + return this.rooms; + } + + public get hasTagSortingMap(): boolean { + return !!this.sortAlgorithms; + } + protected get hasFilters(): boolean { return this.allowedByFilter.size > 0; } @@ -164,7 +176,7 @@ export class Algorithm extends EventEmitter { // If we removed the last filter, tell consumers that we've "updated" our filtered // view. This will trick them into getting the complete room list. - if (!this.hasFilters) { + if (!this.hasFilters && !this.updatesInhibited) { this.emit(LIST_UPDATED_EVENT); } } @@ -174,6 +186,7 @@ export class Algorithm extends EventEmitter { await this.recalculateFilteredRooms(); // re-emit the update so the list store can fire an off-cycle update if needed + if (this.updatesInhibited) return; this.emit(FILTER_CHANGED); } @@ -299,6 +312,7 @@ export class Algorithm extends EventEmitter { this.recalculateStickyRoom(); // Finally, trigger an update + if (this.updatesInhibited) return; this.emit(LIST_UPDATED_EVENT); } @@ -309,10 +323,6 @@ export class Algorithm extends EventEmitter { console.warn("Recalculating filtered room list"); const filters = Array.from(this.allowedByFilter.keys()); - const orderedFilters = new ArrayUtil(filters) - .groupBy(f => f.relativePriority) - .orderBy(getEnumValues(FilterPriority)) - .value; const newMap: ITagMap = {}; for (const tagId of Object.keys(this.cachedRooms)) { // Cheaply clone the rooms so we can more easily do operations on the list. @@ -322,16 +332,7 @@ export class Algorithm extends EventEmitter { this.tryInsertStickyRoomToFilterSet(rooms, tagId); let remainingRooms = rooms.map(r => r); let allowedRoomsInThisTag = []; - let lastFilterPriority = orderedFilters[0].relativePriority; - for (const filter of orderedFilters) { - if (filter.relativePriority !== lastFilterPriority) { - // Every time the filter changes priority, we want more specific filtering. - // To accomplish that, reset the variables to make it look like the process - // has started over, but using the filtered rooms as the seed. - remainingRooms = allowedRoomsInThisTag; - allowedRoomsInThisTag = []; - lastFilterPriority = filter.relativePriority; - } + for (const filter of filters) { const filteredRooms = remainingRooms.filter(r => filter.isVisible(r)); for (const room of filteredRooms) { const idx = remainingRooms.indexOf(room); @@ -350,6 +351,7 @@ export class Algorithm extends EventEmitter { const allowedRooms = Object.values(newMap).reduce((rv, v) => { rv.push(...v); return rv; }, []); this.allowedRoomsByFilters = new Set(allowedRooms); this.filteredRooms = newMap; + if (this.updatesInhibited) return; this.emit(LIST_UPDATED_EVENT); } @@ -404,6 +406,7 @@ export class Algorithm extends EventEmitter { if (!!this._cachedStickyRooms) { // Clear the cache if we won't be needing it this._cachedStickyRooms = null; + if (this.updatesInhibited) return; this.emit(LIST_UPDATED_EVENT); } return; @@ -446,6 +449,7 @@ export class Algorithm extends EventEmitter { } // Finally, trigger an update + if (this.updatesInhibited) return; this.emit(LIST_UPDATED_EVENT); } diff --git a/src/stores/room-list/filters/CommunityFilterCondition.ts b/src/stores/room-list/filters/CommunityFilterCondition.ts index fbdfefb983..a66bc01bce 100644 --- a/src/stores/room-list/filters/CommunityFilterCondition.ts +++ b/src/stores/room-list/filters/CommunityFilterCondition.ts @@ -1,5 +1,5 @@ /* -Copyright 2020 The Matrix.org Foundation C.I.C. +Copyright 2020, 2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,7 +15,7 @@ limitations under the License. */ import { Room } from "matrix-js-sdk/src/models/room"; -import { FILTER_CHANGED, FilterPriority, IFilterCondition } from "./IFilterCondition"; +import { FILTER_CHANGED, FilterKind, IFilterCondition } from "./IFilterCondition"; import { Group } from "matrix-js-sdk/src/models/group"; import { EventEmitter } from "events"; import GroupStore from "../../GroupStore"; @@ -39,9 +39,8 @@ export class CommunityFilterCondition extends EventEmitter implements IFilterCon this.onStoreUpdate(); // trigger a false update to seed the store } - public get relativePriority(): FilterPriority { - // Lowest priority so we can coarsely find rooms. - return FilterPriority.Lowest; + public get kind(): FilterKind { + return FilterKind.Prefilter; } public isVisible(room: Room): boolean { diff --git a/src/stores/room-list/filters/IFilterCondition.ts b/src/stores/room-list/filters/IFilterCondition.ts index 3b054eaece..cb9841a3c9 100644 --- a/src/stores/room-list/filters/IFilterCondition.ts +++ b/src/stores/room-list/filters/IFilterCondition.ts @@ -1,5 +1,5 @@ /* -Copyright 2020 The Matrix.org Foundation C.I.C. +Copyright 2020, 2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -19,10 +19,19 @@ import { EventEmitter } from "events"; export const FILTER_CHANGED = "filter_changed"; -export enum FilterPriority { - Lowest, - // in the middle would be Low, Normal, and High if we had a need - Highest, +export enum FilterKind { + /** + * A prefilter is one which coarsely determines which rooms are + * available for runtime filtering/rendering. Typically this will + * be things like Space selection. + */ + Prefilter, + + /** + * Runtime filters operate on the data set exposed by prefilters. + * Typically these are dynamic values like room name searching. + */ + Runtime, } /** @@ -39,10 +48,9 @@ export enum FilterPriority { */ export interface IFilterCondition extends EventEmitter { /** - * The relative priority that this filter should be applied with. - * Lower priorities get applied first. + * The kind of filter this presents. */ - relativePriority: FilterPriority; + kind: FilterKind; /** * Determines if a given room should be visible under this diff --git a/src/stores/room-list/filters/NameFilterCondition.ts b/src/stores/room-list/filters/NameFilterCondition.ts index 88edaecfb6..68c5a9bd6d 100644 --- a/src/stores/room-list/filters/NameFilterCondition.ts +++ b/src/stores/room-list/filters/NameFilterCondition.ts @@ -1,5 +1,5 @@ /* -Copyright 2020 The Matrix.org Foundation C.I.C. +Copyright 2020, 2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,7 +15,7 @@ limitations under the License. */ import { Room } from "matrix-js-sdk/src/models/room"; -import { FILTER_CHANGED, FilterPriority, IFilterCondition } from "./IFilterCondition"; +import { FILTER_CHANGED, FilterKind, IFilterCondition } from "./IFilterCondition"; import { EventEmitter } from "events"; import { removeHiddenChars } from "matrix-js-sdk/src/utils"; import { throttle } from "lodash"; @@ -31,9 +31,8 @@ export class NameFilterCondition extends EventEmitter implements IFilterConditio super(); } - public get relativePriority(): FilterPriority { - // We want this one to be at the highest priority so it can search within other filters. - return FilterPriority.Highest; + public get kind(): FilterKind { + return FilterKind.Runtime; } public get search(): string { diff --git a/src/stores/room-list/filters/SpaceFilterCondition.ts b/src/stores/room-list/filters/SpaceFilterCondition.ts index 49c58c9d1d..ad0ab88868 100644 --- a/src/stores/room-list/filters/SpaceFilterCondition.ts +++ b/src/stores/room-list/filters/SpaceFilterCondition.ts @@ -17,7 +17,7 @@ limitations under the License. import { EventEmitter } from "events"; import { Room } from "matrix-js-sdk/src/models/room"; -import { FILTER_CHANGED, FilterPriority, IFilterCondition } from "./IFilterCondition"; +import { FILTER_CHANGED, FilterKind, IFilterCondition } from "./IFilterCondition"; import { IDestroyable } from "../../../utils/IDestroyable"; import SpaceStore, {HOME_SPACE} from "../../SpaceStore"; import { setHasDiff } from "../../../utils/sets"; @@ -32,9 +32,8 @@ export class SpaceFilterCondition extends EventEmitter implements IFilterConditi private roomIds = new Set(); private space: Room = null; - public get relativePriority(): FilterPriority { - // Lowest priority so we can coarsely find rooms. - return FilterPriority.Lowest; + public get kind(): FilterKind { + return FilterKind.Prefilter; } public isVisible(room: Room): boolean { @@ -46,12 +45,7 @@ export class SpaceFilterCondition extends EventEmitter implements IFilterConditi this.roomIds = SpaceStore.instance.getSpaceFilteredRoomIds(this.space); if (setHasDiff(beforeRoomIds, this.roomIds)) { - // XXX: Room List Store has a bug where rooms which are synced after the filter is set - // are excluded from the filter, this is a workaround for it. this.emit(FILTER_CHANGED); - setTimeout(() => { - this.emit(FILTER_CHANGED); - }, 500); } }; From 746856ed103a136125c216ef89d1f406f7ecbbb4 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 31 Mar 2021 23:40:25 -0600 Subject: [PATCH 17/25] Appease the linter --- src/stores/room-list/algorithms/Algorithm.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/stores/room-list/algorithms/Algorithm.ts b/src/stores/room-list/algorithms/Algorithm.ts index 83f333585d..1d7000bfd2 100644 --- a/src/stores/room-list/algorithms/Algorithm.ts +++ b/src/stores/room-list/algorithms/Algorithm.ts @@ -28,7 +28,7 @@ import { ListAlgorithm, SortAlgorithm, } from "./models"; -import { FILTER_CHANGED, FilterKind, IFilterCondition } from "../filters/IFilterCondition"; +import { FILTER_CHANGED, IFilterCondition } from "../filters/IFilterCondition"; import { EffectiveMembership, getEffectiveMembership, splitRoomsByMembership } from "../../../utils/membership"; import { OrderingAlgorithm } from "./list-ordering/OrderingAlgorithm"; import { getListAlgorithmInstance } from "./list-ordering"; @@ -330,8 +330,8 @@ export class Algorithm extends EventEmitter { // to the rooms we know will be deduped by the Set. const rooms = this.cachedRooms[tagId].map(r => r); // cheap clone this.tryInsertStickyRoomToFilterSet(rooms, tagId); - let remainingRooms = rooms.map(r => r); - let allowedRoomsInThisTag = []; + const remainingRooms = rooms.map(r => r); + const allowedRoomsInThisTag = []; for (const filter of filters) { const filteredRooms = remainingRooms.filter(r => filter.isVisible(r)); for (const room of filteredRooms) { From a0049f956d72f3145609107611ea1e33248f4b97 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 31 Mar 2021 23:51:17 -0600 Subject: [PATCH 18/25] Patch over legacy Groups test --- test/components/views/rooms/RoomList-test.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/components/views/rooms/RoomList-test.js b/test/components/views/rooms/RoomList-test.js index 1c2a1c9992..fcdd71629e 100644 --- a/test/components/views/rooms/RoomList-test.js +++ b/test/components/views/rooms/RoomList-test.js @@ -296,6 +296,11 @@ describe('RoomList', () => { GroupStore._notifyListeners(); await waitForRoomListStoreUpdate(); + + // XXX: Even though the store updated, it can take a bit before the update makes + // it to the components. This gives it plenty of time to figure out what to do. + await (new Promise(resolve => setTimeout(resolve, 500))); + expectRoomInSubList(otherRoom, (s) => s.props.tagId === DefaultTagID.Untagged); }); From 343ce3b5027b2f439f74e29f3177e335f5cd0773 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 1 Apr 2021 00:02:05 -0600 Subject: [PATCH 19/25] Make log spam more quiet --- src/stores/room-list/algorithms/Algorithm.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/stores/room-list/algorithms/Algorithm.ts b/src/stores/room-list/algorithms/Algorithm.ts index 1d7000bfd2..83ee803115 100644 --- a/src/stores/room-list/algorithms/Algorithm.ts +++ b/src/stores/room-list/algorithms/Algorithm.ts @@ -516,7 +516,12 @@ export class Algorithm extends EventEmitter { if (isNullOrUndefined(rooms)) throw new Error(`Array of rooms cannot be null`); if (!this.sortAlgorithms) throw new Error(`Cannot set known rooms without a tag sorting map`); - console.warn("Resetting known rooms, initiating regeneration"); + if (!this.updatesInhibited) { + // We only log this if we're expecting to be publishing updates, which means that + // this could be an unexpected invocation. If we're inhibited, then this is probably + // an intentional invocation. + console.warn("Resetting known rooms, initiating regeneration"); + } // Before we go any further we need to clear (but remember) the sticky room to // avoid accidentally duplicating it in the list. From 4fcb25898192f7bd72ee2fcf78df9f493cce2ef7 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Thu, 1 Apr 2021 08:58:39 +0100 Subject: [PATCH 20/25] Refactor SeshatResetDialog props interface to use IDialogProps --- src/components/views/dialogs/SeshatResetDialog.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/components/views/dialogs/SeshatResetDialog.tsx b/src/components/views/dialogs/SeshatResetDialog.tsx index 09bf25f5cc..f40b7767f7 100644 --- a/src/components/views/dialogs/SeshatResetDialog.tsx +++ b/src/components/views/dialogs/SeshatResetDialog.tsx @@ -21,12 +21,10 @@ import {replaceableComponent} from "../../../utils/replaceableComponent"; import BaseDialog from "./BaseDialog"; import DialogButtons from "../elements/DialogButtons"; -interface IProps { - onFinished(): void; -} +import {IDialogProps} from "./IDialogProps"; @replaceableComponent("views.dialogs.SeshatResetDialog") -export default class SeshatResetDialog extends React.PureComponent { +export default class SeshatResetDialog extends React.PureComponent { render() { return ( Date: Thu, 1 Apr 2021 09:03:50 +0100 Subject: [PATCH 21/25] Update seshat reset dialog copy --- src/components/views/dialogs/SeshatResetDialog.tsx | 10 ++++++---- src/i18n/strings/en_EN.json | 5 +++-- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/components/views/dialogs/SeshatResetDialog.tsx b/src/components/views/dialogs/SeshatResetDialog.tsx index f40b7767f7..135f5d8197 100644 --- a/src/components/views/dialogs/SeshatResetDialog.tsx +++ b/src/components/views/dialogs/SeshatResetDialog.tsx @@ -30,12 +30,14 @@ export default class SeshatResetDialog extends React.PureComponent + title={_t("Reset event store?")}>

- {_t( - "Your event store appears to be corrupted. " + - "Your messages will be re-indexed as soon as the store is initialised.", + {_t("You most likely do not want to reset your event index store")} +
+ {_t("If you do, please note that none of your messages will be deleted, " + + "but the search experience might be degraded for a few moments" + + "whilst the index is recreated", )}

diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 9c72f1c1c5..db61122005 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2300,8 +2300,9 @@ "Use your preferred Matrix homeserver if you have one, or host your own.": "Use your preferred Matrix homeserver if you have one, or host your own.", "Learn more": "Learn more", "About homeservers": "About homeservers", - "Reset event index store?": "Reset event index store?", - "Your event store appears corrupted. This action will restart this application.": "Your event store appears corrupted. This action will restart this application.", + "Reset event store?": "Reset event store?", + "You most likely do not want to reset your event index store": "You most likely do not want to reset your event index store", + "If you do, please note that none of your messages will be deleted, but the search experience might be degraded for a few momentswhilst the index is recreated": "If you do, please note that none of your messages will be deleted, but the search experience might be degraded for a few momentswhilst the index is recreated", "Reset event store": "Reset event store", "Sign out and remove encryption keys?": "Sign out and remove encryption keys?", "Clear Storage and Sign Out": "Clear Storage and Sign Out", From d2f40a859bed446dd104258c94d74621a47cc2b6 Mon Sep 17 00:00:00 2001 From: Panagiotis <27917356+panoschal@users.noreply.github.com> Date: Thu, 1 Apr 2021 12:30:49 +0300 Subject: [PATCH 22/25] define PASSWORD_MIN_SCORE in one place and import from there --- src/components/structures/auth/ForgotPassword.js | 3 +-- src/components/views/auth/RegistrationForm.tsx | 2 +- src/components/views/settings/ChangePassword.js | 3 +-- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/components/structures/auth/ForgotPassword.js b/src/components/structures/auth/ForgotPassword.js index dfbb802d22..6188fdb5e4 100644 --- a/src/components/structures/auth/ForgotPassword.js +++ b/src/components/structures/auth/ForgotPassword.js @@ -29,8 +29,7 @@ import CountlyAnalytics from "../../../CountlyAnalytics"; import ServerPicker from "../../views/elements/ServerPicker"; import PassphraseField from '../../views/auth/PassphraseField'; import {replaceableComponent} from "../../../utils/replaceableComponent"; - -const PASSWORD_MIN_SCORE = 3; // safely unguessable: moderate protection from offline slow-hash scenario. +import { PASSWORD_MIN_SCORE } from '../../views/auth/RegistrationForm'; // Phases // Show the forgot password inputs diff --git a/src/components/views/auth/RegistrationForm.tsx b/src/components/views/auth/RegistrationForm.tsx index 85e0933be9..8f0a293a3c 100644 --- a/src/components/views/auth/RegistrationForm.tsx +++ b/src/components/views/auth/RegistrationForm.tsx @@ -40,7 +40,7 @@ enum RegistrationField { PasswordConfirm = "field_password_confirm", } -const PASSWORD_MIN_SCORE = 3; // safely unguessable: moderate protection from offline slow-hash scenario. +export const PASSWORD_MIN_SCORE = 3; // safely unguessable: moderate protection from offline slow-hash scenario. interface IProps { // Values pre-filled in the input boxes when the component loads diff --git a/src/components/views/settings/ChangePassword.js b/src/components/views/settings/ChangePassword.js index aa635ef974..3a7fb2e2b3 100644 --- a/src/components/views/settings/ChangePassword.js +++ b/src/components/views/settings/ChangePassword.js @@ -28,13 +28,12 @@ import Modal from "../../../Modal"; import PassphraseField from "../auth/PassphraseField"; import CountlyAnalytics from "../../../CountlyAnalytics"; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { PASSWORD_MIN_SCORE } from '../auth/RegistrationForm'; const FIELD_OLD_PASSWORD = 'field_old_password'; const FIELD_NEW_PASSWORD = 'field_new_password'; const FIELD_NEW_PASSWORD_CONFIRM = 'field_new_password_confirm'; -const PASSWORD_MIN_SCORE = 3; // safely unguessable: moderate protection from offline slow-hash scenario. - @replaceableComponent("views.settings.ChangePassword") export default class ChangePassword extends React.Component { static propTypes = { From af443c4cff2e78388ca1c28922d7b0833f5f880c Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 2 Apr 2021 19:33:16 -0600 Subject: [PATCH 23/25] Update docs/room-list-store.md Co-authored-by: J. Ryan Stinnett --- docs/room-list-store.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/room-list-store.md b/docs/room-list-store.md index f6330f5722..6fc5f71124 100644 --- a/docs/room-list-store.md +++ b/docs/room-list-store.md @@ -145,7 +145,7 @@ expect a change in the condition unless the condition says it has changed. This maintain the caching behaviour described above. One might ask why we don't just use prefilter conditions for everything, and the answer is one of slight -subtly: in the cases of prefilters we are knowingly exposing the user to a workspace-style UX where +subtlety: in the cases of prefilters we are knowingly exposing the user to a workspace-style UX where room notifications are self-contained within that workspace. Runtime filters tend to not want to affect visible notification counts (as it doesn't want the room header to suddenly be confusing to the user as they type), and occasionally UX like "found 2/12 rooms" is desirable. If prefiltering were used instead, From 479df8ac5f79ceab096454964d3a5825d04cd82e Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 2 Apr 2021 19:35:10 -0600 Subject: [PATCH 24/25] Clarify docs --- src/stores/room-list/RoomListStore.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/stores/room-list/RoomListStore.ts b/src/stores/room-list/RoomListStore.ts index 70ce6c1263..88df05b5d0 100644 --- a/src/stores/room-list/RoomListStore.ts +++ b/src/stores/room-list/RoomListStore.ts @@ -659,12 +659,17 @@ export class RoomListStoreClass extends AsyncStoreWithClient { if (trigger) this.updateFn.trigger(); } + /** + * Adds a filter condition to the room list store. Filters may be applied async, + * and thus might not cause an update to the store immediately. + * @param {IFilterCondition} filter The filter condition to add. + */ public addFilter(filter: IFilterCondition): void { if (SettingsStore.getValue("advancedRoomListLogging")) { // TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602 console.log("Adding filter condition:", filter); } - let promise = Promise.resolve(); // use a promise to maintain sync API contract + let promise = Promise.resolve(); if (filter.kind === FilterKind.Prefilter) { filter.on(FILTER_CHANGED, this.onPrefilterUpdated); this.prefilterConditions.push(filter); @@ -678,12 +683,19 @@ export class RoomListStoreClass extends AsyncStoreWithClient { promise.then(() => this.updateFn.trigger()); } + /** + * Removes a filter condition from the room list store. If the filter was + * not previously added to the room list store, this will no-op. The effects + * of removing a filter may be applied async and therefore might not cause + * an update right away. + * @param {IFilterCondition} filter The filter condition to remove. + */ public removeFilter(filter: IFilterCondition): void { if (SettingsStore.getValue("advancedRoomListLogging")) { // TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602 console.log("Removing filter condition:", filter); } - let promise = Promise.resolve(); // use a promise to maintain sync API contract + let promise = Promise.resolve(); let idx = this.filterConditions.indexOf(filter); if (idx >= 0) { this.filterConditions.splice(idx, 1); From 10cf362da52553e8187a5981bdf19da3f706ec31 Mon Sep 17 00:00:00 2001 From: Felix Krull Date: Tue, 6 Apr 2021 13:55:22 +0200 Subject: [PATCH 25/25] Fix viewing invitations when the inviter has no avatar set Signed-off-by: Felix Krull --- src/components/structures/GroupView.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index b006b323fb..ed6167cbe7 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -981,7 +981,7 @@ export default class GroupView extends React.Component {
; } - const httpInviterAvatar = this.state.inviterProfile + const httpInviterAvatar = this.state.inviterProfile && this.state.inviterProfile.avatarUrl ? mediaFromMxc(this.state.inviterProfile.avatarUrl).getSquareThumbnailHttp(36) : null;