From 325e2ba99b25b253db453a7b7027fc86e4e1582c Mon Sep 17 00:00:00 2001 From: Timo <16718859+toger5@users.noreply.github.com> Date: Wed, 5 Jan 2022 16:14:44 +0100 Subject: [PATCH] Right panel store refactor (#7313) Co-authored-by: J. Ryan Stinnett --- src/@types/global.d.ts | 2 +- src/TextForEvent.tsx | 10 +- src/components/structures/FilePanel.tsx | 4 +- src/components/structures/GroupView.js | 21 +- src/components/structures/LoggedInView.tsx | 7 +- src/components/structures/RightPanel.tsx | 212 ++++------ src/components/structures/RoomStatusBar.tsx | 3 + src/components/structures/RoomView.tsx | 41 +- src/components/structures/SpaceRoomView.tsx | 48 +-- src/components/structures/ThreadView.tsx | 18 +- src/components/structures/TimelinePanel.tsx | 1 + src/components/structures/UserView.tsx | 6 +- .../views/context_menus/RoomContextMenu.tsx | 12 +- .../views/elements/MemberEventListSummary.tsx | 12 +- .../views/groups/GroupMemberList.js | 10 +- .../messages/MKeyVerificationRequest.tsx | 10 +- src/components/views/right_panel/BaseCard.tsx | 19 +- .../views/right_panel/EncryptionPanel.tsx | 10 +- .../views/right_panel/GroupHeaderButtons.tsx | 2 +- .../views/right_panel/HeaderButtons.tsx | 45 +-- .../views/right_panel/RoomHeaderButtons.tsx | 23 +- .../views/right_panel/RoomSummaryCard.tsx | 27 +- .../views/right_panel/TimelineCard.tsx | 8 +- src/components/views/right_panel/UserInfo.tsx | 22 +- .../views/right_panel/WidgetCard.tsx | 13 +- src/components/views/rooms/AppsDrawer.tsx | 3 + src/components/views/rooms/MemberList.tsx | 2 +- src/components/views/rooms/RoomHeader.tsx | 2 +- src/components/views/rooms/RoomListHeader.tsx | 5 +- .../views/toasts/VerificationRequestToast.tsx | 18 +- src/dispatcher/actions.ts | 10 - src/dispatcher/dispatch-actions/threads.ts | 21 +- .../AfterRightPanelPhaseChangePayload.ts | 31 -- .../payloads/SetRightPanelPhasePayload.ts | 47 --- .../payloads/ToggleRightPanelPayload.ts | 27 -- src/settings/Settings.tsx | 21 +- .../handlers/DeviceSettingsHandler.ts | 25 -- src/stores/RightPanelStore.ts | 245 ------------ src/stores/right-panel/RightPanelStore.ts | 370 ++++++++++++++++++ .../right-panel/RightPanelStoreIPanelState.ts | 136 +++++++ .../RightPanelStorePhases.ts | 0 src/verification.ts | 27 +- 42 files changed, 765 insertions(+), 811 deletions(-) delete mode 100644 src/dispatcher/payloads/AfterRightPanelPhaseChangePayload.ts delete mode 100644 src/dispatcher/payloads/SetRightPanelPhasePayload.ts delete mode 100644 src/dispatcher/payloads/ToggleRightPanelPayload.ts delete mode 100644 src/stores/RightPanelStore.ts create mode 100644 src/stores/right-panel/RightPanelStore.ts create mode 100644 src/stores/right-panel/RightPanelStoreIPanelState.ts rename src/stores/{ => right-panel}/RightPanelStorePhases.ts (100%) diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index c11e34c663..7b1a95ac55 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -32,7 +32,7 @@ import SettingsStore from "../settings/SettingsStore"; import { ActiveRoomObserver } from "../ActiveRoomObserver"; import { Notifier } from "../Notifier"; import type { Renderer } from "react-dom"; -import RightPanelStore from "../stores/RightPanelStore"; +import RightPanelStore from "../stores/right-panel/RightPanelStore"; import WidgetStore from "../stores/WidgetStore"; import CallHandler from "../CallHandler"; import { Analytics } from "../Analytics"; diff --git a/src/TextForEvent.tsx b/src/TextForEvent.tsx index 639bec8a3d..14d4768e83 100644 --- a/src/TextForEvent.tsx +++ b/src/TextForEvent.tsx @@ -27,12 +27,12 @@ import { isValid3pidInvite } from "./RoomInvite"; import SettingsStore from "./settings/SettingsStore"; import { ALL_RULE_TYPES, ROOM_RULE_TYPES, SERVER_RULE_TYPES, USER_RULE_TYPES } from "./mjolnir/BanList"; import { WIDGET_LAYOUT_EVENT_TYPE } from "./stores/widgets/WidgetLayoutStore"; -import { RightPanelPhases } from './stores/RightPanelStorePhases'; +import { RightPanelPhases } from './stores/right-panel/RightPanelStorePhases'; import { Action } from './dispatcher/actions'; import defaultDispatcher from './dispatcher/dispatcher'; -import { SetRightPanelPhasePayload } from './dispatcher/payloads/SetRightPanelPhasePayload'; import { MatrixClientPeg } from "./MatrixClientPeg"; import { ROOM_SECURITY_TAB } from "./components/views/dialogs/RoomSettingsDialog"; +import RightPanelStore from './stores/right-panel/RightPanelStore'; // These functions are frequently used just to check whether an event has // any text to display at all. For this reason they return deferred values @@ -503,11 +503,7 @@ const onPinnedOrUnpinnedMessageClick = (messageId: string, roomId: string): void }; const onPinnedMessagesClick = (): void => { - defaultDispatcher.dispatch({ - action: Action.SetRightPanelPhase, - phase: RightPanelPhases.PinnedMessages, - allowClose: false, - }); + RightPanelStore.instance.setCard({ phase: RightPanelPhases.PinnedMessages }, false); }; function textForPinnedEvent(event: MatrixEvent, allowJSX: boolean): () => string | JSX.Element | null { diff --git a/src/components/structures/FilePanel.tsx b/src/components/structures/FilePanel.tsx index 9a64b1180c..63b044b6e5 100644 --- a/src/components/structures/FilePanel.tsx +++ b/src/components/structures/FilePanel.tsx @@ -27,9 +27,9 @@ import { logger } from "matrix-js-sdk/src/logger"; import { MatrixClientPeg } from '../../MatrixClientPeg'; import EventIndexPeg from "../../indexing/EventIndexPeg"; import { _t } from '../../languageHandler'; -import BaseCard from "../views/right_panel/BaseCard"; -import { RightPanelPhases } from "../../stores/RightPanelStorePhases"; +import { RightPanelPhases } from "../../stores/right-panel/RightPanelStorePhases"; import DesktopBuildsNotice, { WarningKind } from "../views/elements/DesktopBuildsNotice"; +import BaseCard from "../views/right_panel/BaseCard"; import { replaceableComponent } from "../../utils/replaceableComponent"; import ResizeNotifier from '../../utils/ResizeNotifier'; import TimelinePanel from "./TimelinePanel"; diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index da32b4f8ae..f891c4100e 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -38,13 +38,14 @@ import GroupStore from '../../stores/GroupStore'; import FlairStore from '../../stores/FlairStore'; import { showGroupAddRoomDialog } from '../../GroupAddressPicker'; import { makeGroupPermalink, makeUserPermalink } from "../../utils/permalinks/Permalinks"; -import RightPanelStore from "../../stores/RightPanelStore"; +import RightPanelStore from "../../stores/right-panel/RightPanelStore"; import AutoHideScrollbar from "./AutoHideScrollbar"; import { mediaFromMxc } from "../../customisations/Media"; import { replaceableComponent } from "../../utils/replaceableComponent"; import { createSpaceFromCommunity } from "../../utils/space"; import { Action } from "../../dispatcher/actions"; -import { RightPanelPhases } from "../../stores/RightPanelStorePhases"; +import { RightPanelPhases } from "../../stores/right-panel/RightPanelStorePhases"; +import { UPDATE_EVENT } from "../../stores/AsyncStore"; const LONG_DESC_PLACEHOLDER = _td( `

HTML for your community's page

@@ -427,7 +428,7 @@ export default class GroupView extends React.Component { membershipBusy: false, publicityBusy: false, inviterProfile: null, - showRightPanel: RightPanelStore.getSharedInstance().isOpenForGroup, + showRightPanel: RightPanelStore.instance.isOpenForGroup, showUpgradeNotice: !localStorage.getItem(UPGRADE_NOTICE_LS_KEY), }; @@ -439,7 +440,7 @@ export default class GroupView extends React.Component { this._initGroupStore(this.props.groupId, true); this._dispatcherRef = dis.register(this._onAction); - this._rightPanelStoreToken = RightPanelStore.getSharedInstance().addListener(this._onRightPanelStoreUpdate); + RightPanelStore.instance.on(UPDATE_EVENT, this._onRightPanelStoreUpdate); } componentWillUnmount() { @@ -447,10 +448,7 @@ export default class GroupView extends React.Component { this._matrixClient.removeListener("Group.myMembership", this._onGroupMyMembership); dis.unregister(this._dispatcherRef); - // Remove RightPanelStore listener - if (this._rightPanelStoreToken) { - this._rightPanelStoreToken.remove(); - } + RightPanelStore.instance.off(UPDATE_EVENT, this._onRightPanelStoreUpdate); } // TODO: [REACT-WARNING] Replace with appropriate lifecycle event @@ -468,7 +466,7 @@ export default class GroupView extends React.Component { _onRightPanelStoreUpdate = () => { this.setState({ - showRightPanel: RightPanelStore.getSharedInstance().isOpenForGroup, + showRightPanel: RightPanelStore.instance.isOpenForGroup, }); }; @@ -824,10 +822,7 @@ export default class GroupView extends React.Component { }; _onAdminsLinkClick = () => { - dis.dispatch({ - action: Action.SetRightPanelPhase, - phase: RightPanelPhases.GroupMemberList, - }); + RightPanelStore.instance.setCard({ phase: RightPanelPhases.GroupMemberList }); }; _getGroupSection() { diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx index 83cde3d04b..f270d6273e 100644 --- a/src/components/structures/LoggedInView.tsx +++ b/src/components/structures/LoggedInView.tsx @@ -44,7 +44,6 @@ import CallContainer from '../views/voip/CallContainer'; import { ViewRoomDeltaPayload } from "../../dispatcher/payloads/ViewRoomDeltaPayload"; import RoomListStore from "../../stores/room-list/RoomListStore"; import NonUrgentToastContainer from "./NonUrgentToastContainer"; -import { ToggleRightPanelPayload } from "../../dispatcher/payloads/ToggleRightPanelPayload"; import { IOOBData, IThreepidInvite } from "../../stores/ThreepidInviteStore"; import Modal from "../../Modal"; import { ICollapseConfig } from "../../resizer/distributors/collapse"; @@ -68,6 +67,7 @@ import GroupFilterPanel from './GroupFilterPanel'; import CustomRoomTagPanel from './CustomRoomTagPanel'; import { mediaFromMxc } from "../../customisations/Media"; import LegacyCommunityPreview from "./LegacyCommunityPreview"; +import RightPanelStore from '../../stores/right-panel/RightPanelStore'; // We need to fetch each pinned message individually (if we don't already have it) // so each pinned message may trigger a request. Limit the number per room for sanity. @@ -489,10 +489,7 @@ class LoggedInView extends React.Component { break; case NavigationAction.ToggleRoomSidePanel: if (this.props.page_type === "room_view" || this.props.page_type === "group_view") { - dis.dispatch({ - action: Action.ToggleRightPanel, - type: this.props.page_type === "room_view" ? "room" : "group", - }); + RightPanelStore.instance.togglePanel(); handled = true; } break; diff --git a/src/components/structures/RightPanel.tsx b/src/components/structures/RightPanel.tsx index f0b5918581..b01f7b9010 100644 --- a/src/components/structures/RightPanel.tsx +++ b/src/components/structures/RightPanel.tsx @@ -20,17 +20,12 @@ import { Room } from "matrix-js-sdk/src/models/room"; import { RoomState } from "matrix-js-sdk/src/models/room-state"; import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; -import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; import { throttle } from 'lodash'; import dis from '../../dispatcher/dispatcher'; import GroupStore from '../../stores/GroupStore'; -import { - RIGHT_PANEL_PHASES_NO_ARGS, - RIGHT_PANEL_SPACE_PHASES, - RightPanelPhases, -} from "../../stores/RightPanelStorePhases"; -import RightPanelStore from "../../stores/RightPanelStore"; +import { RightPanelPhases } from '../../stores/right-panel/RightPanelStorePhases'; +import RightPanelStore from "../../stores/right-panel/RightPanelStore"; import MatrixClientContext from "../../contexts/MatrixClientContext"; import { Action } from "../../dispatcher/actions"; import RoomSummaryCard from "../views/right_panel/RoomSummaryCard"; @@ -50,16 +45,17 @@ import ThreadPanel from "./ThreadPanel"; import NotificationPanel from "./NotificationPanel"; import ResizeNotifier from "../../utils/ResizeNotifier"; import PinnedMessagesCard from "../views/right_panel/PinnedMessagesCard"; -import SpaceStore from "../../stores/spaces/SpaceStore"; import { RoomPermalinkCreator } from '../../utils/permalinks/Permalinks'; import { E2EStatus } from '../../utils/ShieldUtils'; import { dispatchShowThreadsPanelEvent } from '../../dispatcher/dispatch-actions/threads'; import TimelineCard from '../views/right_panel/TimelineCard'; +import { UPDATE_EVENT } from '../../stores/AsyncStore'; +import { IRightPanelCard, IRightPanelCardState } from '../../stores/right-panel/RightPanelStoreIPanelState'; interface IProps { room?: Room; // if showing panels for a given room, this is set groupId?: string; // if showing panels for a given group, this is set - member?: RoomMember; // used if we know the room member ahead of opening the panel + overwriteCard?: IRightPanelCard; // used to display a custom card and ignoring the RightPanelStore (used for UserView) resizeNotifier: ResizeNotifier; permalinkCreator?: RoomPermalinkCreator; e2eStatus?: E2EStatus; @@ -68,17 +64,8 @@ interface IProps { interface IState { phase: RightPanelPhases; isUserPrivilegedInGroup?: boolean; - member?: RoomMember; - verificationRequest?: VerificationRequest; - verificationRequestPromise?: Promise; - space?: Room; - widgetId?: string; - groupRoomId?: string; - groupId?: string; - event: MatrixEvent; - initialEvent?: MatrixEvent; - initialEventHighlighted?: boolean; searchQuery: string; + cardState?: IRightPanelCardState; } @replaceableComponent("structures.RightPanel") @@ -89,11 +76,11 @@ export default class RightPanel extends React.Component { constructor(props, context) { super(props, context); + this.state = { - ...RightPanelStore.getSharedInstance().roomPanelPhaseParams, - phase: this.getPhaseFromProps(), + cardState: RightPanelStore.instance.currentCard?.state, + phase: RightPanelStore.instance.currentCard?.phase, isUserPrivilegedInGroup: null, - member: this.getUserForPanel(), searchQuery: "", }; } @@ -102,56 +89,11 @@ export default class RightPanel extends React.Component { this.forceUpdate(); }, 500, { leading: true, trailing: true }); - // Helper function to split out the logic for getPhaseFromProps() and the constructor - // as both are called at the same time in the constructor. - private getUserForPanel(): RoomMember { - if (this.state && this.state.member) return this.state.member; - const lastParams = RightPanelStore.getSharedInstance().roomPanelPhaseParams; - return this.props.member || lastParams['member']; - } - - // gets the current phase from the props and also maybe the store - private getPhaseFromProps() { - const rps = RightPanelStore.getSharedInstance(); - const userForPanel = this.getUserForPanel(); - if (this.props.groupId) { - if (!RIGHT_PANEL_PHASES_NO_ARGS.includes(rps.groupPanelPhase)) { - dis.dispatch({ action: Action.SetRightPanelPhase, phase: RightPanelPhases.GroupMemberList }); - return RightPanelPhases.GroupMemberList; - } - return rps.groupPanelPhase; - } else if (SpaceStore.spacesEnabled && this.props.room?.isSpaceRoom() - && !RIGHT_PANEL_SPACE_PHASES.includes(rps.roomPanelPhase) - ) { - return RightPanelPhases.SpaceMemberList; - } else if (userForPanel) { - // XXX FIXME AAAAAARGH: What is going on with this class!? It takes some of its state - // from its props and some from a store, except if the contents of the store changes - // while it's mounted in which case it replaces all of its state with that of the store, - // except it uses a dispatch instead of a normal store listener? - // Unfortunately rewriting this would almost certainly break showing the right panel - // in some of the many cases, and I don't have time to re-architect it and test all - // the flows now, so adding yet another special case so if the store thinks there is - // a verification going on for the member we're displaying, we show that, otherwise - // we race if a verification is started while the panel isn't displayed because we're - // not mounted in time to get the dispatch. - // Until then, let this code serve as a warning from history. - if ( - rps.roomPanelPhaseParams.member && - userForPanel.userId === rps.roomPanelPhaseParams.member.userId && - rps.roomPanelPhaseParams.verificationRequest - ) { - return rps.roomPanelPhase; - } - return RightPanelPhases.RoomMemberInfo; - } - return rps.roomPanelPhase; - } - public componentDidMount(): void { this.dispatcherRef = dis.register(this.onAction); const cli = this.context; cli.on("RoomState.members", this.onRoomStateMember); + RightPanelStore.instance.on(UPDATE_EVENT, this.onRightPanelStoreUpdate); this.initGroupStore(this.props.groupId); } @@ -160,6 +102,7 @@ export default class RightPanel extends React.Component { if (this.context) { this.context.removeListener("RoomState.members", this.onRoomStateMember); } + RightPanelStore.instance.off(UPDATE_EVENT, this.onRightPanelStoreUpdate); this.unregisterGroupStore(); } @@ -193,42 +136,36 @@ export default class RightPanel extends React.Component { // redraw the badge on the membership list if (this.state.phase === RightPanelPhases.RoomMemberList && member.roomId === this.props.room.roomId) { this.delayedUpdate(); - } else if (this.state.phase === RightPanelPhases.RoomMemberInfo && member.roomId === this.props.room.roomId && - member.userId === this.state.member.userId) { + } else if ( + this.state.phase === RightPanelPhases.RoomMemberInfo && member.roomId === this.props.room.roomId && + member.userId === this.state.cardState.member.userId + ) { // refresh the member info (e.g. new power level) this.delayedUpdate(); } }; + private onRightPanelStoreUpdate = () => { + const currentPanel = RightPanelStore.instance.currentCard; + this.setState({ + cardState: currentPanel.state, + phase: currentPanel.phase, + }); + }; + private onAction = (payload: ActionPayload) => { const isChangingRoom = payload.action === Action.ViewRoom && payload.room_id !== this.props.room.roomId; const isViewingThread = this.state.phase === RightPanelPhases.ThreadView; if (isChangingRoom && isViewingThread) { dispatchShowThreadsPanelEvent(); } - - if (payload.action === Action.AfterRightPanelPhaseChange) { - this.setState({ - phase: payload.phase, - groupRoomId: payload.groupRoomId, - groupId: payload.groupId, - member: payload.member, - event: payload.event, - initialEvent: payload.initialEvent, - initialEventHighlighted: payload.highlighted, - verificationRequest: payload.verificationRequest, - verificationRequestPromise: payload.verificationRequestPromise, - widgetId: payload.widgetId, - space: payload.space, - }); - } }; private onClose = () => { // XXX: There are three different ways of 'closing' this panel depending on what state // things are in... this knows far more than it should do about the state of the rest // of the app and is generally a bit silly. - if (this.props.member) { + if (this.props.overwriteCard?.state?.member) { // If we have a user prop then we're displaying a user from the 'user' page type // in LoggedInView, so need to change the page type to close the panel (we switch // to the home page which is not obviously the correct thing to do, but I'm not sure @@ -238,16 +175,12 @@ export default class RightPanel extends React.Component { }); } else if ( this.state.phase === RightPanelPhases.EncryptionPanel && - this.state.verificationRequest && this.state.verificationRequest.pending + this.state.cardState.verificationRequest && this.state.cardState.verificationRequest.pending ) { // When the user clicks close on the encryption panel cancel the pending request first if any - this.state.verificationRequest.cancel(); + this.state.cardState.verificationRequest.cancel(); } else { - // the RightPanelStore has no way of knowing which mode room/group it is in, so we handle closing here - dis.dispatch({ - action: Action.ToggleRightPanel, - type: this.props.groupId ? "group" : "room", - }); + RightPanelStore.instance.togglePanel(); } }; @@ -256,13 +189,14 @@ export default class RightPanel extends React.Component { }; public render(): JSX.Element { - let panel =
; + let card =
; const roomId = this.props.room ? this.props.room.roomId : undefined; - - switch (this.state.phase) { + const phase = this.props.overwriteCard?.phase ?? this.state.phase; + const cardState = this.props.overwriteCard?.state ?? this.state.cardState; + switch (phase) { case RightPanelPhases.RoomMemberList: if (roomId) { - panel = { } break; case RightPanelPhases.SpaceMemberList: - panel = { case RightPanelPhases.GroupMemberList: if (this.props.groupId) { - panel = ; + card = ; } break; case RightPanelPhases.GroupRoomList: - panel = ; + card = ; break; case RightPanelPhases.RoomMemberInfo: case RightPanelPhases.SpaceMemberInfo: - case RightPanelPhases.EncryptionPanel: - panel = ; break; - + } case RightPanelPhases.Room3pidMemberInfo: case RightPanelPhases.Space3pidMemberInfo: - panel = ; + card = ; break; case RightPanelPhases.GroupMemberInfo: - panel = ; + key={cardState.member.userId} + phase={phase} + onClose={this.onClose} + />; break; case RightPanelPhases.GroupRoomInfo: - panel = ; + key={cardState.groupRoomId} + />; break; case RightPanelPhases.NotificationPanel: - panel = ; + card = ; break; case RightPanelPhases.PinnedMessages: if (SettingsStore.getValue("feature_pinning")) { - panel = ; + card = ; } break; case RightPanelPhases.Timeline: if (!SettingsStore.getValue("feature_maximised_widgets")) break; - panel = { />; break; case RightPanelPhases.FilePanel: - panel = ; + card = ; break; case RightPanelPhases.ThreadView: - panel = ; + e2eStatus={this.props.e2eStatus} + />; break; case RightPanelPhases.ThreadPanel: - panel = { break; case RightPanelPhases.RoomSummary: - panel = ; + card = ; break; case RightPanelPhases.Widget: - panel = ; + card = ; break; } return ( ); } diff --git a/src/components/structures/RoomStatusBar.tsx b/src/components/structures/RoomStatusBar.tsx index 4bc13c7241..aad5bb5cc2 100644 --- a/src/components/structures/RoomStatusBar.tsx +++ b/src/components/structures/RoomStatusBar.tsx @@ -84,6 +84,7 @@ interface IState { @replaceableComponent("structures.RoomStatusBar") export default class RoomStatusBar extends React.PureComponent { + private unmounted = false; public static contextType = MatrixClientContext; constructor(props: IProps, context: typeof MatrixClientContext) { @@ -110,6 +111,7 @@ export default class RoomStatusBar extends React.PureComponent { } public componentWillUnmount(): void { + this.unmounted = true; // we may have entirely lost our client as we're logging out before clicking login on the guest bar... const client = this.context; if (client) { @@ -122,6 +124,7 @@ export default class RoomStatusBar extends React.PureComponent { if (state === "SYNCING" && prevState === "SYNCING") { return; } + if (this.unmounted) return; this.setState({ syncState: state, syncStateData: data, diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 73e5d4532c..6943503f01 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -53,7 +53,7 @@ import WidgetEchoStore from '../../stores/WidgetEchoStore'; import SettingsStore from "../../settings/SettingsStore"; import { Layout } from "../../settings/enums/Layout"; import AccessibleButton from "../views/elements/AccessibleButton"; -import RightPanelStore from "../../stores/RightPanelStore"; +import RightPanelStore from "../../stores/right-panel/RightPanelStore"; import { haveTileForEvent } from "../views/rooms/EventTile"; import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext"; import MatrixClientContext, { withMatrixClientHOC, MatrixClientProps } from "../../contexts/MatrixClientContext"; @@ -98,8 +98,7 @@ import { dispatchShowThreadEvent } from '../../dispatcher/dispatch-actions/threa import { fetchInitialEvent } from "../../utils/EventUtils"; import { ComposerType } from "../../dispatcher/payloads/ComposerInsertPayload"; import AppsDrawer from '../views/rooms/AppsDrawer'; -import { SetRightPanelPhasePayload } from '../../dispatcher/payloads/SetRightPanelPhasePayload'; -import { RightPanelPhases } from '../../stores/RightPanelStorePhases'; +import { RightPanelPhases } from '../../stores/right-panel/RightPanelStorePhases'; const DEBUG = false; let debuglog = function(msg: string) {}; @@ -214,7 +213,6 @@ export interface IRoomState { export class RoomView extends React.Component { private readonly dispatcherRef: string; private readonly roomStoreToken: EventSubscription; - private readonly rightPanelStoreToken: EventSubscription; private settingWatchers: string[]; private unmounted = false; @@ -246,7 +244,7 @@ export class RoomView extends React.Component { canPeek: false, showApps: false, isPeeking: false, - showRightPanel: RightPanelStore.getSharedInstance().isOpenForRoom, + showRightPanel: RightPanelStore.instance.isOpenForRoom, joining: false, atEndOfLiveTimeline: true, atEndOfLiveTimelineInit: false, @@ -289,7 +287,8 @@ export class RoomView extends React.Component { this.context.on("Event.decrypted", this.onEventDecrypted); // Start listening for RoomViewStore updates this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate); - this.rightPanelStoreToken = RightPanelStore.getSharedInstance().addListener(this.onRightPanelStoreUpdate); + + RightPanelStore.instance.on(UPDATE_EVENT, this.onRightPanelStoreUpdate); WidgetEchoStore.on(UPDATE_EVENT, this.onWidgetEchoStoreUpdate); WidgetStore.instance.on(UPDATE_EVENT, this.onWidgetStoreUpdate); @@ -337,13 +336,9 @@ export class RoomView extends React.Component { }); if (WidgetLayoutStore.instance.hasMaximisedWidget(this.state.room)) { // Show chat in right panel when a widget is maximised - dis.dispatch({ - action: Action.SetRightPanelPhase, - phase: RightPanelPhases.Timeline, - }); + RightPanelStore.instance.setCard({ phase: RightPanelPhases.Timeline }); } this.checkWidgets(this.state.room); - this.checkRightPanel(this.state.room); }; private checkWidgets = (room) => { @@ -361,22 +356,6 @@ export class RoomView extends React.Component { : MainSplitContentType.Timeline; }; - private checkRightPanel = (room) => { - // This is a hack to hide the chat. This should not be necessary once the right panel - // phase is stored per room. (need to be done after check widget so that mainSplitContentType is updated) - if ( - RightPanelStore.getSharedInstance().roomPanelPhase === RightPanelPhases.Timeline && - this.state.showRightPanel && - !WidgetLayoutStore.instance.hasMaximisedWidget(this.state.room) - ) { - // Two timelines are shown prevent this by hiding the right panel - dis.dispatch({ - action: Action.ToggleRightPanel, - type: "room", - }); - } - }; - private onReadReceiptsChange = () => { this.setState({ showReadReceipts: SettingsStore.getValue("showReadReceipts", this.state.roomId), @@ -754,11 +733,8 @@ export class RoomView extends React.Component { if (this.roomStoreToken) { this.roomStoreToken.remove(); } - // Remove RightPanelStore listener - if (this.rightPanelStoreToken) { - this.rightPanelStoreToken.remove(); - } + RightPanelStore.instance.off(UPDATE_EVENT, this.onRightPanelStoreUpdate); WidgetEchoStore.removeListener(UPDATE_EVENT, this.onWidgetEchoStoreUpdate); WidgetStore.instance.removeListener(UPDATE_EVENT, this.onWidgetStoreUpdate); @@ -793,7 +769,7 @@ export class RoomView extends React.Component { private onRightPanelStoreUpdate = () => { this.setState({ - showRightPanel: RightPanelStore.getSharedInstance().isOpenForRoom, + showRightPanel: RightPanelStore.instance.isOpenForRoom, }); }; @@ -1039,7 +1015,6 @@ export class RoomView extends React.Component { this.updateE2EStatus(room); this.updatePermissions(room); this.checkWidgets(room); - this.checkRightPanel(room); this.setState({ liveTimeline: room.getLiveTimeline(), diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx index 81ca9cbf5d..7ad894243b 100644 --- a/src/components/structures/SpaceRoomView.tsx +++ b/src/components/structures/SpaceRoomView.tsx @@ -18,7 +18,6 @@ import React, { RefObject, useContext, useRef, useState } from "react"; import { EventType } from "matrix-js-sdk/src/@types/event"; import { JoinRule, Preset } from "matrix-js-sdk/src/@types/partials"; import { Room } from "matrix-js-sdk/src/models/room"; -import { EventSubscription } from "fbemitter"; import { logger } from "matrix-js-sdk/src/logger"; import MatrixClientContext from "../../contexts/MatrixClientContext"; @@ -43,9 +42,8 @@ import MainSplit from './MainSplit'; import ErrorBoundary from "../views/elements/ErrorBoundary"; import { ActionPayload } from "../../dispatcher/payloads"; import RightPanel from "./RightPanel"; -import RightPanelStore from "../../stores/RightPanelStore"; -import { RightPanelPhases } from "../../stores/RightPanelStorePhases"; -import { SetRightPanelPhasePayload } from "../../dispatcher/payloads/SetRightPanelPhasePayload"; +import RightPanelStore from "../../stores/right-panel/RightPanelStore"; +import { RightPanelPhases } from "../../stores/right-panel/RightPanelStorePhases"; import { useStateArray } from "../../hooks/useStateArray"; import SpacePublicShare from "../views/spaces/SpacePublicShare"; import { @@ -85,6 +83,7 @@ import { useDispatcher } from "../../hooks/useDispatcher"; import { useRoomState } from "../../hooks/useRoomState"; import { shouldShowComponent } from "../../customisations/helpers/UIComponents"; import { UIComponent } from "../../settings/UIFeature"; +import { UPDATE_EVENT } from "../../stores/AsyncStore"; interface IProps { space: Room; @@ -164,10 +163,9 @@ const SpaceInfo = ({ space }: { space: Room }) => { kind="link" className="mx_SpaceRoomView_info_memberCount" onClick={() => { - defaultDispatcher.dispatch({ - action: Action.SetRightPanelPhase, + RightPanelStore.instance.setCard({ phase: RightPanelPhases.RoomMemberList, - refireParams: { space }, + state: { spaceId: space.roomId }, }); }} > @@ -473,11 +471,7 @@ const SpaceLanding = ({ space }: { space: Room }) => { } const onMembersClick = () => { - defaultDispatcher.dispatch({ - action: Action.SetRightPanelPhase, - phase: RightPanelPhases.RoomMemberList, - refireParams: { space }, - }); + RightPanelStore.instance.setCard({ phase: RightPanelPhases.RoomMemberList, state: { spaceId: space.roomId } }); }; return
@@ -796,7 +790,6 @@ export default class SpaceRoomView extends React.PureComponent { private readonly creator: string; private readonly dispatcherRef: string; - private readonly rightPanelStoreToken: EventSubscription; constructor(props, context) { super(props, context); @@ -813,18 +806,18 @@ export default class SpaceRoomView extends React.PureComponent { this.state = { phase, - showRightPanel: RightPanelStore.getSharedInstance().isOpenForRoom, + showRightPanel: RightPanelStore.instance.isOpenForRoom, myMembership: this.props.space.getMyMembership(), }; this.dispatcherRef = defaultDispatcher.register(this.onAction); - this.rightPanelStoreToken = RightPanelStore.getSharedInstance().addListener(this.onRightPanelStoreUpdate); + RightPanelStore.instance.on(UPDATE_EVENT, this.onRightPanelStoreUpdate); this.context.on("Room.myMembership", this.onMyMembership); } componentWillUnmount() { defaultDispatcher.unregister(this.dispatcherRef); - this.rightPanelStoreToken.remove(); + RightPanelStore.instance.off(UPDATE_EVENT, this.onRightPanelStoreUpdate); this.context.off("Room.myMembership", this.onMyMembership); } @@ -836,7 +829,7 @@ export default class SpaceRoomView extends React.PureComponent { private onRightPanelStoreUpdate = () => { this.setState({ - showRightPanel: RightPanelStore.getSharedInstance().isOpenForRoom, + showRightPanel: RightPanelStore.instance.isOpenForRoom, }); }; @@ -849,28 +842,19 @@ export default class SpaceRoomView extends React.PureComponent { if (payload.action !== Action.ViewUser && payload.action !== "view_3pid_invite") return; if (payload.action === Action.ViewUser && payload.member) { - defaultDispatcher.dispatch({ - action: Action.SetRightPanelPhase, + RightPanelStore.instance.setCard({ phase: RightPanelPhases.SpaceMemberInfo, - refireParams: { - space: this.props.space, - member: payload.member, - }, + state: { spaceId: this.props.space.roomId, member: payload.member }, }); } else if (payload.action === "view_3pid_invite" && payload.event) { - defaultDispatcher.dispatch({ - action: Action.SetRightPanelPhase, + RightPanelStore.instance.setCard({ phase: RightPanelPhases.Space3pidMemberInfo, - refireParams: { - space: this.props.space, - event: payload.event, - }, + state: { spaceId: this.props.space.roomId, member: payload.member }, }); } else { - defaultDispatcher.dispatch({ - action: Action.SetRightPanelPhase, + RightPanelStore.instance.setCard({ phase: RightPanelPhases.SpaceMemberList, - refireParams: { space: this.props.space }, + state: { spaceId: this.props.space.roomId }, }); } }; diff --git a/src/components/structures/ThreadView.tsx b/src/components/structures/ThreadView.tsx index e53394ad08..cb46b37fc7 100644 --- a/src/components/structures/ThreadView.tsx +++ b/src/components/structures/ThreadView.tsx @@ -20,7 +20,7 @@ import { Thread, ThreadEvent } from 'matrix-js-sdk/src/models/thread'; import { RelationType } from 'matrix-js-sdk/src/@types/event'; import BaseCard from "../views/right_panel/BaseCard"; -import { RightPanelPhases } from "../../stores/RightPanelStorePhases"; +import { RightPanelPhases } from "../../stores/right-panel/RightPanelStorePhases"; import { replaceableComponent } from "../../utils/replaceableComponent"; import ResizeNotifier from '../../utils/ResizeNotifier'; import { TileShape } from '../views/rooms/EventTile'; @@ -30,7 +30,6 @@ import { Layout } from '../../settings/enums/Layout'; import TimelinePanel from './TimelinePanel'; import dis from "../../dispatcher/dispatcher"; import { ActionPayload } from '../../dispatcher/payloads'; -import { SetRightPanelPhasePayload } from '../../dispatcher/payloads/SetRightPanelPhasePayload'; import { Action } from '../../dispatcher/actions'; import { MatrixClientPeg } from '../../MatrixClientPeg'; import { E2EStatus } from '../../utils/ShieldUtils'; @@ -40,7 +39,7 @@ import ContentMessages from '../../ContentMessages'; import UploadBar from './UploadBar'; import { _t } from '../../languageHandler'; import ThreadListContextMenu from '../views/context_menus/ThreadListContextMenu'; -import RightPanelStore from '../../stores/RightPanelStore'; +import RightPanelStore from '../../stores/right-panel/RightPanelStore'; import SettingsStore from '../../settings/SettingsStore'; import { WidgetLayoutStore } from '../../stores/widgets/WidgetLayoutStore'; @@ -52,7 +51,7 @@ interface IProps { permalinkCreator?: RoomPermalinkCreator; e2eStatus?: E2EStatus; initialEvent?: MatrixEvent; - initialEventHighlighted?: boolean; + isInitialEventHighlighted?: boolean; } interface IState { thread?: Thread; @@ -94,10 +93,7 @@ export default class ThreadView extends React.Component { } if (prevProps.room !== this.props.room) { - dis.dispatch({ - action: Action.SetRightPanelPhase, - phase: RightPanelPhases.RoomSummary, - }); + RightPanelStore.instance.setCard({ phase: RightPanelPhases.RoomSummary }); } } @@ -168,7 +164,7 @@ export default class ThreadView extends React.Component { }; private onScroll = (): void => { - if (this.props.initialEvent && this.props.initialEventHighlighted) { + if (this.props.initialEvent && this.props.isInitialEventHighlighted) { dis.dispatch({ action: Action.ViewRoom, room_id: this.props.room.roomId, @@ -189,7 +185,7 @@ export default class ThreadView extends React.Component { }; public render(): JSX.Element { - const highlightedEventId = this.props.initialEventHighlighted + const highlightedEventId = this.props.isInitialEventHighlighted ? this.props.initialEvent?.getId() : null; @@ -198,7 +194,7 @@ export default class ThreadView extends React.Component { event_id: this.state.thread?.id, }; - let previousPhase = RightPanelStore.getSharedInstance().previousPhase; + let previousPhase = RightPanelStore.instance.previousCard.phase; if (!SettingsStore.getValue("feature_maximised_widgets")) { previousPhase = RightPanelPhases.ThreadPanel; } diff --git a/src/components/structures/TimelinePanel.tsx b/src/components/structures/TimelinePanel.tsx index 756a368e33..ff11e0aaa3 100644 --- a/src/components/structures/TimelinePanel.tsx +++ b/src/components/structures/TimelinePanel.tsx @@ -680,6 +680,7 @@ class TimelinePanel extends React.Component { }; private onSync = (clientSyncState: SyncState, prevState: SyncState, data: object): void => { + if (this.unmounted) return; this.setState({ clientSyncState }); }; diff --git a/src/components/structures/UserView.tsx b/src/components/structures/UserView.tsx index 04e97bc057..657b9ab6ff 100644 --- a/src/components/structures/UserView.tsx +++ b/src/components/structures/UserView.tsx @@ -29,6 +29,7 @@ import MainSplit from "./MainSplit"; import RightPanel from "./RightPanel"; import Spinner from "../views/elements/Spinner"; import ResizeNotifier from "../../utils/ResizeNotifier"; +import { RightPanelPhases } from "../../stores/right-panel/RightPanelStorePhases"; interface IProps { userId?: string; @@ -88,7 +89,10 @@ export default class UserView extends React.Component { if (this.state.loading) { return ; } else if (this.state.member) { - const panel = ; + const panel = ; return ( ); diff --git a/src/components/views/context_menus/RoomContextMenu.tsx b/src/components/views/context_menus/RoomContextMenu.tsx index 0fe7726c62..ee7e7e1694 100644 --- a/src/components/views/context_menus/RoomContextMenu.tsx +++ b/src/components/views/context_menus/RoomContextMenu.tsx @@ -38,12 +38,10 @@ import Modal from "../../../Modal"; import ExportDialog from "../dialogs/ExportDialog"; import { onRoomFilesClick, onRoomMembersClick } from "../right_panel/RoomSummaryCard"; import RoomViewStore from "../../../stores/RoomViewStore"; -import defaultDispatcher from "../../../dispatcher/dispatcher"; -import { SetRightPanelPhasePayload } from "../../../dispatcher/payloads/SetRightPanelPhasePayload"; -import { Action } from "../../../dispatcher/actions"; -import { RightPanelPhases } from "../../../stores/RightPanelStorePhases"; +import { RightPanelPhases } from '../../../stores/right-panel/RightPanelStorePhases'; import { ROOM_NOTIFICATIONS_TAB } from "../dialogs/RoomSettingsDialog"; import { useEventEmitterState } from "../../../hooks/useEventEmitter"; +import RightPanelStore from "../../../stores/right-panel/RightPanelStore"; import DMRoomMap from "../../../utils/DMRoomMap"; interface IProps extends IContextMenuProps { @@ -272,11 +270,7 @@ const RoomContextMenu = ({ room, onFinished, ...props }: IProps) => { ev.stopPropagation(); ensureViewingRoom(); - defaultDispatcher.dispatch({ - action: Action.SetRightPanelPhase, - phase: RightPanelPhases.RoomSummary, - allowClose: false, - }); + RightPanelStore.instance.setCard({ phase: RightPanelPhases.RoomSummary }, false); onFinished(); }} label={_t("Widgets")} diff --git a/src/components/views/elements/MemberEventListSummary.tsx b/src/components/views/elements/MemberEventListSummary.tsx index 69ab4a60f0..53a604fe9d 100644 --- a/src/components/views/elements/MemberEventListSummary.tsx +++ b/src/components/views/elements/MemberEventListSummary.tsx @@ -26,19 +26,13 @@ import { formatCommaSeparatedList } from '../../../utils/FormattingUtils'; import { isValid3pidInvite } from "../../../RoomInvite"; import EventListSummary from "./EventListSummary"; import { replaceableComponent } from "../../../utils/replaceableComponent"; -import defaultDispatcher from '../../../dispatcher/dispatcher'; -import { RightPanelPhases } from '../../../stores/RightPanelStorePhases'; -import { Action } from '../../../dispatcher/actions'; -import { SetRightPanelPhasePayload } from '../../../dispatcher/payloads/SetRightPanelPhasePayload'; +import { RightPanelPhases } from '../../../stores/right-panel/RightPanelStorePhases'; import { jsxJoin } from '../../../utils/ReactUtils'; import { Layout } from '../../../settings/enums/Layout'; +import RightPanelStore from '../../../stores/right-panel/RightPanelStore'; const onPinnedMessagesClick = (): void => { - defaultDispatcher.dispatch({ - action: Action.SetRightPanelPhase, - phase: RightPanelPhases.PinnedMessages, - allowClose: false, - }); + RightPanelStore.instance.setCard({ phase: RightPanelPhases.PinnedMessages }, false); }; const SENDER_AS_DISPLAY_NAME_EVENTS = [EventType.RoomServerAcl, EventType.RoomPinnedEvents]; diff --git a/src/components/views/groups/GroupMemberList.js b/src/components/views/groups/GroupMemberList.js index 404145a241..43651e20e6 100644 --- a/src/components/views/groups/GroupMemberList.js +++ b/src/components/views/groups/GroupMemberList.js @@ -20,14 +20,13 @@ import PropTypes from 'prop-types'; import { _t } from '../../../languageHandler'; import * as sdk from '../../../index'; -import dis from '../../../dispatcher/dispatcher'; import GroupStore from '../../../stores/GroupStore'; import { showGroupInviteDialog } from '../../../GroupAddressPicker'; import AccessibleButton from '../elements/AccessibleButton'; -import { RightPanelPhases } from "../../../stores/RightPanelStorePhases"; +import { RightPanelPhases } from '../../../stores/right-panel/RightPanelStorePhases'; import AutoHideScrollbar from "../../structures/AutoHideScrollbar"; -import { Action } from "../../../dispatcher/actions"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import RightPanelStore from '../../../stores/right-panel/RightPanelStore'; const INITIAL_LOAD_NUM_MEMBERS = 30; @@ -170,10 +169,9 @@ export default class GroupMemberList extends React.Component { onInviteToGroupButtonClick = () => { showGroupInviteDialog(this.props.groupId).then(() => { - dis.dispatch({ - action: Action.SetRightPanelPhase, + RightPanelStore.instance.setCard({ phase: RightPanelPhases.GroupMemberList, - refireParams: { groupId: this.props.groupId }, + state: { groupId: this.props.groupId }, }); }); }; diff --git a/src/components/views/messages/MKeyVerificationRequest.tsx b/src/components/views/messages/MKeyVerificationRequest.tsx index 4737bb2dec..0361258833 100644 --- a/src/components/views/messages/MKeyVerificationRequest.tsx +++ b/src/components/views/messages/MKeyVerificationRequest.tsx @@ -22,12 +22,11 @@ import { MatrixClientPeg } from '../../../MatrixClientPeg'; import { _t } from '../../../languageHandler'; import { getNameForEventRoom, userLabelForEventRoom } from '../../../utils/KeyVerificationStateObserver'; -import dis from "../../../dispatcher/dispatcher"; -import { RightPanelPhases } from "../../../stores/RightPanelStorePhases"; -import { Action } from "../../../dispatcher/actions"; +import { RightPanelPhases } from '../../../stores/right-panel/RightPanelStorePhases'; import EventTileBubble from "./EventTileBubble"; import { replaceableComponent } from "../../../utils/replaceableComponent"; import AccessibleButton from '../elements/AccessibleButton'; +import RightPanelStore from '../../../stores/right-panel/RightPanelStore'; interface IProps { mxEvent: MatrixEvent; @@ -52,10 +51,9 @@ export default class MKeyVerificationRequest extends React.Component { private openRequest = () => { const { verificationRequest } = this.props.mxEvent; const member = MatrixClientPeg.get().getUser(verificationRequest.otherUserId); - dis.dispatch({ - action: Action.SetRightPanelPhase, + RightPanelStore.instance.setCard({ phase: RightPanelPhases.EncryptionPanel, - refireParams: { verificationRequest, member }, + state: { verificationRequest, member }, }); }; diff --git a/src/components/views/right_panel/BaseCard.tsx b/src/components/views/right_panel/BaseCard.tsx index 4b18c63e12..94cfed71ed 100644 --- a/src/components/views/right_panel/BaseCard.tsx +++ b/src/components/views/right_panel/BaseCard.tsx @@ -20,10 +20,8 @@ import classNames from 'classnames'; import AutoHideScrollbar from "../../structures/AutoHideScrollbar"; import { _t } from "../../../languageHandler"; import AccessibleButton from "../elements/AccessibleButton"; -import defaultDispatcher from "../../../dispatcher/dispatcher"; -import { SetRightPanelPhasePayload } from "../../../dispatcher/payloads/SetRightPanelPhasePayload"; -import { Action } from "../../../dispatcher/actions"; -import { RightPanelPhases } from "../../../stores/RightPanelStorePhases"; +import { RightPanelPhases } from '../../../stores/right-panel/RightPanelStorePhases'; +import RightPanelStore from '../../../stores/right-panel/RightPanelStore'; interface IProps { header?: ReactNode; @@ -34,7 +32,7 @@ interface IProps { previousPhaseLabel?: string; closeLabel?: string; onClose?(): void; - refireParams?; + cardState?; } interface IGroupProps { @@ -59,16 +57,15 @@ const BaseCard: React.FC = ({ previousPhase, previousPhaseLabel, children, - refireParams, + cardState, }) => { let backButton; if (previousPhase) { const onBackClick = () => { - defaultDispatcher.dispatch({ - action: Action.SetRightPanelPhase, - phase: previousPhase, - refireParams: refireParams, - }); + // TODO RightPanelStore (will be addressed in a follow up PR): this should ideally be: + // RightPanelStore.instance.popRightPanel(); + + RightPanelStore.instance.setCard({ phase: previousPhase, state: cardState }); }; const label = previousPhaseLabel ?? _t("Back"); backButton = ; diff --git a/src/components/views/right_panel/EncryptionPanel.tsx b/src/components/views/right_panel/EncryptionPanel.tsx index c0bc34157b..a87c3c925d 100644 --- a/src/components/views/right_panel/EncryptionPanel.tsx +++ b/src/components/views/right_panel/EncryptionPanel.tsx @@ -31,9 +31,8 @@ import { useEventEmitter } from "../../../hooks/useEventEmitter"; import Modal from "../../../Modal"; import * as sdk from "../../../index"; import { _t } from "../../../languageHandler"; -import dis from "../../../dispatcher/dispatcher"; -import { Action } from "../../../dispatcher/actions"; -import { RightPanelPhases } from "../../../stores/RightPanelStorePhases"; +import { RightPanelPhases } from '../../../stores/right-panel/RightPanelStorePhases'; +import RightPanelStore from "../../../stores/right-panel/RightPanelStore"; // cancellation codes which constitute a key mismatch const MISMATCHES = ["m.key_mismatch", "m.user_error", "m.mismatched_sas"]; @@ -117,10 +116,9 @@ const EncryptionPanel: React.FC = (props: IProps) => { setRequest(verificationRequest_); setPhase(verificationRequest_.phase); // Notify the RightPanelStore about this - dis.dispatch({ - action: Action.SetRightPanelPhase, + RightPanelStore.instance.setCard({ phase: RightPanelPhases.EncryptionPanel, - refireParams: { member, verificationRequest: verificationRequest_ }, + state: { member, verificationRequest: verificationRequest_ }, }); }, [member]); diff --git a/src/components/views/right_panel/GroupHeaderButtons.tsx b/src/components/views/right_panel/GroupHeaderButtons.tsx index 6ecbcb9769..723e5440b0 100644 --- a/src/components/views/right_panel/GroupHeaderButtons.tsx +++ b/src/components/views/right_panel/GroupHeaderButtons.tsx @@ -23,7 +23,7 @@ import React from 'react'; import { _t } from '../../../languageHandler'; import HeaderButton from './HeaderButton'; import HeaderButtons, { HeaderKind } from './HeaderButtons'; -import { RightPanelPhases } from "../../../stores/RightPanelStorePhases"; +import { RightPanelPhases } from '../../../stores/right-panel/RightPanelStorePhases'; import { Action } from "../../../dispatcher/actions"; import { ActionPayload } from "../../../dispatcher/payloads"; import { ViewUserPayload } from "../../../dispatcher/payloads/ViewUserPayload"; diff --git a/src/components/views/right_panel/HeaderButtons.tsx b/src/components/views/right_panel/HeaderButtons.tsx index 20ab61cfee..ef2b03af39 100644 --- a/src/components/views/right_panel/HeaderButtons.tsx +++ b/src/components/views/right_panel/HeaderButtons.tsx @@ -21,15 +21,11 @@ limitations under the License. import React from 'react'; import dis from '../../../dispatcher/dispatcher'; -import RightPanelStore from "../../../stores/RightPanelStore"; -import { RightPanelPhases } from "../../../stores/RightPanelStorePhases"; -import { Action } from '../../../dispatcher/actions'; -import { - SetRightPanelPhasePayload, - SetRightPanelPhaseRefireParams, -} from '../../../dispatcher/payloads/SetRightPanelPhasePayload'; -import type { EventSubscription } from "fbemitter"; +import RightPanelStore from "../../../stores/right-panel/RightPanelStore"; +import { RightPanelPhases } from '../../../stores/right-panel/RightPanelStorePhases'; +import { IRightPanelCardState } from '../../../stores/right-panel/RightPanelStoreIPanelState'; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { UPDATE_EVENT } from '../../../stores/AsyncStore'; import { NotificationColor } from '../../../stores/notifications/NotificationColor'; export enum HeaderKind { @@ -47,38 +43,35 @@ interface IProps {} @replaceableComponent("views.right_panel.HeaderButtons") export default abstract class HeaderButtons

extends React.Component { - private storeToken: EventSubscription; + private unmounted = false; private dispatcherRef: string; constructor(props: IProps & P, kind: HeaderKind) { super(props); - const rps = RightPanelStore.getSharedInstance(); + const rps = RightPanelStore.instance; this.state = { headerKind: kind, + phase: rps.currentCard.phase, threadNotificationColor: NotificationColor.None, - phase: kind === HeaderKind.Room ? rps.visibleRoomPanelPhase : rps.visibleGroupPanelPhase, }; } public componentDidMount() { - this.storeToken = RightPanelStore.getSharedInstance().addListener(this.onRightPanelUpdate.bind(this)); + RightPanelStore.instance.on(UPDATE_EVENT, this.onRightPanelStoreUpdate); this.dispatcherRef = dis.register(this.onAction.bind(this)); // used by subclasses } public componentWillUnmount() { - if (this.storeToken) this.storeToken.remove(); + this.unmounted = true; + RightPanelStore.instance.off(UPDATE_EVENT, this.onRightPanelStoreUpdate); if (this.dispatcherRef) dis.unregister(this.dispatcherRef); } protected abstract onAction(payload); - public setPhase(phase: RightPanelPhases, extras?: Partial) { - dis.dispatch({ - action: Action.SetRightPanelPhase, - phase: phase, - refireParams: extras, - }); + public setPhase(phase: RightPanelPhases, cardState?: Partial) { + RightPanelStore.instance.setCard({ phase, state: cardState }); } public isPhase(phases: string | string[]) { @@ -89,14 +82,12 @@ export default abstract class HeaderButtons

extends React.Component { + if (this.unmounted) return; + let phase = RightPanelStore.instance.currentCard.phase; + if (!RightPanelStore.instance.isOpenForGroup) {phase = null;} + this.setState({ phase }); + }; // XXX: Make renderButtons a prop public abstract renderButtons(): JSX.Element; diff --git a/src/components/views/right_panel/RoomHeaderButtons.tsx b/src/components/views/right_panel/RoomHeaderButtons.tsx index 7062a6b111..0cf4c9a5ee 100644 --- a/src/components/views/right_panel/RoomHeaderButtons.tsx +++ b/src/components/views/right_panel/RoomHeaderButtons.tsx @@ -25,16 +25,15 @@ import { Room } from "matrix-js-sdk/src/models/room"; import { _t } from '../../../languageHandler'; import HeaderButton from './HeaderButton'; import HeaderButtons, { HeaderKind } from './HeaderButtons'; -import { RightPanelPhases } from "../../../stores/RightPanelStorePhases"; +import { RightPanelPhases } from '../../../stores/right-panel/RightPanelStorePhases'; import { Action } from "../../../dispatcher/actions"; import { ActionPayload } from "../../../dispatcher/payloads"; -import RightPanelStore from "../../../stores/RightPanelStore"; +import RightPanelStore from "../../../stores/right-panel/RightPanelStore"; import { replaceableComponent } from "../../../utils/replaceableComponent"; import { useSettingValue } from "../../../hooks/useSettings"; import { useReadPinnedEvents, usePinnedEvents } from './PinnedMessagesCard'; import { dispatchShowThreadsPanelEvent } from "../../../dispatcher/dispatch-actions/threads"; import SettingsStore from "../../../settings/SettingsStore"; -import dis from "../../../dispatcher/dispatcher"; import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore"; import { NotificationColor } from "../../../stores/notifications/NotificationColor"; import { ThreadsRoomNotificationState } from "../../../stores/notifications/ThreadsRoomNotificationState"; @@ -161,7 +160,7 @@ export default class RoomHeaderButtons extends HeaderButtons { } } else if (payload.action === "view_3pid_invite") { if (payload.event) { - this.setPhase(RightPanelPhases.Room3pidMemberInfo, { event: payload.event }); + this.setPhase(RightPanelPhases.Room3pidMemberInfo, { memberInfoEvent: payload.event }); } else { this.setPhase(RightPanelPhases.RoomMemberList); } @@ -170,12 +169,12 @@ export default class RoomHeaderButtons extends HeaderButtons { private onRoomSummaryClicked = () => { // use roomPanelPhase rather than this.state.phase as it remembers the latest one if we close - const lastPhase = RightPanelStore.getSharedInstance().roomPanelPhase; - if (ROOM_INFO_PHASES.includes(lastPhase)) { - if (this.state.phase === lastPhase) { - this.setPhase(lastPhase); + const currentPhase = RightPanelStore.instance.currentCard.phase; + if (ROOM_INFO_PHASES.includes(currentPhase)) { + if (this.state.phase === currentPhase) { + this.setPhase(currentPhase); } else { - this.setPhase(lastPhase, RightPanelStore.getSharedInstance().roomPanelPhaseParams); + this.setPhase(currentPhase, RightPanelStore.instance.currentCard.state); } } else { // This toggles for us, if needed @@ -198,10 +197,7 @@ export default class RoomHeaderButtons extends HeaderButtons { private onThreadsPanelClicked = () => { if (RoomHeaderButtons.THREAD_PHASES.includes(this.state.phase)) { - dis.dispatch({ - action: Action.ToggleRightPanel, - type: "room", - }); + RightPanelStore.instance.togglePanel(); } else { dispatchShowThreadsPanelEvent(); } @@ -227,6 +223,7 @@ export default class RoomHeaderButtons extends HeaderButtons { rightPanelPhaseButtons.set(RightPanelPhases.ThreadPanel, SettingsStore.getValue("feature_thread") ? = ({ app, room }) => { }, [room.roomId]); const onOpenWidgetClick = () => { - defaultDispatcher.dispatch({ - action: Action.SetRightPanelPhase, + // TODO RightPanelStore (will be addressed in a follow up PR): should push the widget + RightPanelStore.instance.setCard({ phase: RightPanelPhases.Widget, - refireParams: { - widgetId: app.id, - }, + state: { widgetId: app.id }, }); }; @@ -237,19 +234,13 @@ const AppsSection: React.FC = ({ room }) => { }; export const onRoomMembersClick = (allowClose = true) => { - defaultDispatcher.dispatch({ - action: Action.SetRightPanelPhase, - phase: RightPanelPhases.RoomMemberList, - allowClose, - }); + // TODO RightPanelStore (will be addressed in a follow up PR): should push the phase + RightPanelStore.instance.setCard({ phase: RightPanelPhases.RoomMemberList }, allowClose); }; export const onRoomFilesClick = (allowClose = true) => { - defaultDispatcher.dispatch({ - action: Action.SetRightPanelPhase, - phase: RightPanelPhases.FilePanel, - allowClose, - }); + // TODO RightPanelStore (will be addressed in a follow up PR): should push the phase + RightPanelStore.instance.setCard({ phase: RightPanelPhases.FilePanel }, allowClose); }; const onRoomSettingsClick = () => { diff --git a/src/components/views/right_panel/TimelineCard.tsx b/src/components/views/right_panel/TimelineCard.tsx index fc0aa4c260..3ce4b82ce3 100644 --- a/src/components/views/right_panel/TimelineCard.tsx +++ b/src/components/views/right_panel/TimelineCard.tsx @@ -55,7 +55,7 @@ interface IState { editState?: EditorStateTransfer; replyToEvent?: MatrixEvent; initialEventId?: string; - initialEventHighlighted?: boolean; + isInitialEventHighlighted?: boolean; // settings: showReadReceipts?: boolean; @@ -103,7 +103,7 @@ export default class TimelineCard extends React.Component { // roomLoadError: RoomViewStore.getRoomLoadError(), initialEventId: RoomViewStore.getInitialEventId(), - initialEventHighlighted: RoomViewStore.isInitialEventHighlighted(), + isInitialEventHighlighted: RoomViewStore.isInitialEventHighlighted(), replyToEvent: RoomViewStore.getQuotingEvent(), }; @@ -127,7 +127,7 @@ export default class TimelineCard extends React.Component { }; private onScroll = (): void => { - if (this.state.initialEventId && this.state.initialEventHighlighted) { + if (this.state.initialEventId && this.state.isInitialEventHighlighted) { dis.dispatch({ action: Action.ViewRoom, room_id: this.props.room.roomId, @@ -145,7 +145,7 @@ export default class TimelineCard extends React.Component { }; public render(): JSX.Element { - const highlightedEventId = this.state.initialEventHighlighted + const highlightedEventId = this.state.isInitialEventHighlighted ? this.state.initialEventId : null; diff --git a/src/components/views/right_panel/UserInfo.tsx b/src/components/views/right_panel/UserInfo.tsx index 74f0bccb5d..09b015e97a 100644 --- a/src/components/views/right_panel/UserInfo.tsx +++ b/src/components/views/right_panel/UserInfo.tsx @@ -44,7 +44,7 @@ import E2EIcon from "../rooms/E2EIcon"; import { useEventEmitter } from "../../../hooks/useEventEmitter"; import { textualPowerLevel } from '../../../Roles'; import MatrixClientContext from "../../../contexts/MatrixClientContext"; -import { RightPanelPhases } from "../../../stores/RightPanelStorePhases"; +import { RightPanelPhases } from '../../../stores/right-panel/RightPanelStorePhases'; import EncryptionPanel from "./EncryptionPanel"; import { useAsyncMemo } from '../../../hooks/useAsyncMemo'; import { legacyVerifyUser, verifyDevice, verifyUser } from '../../../verification'; @@ -63,7 +63,6 @@ import ErrorDialog from "../dialogs/ErrorDialog"; import QuestionDialog from "../dialogs/QuestionDialog"; import ConfirmUserActionDialog from "../dialogs/ConfirmUserActionDialog"; import InfoDialog from "../dialogs/InfoDialog"; -import { SetRightPanelPhasePayload } from "../../../dispatcher/payloads/SetRightPanelPhasePayload"; import RoomAvatar from "../avatars/RoomAvatar"; import RoomName from "../elements/RoomName"; import { mediaFromMxc } from "../../../customisations/Media"; @@ -75,6 +74,8 @@ import { bulkSpaceBehaviour } from "../../../utils/space"; import { shouldShowComponent } from "../../../customisations/helpers/UIComponents"; import { UIComponent } from "../../../settings/UIFeature"; import { TimelineRenderingType } from "../../../contexts/RoomContext"; +import RightPanelStore from '../../../stores/right-panel/RightPanelStore'; +import { IRightPanelCardState } from '../../../stores/right-panel/RightPanelStoreIPanelState'; import { useUserStatusMessage } from "../../../hooks/useUserStatusMessage"; export interface IDevice { @@ -1649,25 +1650,22 @@ const UserInfo: React.FC = ({ const classes = ["mx_UserInfo"]; - let refireParams; + let cardState: IRightPanelCardState; let previousPhase: RightPanelPhases; // We have no previousPhase for when viewing a UserInfo from a Group or without a Room at this time if (room && phase === RightPanelPhases.EncryptionPanel) { previousPhase = RightPanelPhases.RoomMemberInfo; - refireParams = { member }; + cardState = { member }; } else if (room?.isSpaceRoom() && SpaceStore.spacesEnabled) { - previousPhase = previousPhase = RightPanelPhases.SpaceMemberList; - refireParams = { space: room }; + previousPhase = RightPanelPhases.SpaceMemberList; + cardState = { spaceId: room.roomId }; } else if (room) { previousPhase = RightPanelPhases.RoomMemberList; } const onEncryptionPanelClose = () => { - dis.dispatch({ - action: Action.SetRightPanelPhase, - phase: previousPhase, - refireParams: refireParams, - }); + // TODO RightPanelStore (will be addressed in a follow up PR): here we want to pop the panel + RightPanelStore.instance.setCard({ phase: previousPhase, state: cardState }); }; let content; @@ -1723,7 +1721,7 @@ const UserInfo: React.FC = ({ onClose={onClose} closeLabel={closeLabel} previousPhase={previousPhase} - refireParams={refireParams} + cardState={cardState} > { content } ; diff --git a/src/components/views/right_panel/WidgetCard.tsx b/src/components/views/right_panel/WidgetCard.tsx index 8ab73483df..4f86494545 100644 --- a/src/components/views/right_panel/WidgetCard.tsx +++ b/src/components/views/right_panel/WidgetCard.tsx @@ -23,14 +23,12 @@ import WidgetUtils from "../../../utils/WidgetUtils"; import AppTile from "../elements/AppTile"; import { _t } from "../../../languageHandler"; import { useWidgets } from "./RoomSummaryCard"; -import { RightPanelPhases } from "../../../stores/RightPanelStorePhases"; -import defaultDispatcher from "../../../dispatcher/dispatcher"; -import { SetRightPanelPhasePayload } from "../../../dispatcher/payloads/SetRightPanelPhasePayload"; -import { Action } from "../../../dispatcher/actions"; +import { RightPanelPhases } from '../../../stores/right-panel/RightPanelStorePhases'; import { ChevronFace, ContextMenuButton, useContextMenu } from "../../structures/ContextMenu"; import WidgetContextMenu from "../context_menus/WidgetContextMenu"; import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore"; import UIStore from "../../../stores/UIStore"; +import RightPanelStore from "../../../stores/right-panel/RightPanelStore"; interface IProps { room: Room; @@ -50,10 +48,9 @@ const WidgetCard: React.FC = ({ room, widgetId, onClose }) => { useEffect(() => { if (!app || isPinned) { // stop showing this card - defaultDispatcher.dispatch({ - action: Action.SetRightPanelPhase, - phase: RightPanelPhases.RoomSummary, - }); + + //TODO RightPanelStore (will be addressed in a follow up PR): here we want to just pop the widget card. + RightPanelStore.instance.setCard({ phase: RightPanelPhases.RoomSummary }); } }, [app, isPinned]); diff --git a/src/components/views/rooms/AppsDrawer.tsx b/src/components/views/rooms/AppsDrawer.tsx index 01696d22d3..e6d2157f97 100644 --- a/src/components/views/rooms/AppsDrawer.tsx +++ b/src/components/views/rooms/AppsDrawer.tsx @@ -56,6 +56,7 @@ interface IState { @replaceableComponent("views.rooms.AppsDrawer") export default class AppsDrawer extends React.Component { + private unmounted = false; private resizeContainer: HTMLDivElement; private resizer: Resizer; private dispatcherRef: string; @@ -85,6 +86,7 @@ export default class AppsDrawer extends React.Component { } public componentWillUnmount(): void { + this.unmounted = true; ScalarMessaging.stopListening(); WidgetLayoutStore.instance.off(WidgetLayoutStore.emissionForRoom(this.props.room), this.updateApps); if (this.dispatcherRef) dis.unregister(this.dispatcherRef); @@ -213,6 +215,7 @@ export default class AppsDrawer extends React.Component { private centerApps = (): IApp[] => this.state.apps[Container.Center]; private updateApps = (): void => { + if (this.unmounted) return; this.setState({ apps: this.getApps(), }); diff --git a/src/components/views/rooms/MemberList.tsx b/src/components/views/rooms/MemberList.tsx index 21ce299e13..4a9d8b9d10 100644 --- a/src/components/views/rooms/MemberList.tsx +++ b/src/components/views/rooms/MemberList.tsx @@ -33,7 +33,7 @@ import { isValid3pidInvite } from "../../../RoomInvite"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore"; import BaseCard from "../right_panel/BaseCard"; -import { RightPanelPhases } from "../../../stores/RightPanelStorePhases"; +import { RightPanelPhases } from '../../../stores/right-panel/RightPanelStorePhases'; import RoomAvatar from "../avatars/RoomAvatar"; import RoomName from "../elements/RoomName"; import { replaceableComponent } from "../../../utils/replaceableComponent"; diff --git a/src/components/views/rooms/RoomHeader.tsx b/src/components/views/rooms/RoomHeader.tsx index 09ab7767d7..92a8321336 100644 --- a/src/components/views/rooms/RoomHeader.tsx +++ b/src/components/views/rooms/RoomHeader.tsx @@ -38,7 +38,7 @@ import { ContextMenuTooltipButton } from '../../structures/ContextMenu'; import RoomContextMenu from "../context_menus/RoomContextMenu"; import { contextMenuBelow } from './RoomTile'; import { RoomNotificationStateStore } from '../../../stores/notifications/RoomNotificationStateStore'; -import { RightPanelPhases } from '../../../stores/RightPanelStorePhases'; +import { RightPanelPhases } from '../../../stores/right-panel/RightPanelStorePhases'; import { NotificationStateEvents } from '../../../stores/notifications/NotificationState'; export interface ISearchInfo { diff --git a/src/components/views/rooms/RoomListHeader.tsx b/src/components/views/rooms/RoomListHeader.tsx index 56d81e0e8d..a744236dc1 100644 --- a/src/components/views/rooms/RoomListHeader.tsx +++ b/src/components/views/rooms/RoomListHeader.tsx @@ -36,7 +36,7 @@ import { ButtonEvent } from "../elements/AccessibleButton"; import Modal from "../../../Modal"; import EditCommunityPrototypeDialog from "../dialogs/EditCommunityPrototypeDialog"; import { Action } from "../../../dispatcher/actions"; -import { RightPanelPhases } from "../../../stores/RightPanelStorePhases"; +import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases"; import ErrorDialog from "../dialogs/ErrorDialog"; import { showCommunityInviteDialog } from "../../../RoomInvite"; import { useDispatcher } from "../../../hooks/useDispatcher"; @@ -50,6 +50,7 @@ import { UPDATE_HOME_BEHAVIOUR, UPDATE_SELECTED_SPACE, } from "../../../stores/spaces"; +import RightPanelStore from "../../../stores/right-panel/RightPanelStore"; import TooltipTarget from "../elements/TooltipTarget"; const contextMenuBelow = (elementRect: DOMRect) => { @@ -99,7 +100,7 @@ const PrototypeCommunityContextMenu = (props: ComponentProps({ - action: Action.SetRightPanelPhase, - phase: RightPanelPhases.EncryptionPanel, - refireParams: { - verificationRequest: request, - member: cli.getUser(request.otherUserId), + RightPanelStore.instance.setCard( + { + phase: RightPanelPhases.EncryptionPanel, + state: { verificationRequest: request, member: cli.getUser(request.otherUserId) }, }, - }); + undefined, + request.channel.roomId, + ); } else { Modal.createTrackedDialog('Incoming Verification', '', VerificationRequestDialog, { verificationRequest: request, diff --git a/src/dispatcher/actions.ts b/src/dispatcher/actions.ts index 6d6894fc3b..b0bb02d79e 100644 --- a/src/dispatcher/actions.ts +++ b/src/dispatcher/actions.ts @@ -102,16 +102,6 @@ export enum Action { */ ViewRoomDelta = "view_room_delta", - /** - * Sets the phase for the right panel. Should be used with SetRightPanelPhasePayload. - */ - SetRightPanelPhase = "set_right_panel_phase", - - /** - * Toggles the right panel. Should be used with ToggleRightPanelPayload. - */ - ToggleRightPanel = "toggle_right_panel", - /** * Trigged after the phase of the right panel is set. Should be used with AfterRightPanelPhaseChangePayload. */ diff --git a/src/dispatcher/dispatch-actions/threads.ts b/src/dispatcher/dispatch-actions/threads.ts index 288916eb42..9c3e2c1bff 100644 --- a/src/dispatcher/dispatch-actions/threads.ts +++ b/src/dispatcher/dispatch-actions/threads.ts @@ -15,31 +15,26 @@ limitations under the License. */ import { MatrixEvent } from "matrix-js-sdk/src/models/event"; -import { RightPanelPhases } from "../../stores/RightPanelStorePhases"; -import { Action } from "../actions"; -import dis from '../dispatcher'; -import { SetRightPanelPhasePayload } from "../payloads/SetRightPanelPhasePayload"; +import RightPanelStore from "../../stores/right-panel/RightPanelStore"; +import { RightPanelPhases } from "../../stores/right-panel/RightPanelStorePhases"; export const dispatchShowThreadEvent = ( rootEvent: MatrixEvent, initialEvent?: MatrixEvent, highlighted?: boolean, ) => { - dis.dispatch({ - action: Action.SetRightPanelPhase, + // TODO RightPanelStore (will be addressed in a follow up PR): this should really be a push! + RightPanelStore.instance.setCard({ phase: RightPanelPhases.ThreadView, - refireParams: { - event: rootEvent, + state: { + threadHeadEvent: rootEvent, initialEvent, - highlighted, + isInitialEventHighlighted: highlighted, }, }); }; export const dispatchShowThreadsPanelEvent = () => { - dis.dispatch({ - action: Action.SetRightPanelPhase, - phase: RightPanelPhases.ThreadPanel, - }); + RightPanelStore.instance.setCard({ phase: RightPanelPhases.ThreadPanel }); }; diff --git a/src/dispatcher/payloads/AfterRightPanelPhaseChangePayload.ts b/src/dispatcher/payloads/AfterRightPanelPhaseChangePayload.ts deleted file mode 100644 index b45b8bdebe..0000000000 --- a/src/dispatcher/payloads/AfterRightPanelPhaseChangePayload.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* -Copyright 2020 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; - -import { RightPanelPhases } from "../../stores/RightPanelStorePhases"; -import { SetRightPanelPhaseRefireParams } from "./SetRightPanelPhasePayload"; -import { ActionPayload } from "../payloads"; -import { Action } from "../actions"; - -interface AfterRightPanelPhaseChangeAction extends ActionPayload { - action: Action.AfterRightPanelPhaseChange; - phase: RightPanelPhases; - verificationRequestPromise?: Promise; -} - -export type AfterRightPanelPhaseChangePayload - = AfterRightPanelPhaseChangeAction & SetRightPanelPhaseRefireParams; diff --git a/src/dispatcher/payloads/SetRightPanelPhasePayload.ts b/src/dispatcher/payloads/SetRightPanelPhasePayload.ts deleted file mode 100644 index 54a00e0279..0000000000 --- a/src/dispatcher/payloads/SetRightPanelPhasePayload.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* -Copyright 2020 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; -import { Room } from "matrix-js-sdk/src/models/room"; -import { RoomMember } from "matrix-js-sdk/src/models/room-member"; -import { User } from "matrix-js-sdk/src/models/user"; - -import { RightPanelPhases } from "../../stores/RightPanelStorePhases"; -import { ActionPayload } from "../payloads"; -import { Action } from "../actions"; - -export interface SetRightPanelPhasePayload extends ActionPayload { - action: Action.SetRightPanelPhase; - - phase: RightPanelPhases; - refireParams?: SetRightPanelPhaseRefireParams; - - /** - * By default SetRightPanelPhase can close the panel, this allows overriding that behaviour - */ - allowClose?: boolean; -} - -export interface SetRightPanelPhaseRefireParams { - member?: RoomMember | User; - verificationRequest?: VerificationRequest; - groupId?: string; - groupRoomId?: string; - // XXX: The type for event should 'view_3pid_invite' action's payload - event?: any; - widgetId?: string; - space?: Room; -} diff --git a/src/dispatcher/payloads/ToggleRightPanelPayload.ts b/src/dispatcher/payloads/ToggleRightPanelPayload.ts deleted file mode 100644 index 0635194890..0000000000 --- a/src/dispatcher/payloads/ToggleRightPanelPayload.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* -Copyright 2020 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import { ActionPayload } from "../payloads"; -import { Action } from "../actions"; - -export interface ToggleRightPanelPayload extends ActionPayload { - action: Action.ToggleRightPanel; - - /** - * The type of room that the panel is toggled in. - */ - type: "group" | "room"; -} diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index a96ad66e04..f09eca8349 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -32,7 +32,6 @@ import SystemFontController from './controllers/SystemFontController'; import UseSystemFontController from './controllers/UseSystemFontController'; import { SettingLevel } from "./SettingLevel"; import SettingController from "./controllers/SettingController"; -import { RightPanelPhases } from "../stores/RightPanelStorePhases"; import { isMac } from '../Keyboard'; import UIFeatureController from "./controllers/UIFeatureController"; import { UIFeature } from "./UIFeature"; @@ -771,21 +770,13 @@ export const SETTINGS: {[setting: string]: ISetting} = { displayName: _td("Show previews/thumbnails for images"), default: true, }, - "showRightPanelInRoom": { - supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, - default: false, + "RightPanel.phasesGlobal": { + supportedLevels: [SettingLevel.DEVICE], + default: null, }, - "showRightPanelInGroup": { - supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, - default: false, - }, - "lastRightPanelPhaseForRoom": { - supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, - default: RightPanelPhases.RoomSummary, - }, - "lastRightPanelPhaseForGroup": { - supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, - default: RightPanelPhases.GroupMemberList, + "RightPanel.phases": { + supportedLevels: [SettingLevel.ROOM_DEVICE], + default: null, }, "enableEventIndexing": { supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, diff --git a/src/settings/handlers/DeviceSettingsHandler.ts b/src/settings/handlers/DeviceSettingsHandler.ts index 03da2755b1..429019a981 100644 --- a/src/settings/handlers/DeviceSettingsHandler.ts +++ b/src/settings/handlers/DeviceSettingsHandler.ts @@ -57,17 +57,6 @@ export default class DeviceSettingsHandler extends SettingsHandler { return null; // wrong type or otherwise not set } - // Special case the right panel - see `setValue` for rationale. - if ([ - "showRightPanelInRoom", - "showRightPanelInGroup", - "lastRightPanelPhaseForRoom", - "lastRightPanelPhaseForGroup", - ].includes(settingName)) { - const val = JSON.parse(localStorage.getItem(`mx_${settingName}`) || "{}"); - return val['value']; - } - // Special case for old useIRCLayout setting if (settingName === "layout") { const settings = this.getSettings() || {}; @@ -106,20 +95,6 @@ export default class DeviceSettingsHandler extends SettingsHandler { return Promise.resolve(); } - // Special case the right panel because we want to be able to update these all - // concurrently without stomping on one another. We could use async/await, though - // that introduces just enough latency to be annoying. - if ([ - "showRightPanelInRoom", - "showRightPanelInGroup", - "lastRightPanelPhaseForRoom", - "lastRightPanelPhaseForGroup", - ].includes(settingName)) { - localStorage.setItem(`mx_${settingName}`, JSON.stringify({ value: newValue })); - this.watchers.notifyUpdate(settingName, null, SettingLevel.DEVICE, newValue); - return Promise.resolve(); - } - // Special case for old useIRCLayout setting if (settingName === "layout") { const settings = this.getSettings() || {}; diff --git a/src/stores/RightPanelStore.ts b/src/stores/RightPanelStore.ts deleted file mode 100644 index b494586d9a..0000000000 --- a/src/stores/RightPanelStore.ts +++ /dev/null @@ -1,245 +0,0 @@ -/* -Copyright 2019 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import { Store } from 'flux/utils'; -import { logger } from "matrix-js-sdk/src/logger"; - -import dis from '../dispatcher/dispatcher'; -import { pendingVerificationRequestForUser } from '../verification'; -import SettingsStore from "../settings/SettingsStore"; -import { RightPanelPhases, RIGHT_PANEL_PHASES_NO_ARGS } from "./RightPanelStorePhases"; -import { ActionPayload } from "../dispatcher/payloads"; -import { Action } from '../dispatcher/actions'; -import { SettingLevel } from "../settings/SettingLevel"; - -interface RightPanelStoreState { - // Whether or not to show the right panel at all. We split out rooms and groups - // because they're different flows for the user to follow. - showRoomPanel: boolean; - showGroupPanel: boolean; - - // The last phase (screen) the right panel was showing - lastRoomPhase: RightPanelPhases; - lastGroupPhase: RightPanelPhases; - - previousPhase?: RightPanelPhases; - - // Extra information about the last phase - lastRoomPhaseParams: {[key: string]: any}; -} - -const INITIAL_STATE: RightPanelStoreState = { - showRoomPanel: SettingsStore.getValue("showRightPanelInRoom"), - showGroupPanel: SettingsStore.getValue("showRightPanelInGroup"), - lastRoomPhase: SettingsStore.getValue("lastRightPanelPhaseForRoom"), - lastGroupPhase: SettingsStore.getValue("lastRightPanelPhaseForGroup"), - lastRoomPhaseParams: {}, -}; - -const GROUP_PHASES = [ - RightPanelPhases.GroupMemberList, - RightPanelPhases.GroupRoomList, - RightPanelPhases.GroupRoomInfo, - RightPanelPhases.GroupMemberInfo, -]; - -const MEMBER_INFO_PHASES = [ - RightPanelPhases.RoomMemberInfo, - RightPanelPhases.Room3pidMemberInfo, - RightPanelPhases.EncryptionPanel, -]; - -/** - * A class for tracking the state of the right panel between layouts and - * sessions. - */ -export default class RightPanelStore extends Store { - private static instance: RightPanelStore; - private state: RightPanelStoreState; - private lastRoomId: string; - - constructor() { - super(dis); - - // Initialise state - this.state = INITIAL_STATE; - } - - get isOpenForRoom(): boolean { - return this.state.showRoomPanel; - } - - get isOpenForGroup(): boolean { - return this.state.showGroupPanel; - } - - get roomPanelPhase(): RightPanelPhases { - return this.state.lastRoomPhase; - } - - get groupPanelPhase(): RightPanelPhases { - return this.state.lastGroupPhase; - } - - get previousPhase(): RightPanelPhases | null { - return RIGHT_PANEL_PHASES_NO_ARGS.includes(this.state.previousPhase) ? this.state.previousPhase : null; - } - - get visibleRoomPanelPhase(): RightPanelPhases { - return this.isOpenForRoom ? this.roomPanelPhase : null; - } - - get visibleGroupPanelPhase(): RightPanelPhases { - return this.isOpenForGroup ? this.groupPanelPhase : null; - } - - get roomPanelPhaseParams(): any { - return this.state.lastRoomPhaseParams || {}; - } - - private setState(newState: Partial) { - this.state = Object.assign(this.state, newState); - - SettingsStore.setValue( - "showRightPanelInRoom", - null, - SettingLevel.DEVICE, - this.state.showRoomPanel, - ); - SettingsStore.setValue( - "showRightPanelInGroup", - null, - SettingLevel.DEVICE, - this.state.showGroupPanel, - ); - - if (RIGHT_PANEL_PHASES_NO_ARGS.includes(this.state.lastRoomPhase)) { - SettingsStore.setValue( - "lastRightPanelPhaseForRoom", - null, - SettingLevel.DEVICE, - this.state.lastRoomPhase, - ); - } - if (RIGHT_PANEL_PHASES_NO_ARGS.includes(this.state.lastGroupPhase)) { - SettingsStore.setValue( - "lastRightPanelPhaseForGroup", - null, - SettingLevel.DEVICE, - this.state.lastGroupPhase, - ); - } - - this.__emitChange(); - } - - __onDispatch(payload: ActionPayload) { // eslint-disable-line @typescript-eslint/naming-convention - switch (payload.action) { - case Action.ViewRoom: - if (payload.room_id === this.lastRoomId) break; // skip this transition, probably a permalink - // fallthrough - case 'view_group': - this.lastRoomId = payload.room_id; - - // Reset to the member list if we're viewing member info - if (MEMBER_INFO_PHASES.includes(this.state.lastRoomPhase)) { - this.setState({ lastRoomPhase: RightPanelPhases.RoomMemberList, lastRoomPhaseParams: {} }); - } - - // Do the same for groups - if (this.state.lastGroupPhase === RightPanelPhases.GroupMemberInfo) { - this.setState({ lastGroupPhase: RightPanelPhases.GroupMemberList }); - } - break; - - case Action.SetRightPanelPhase: { - let targetPhase = payload.phase; - let refireParams = payload.refireParams; - const allowClose = payload.allowClose ?? true; - // redirect to EncryptionPanel if there is an ongoing verification request - if (targetPhase === RightPanelPhases.RoomMemberInfo && payload.refireParams) { - const { member } = payload.refireParams; - const pendingRequest = pendingVerificationRequestForUser(member); - if (pendingRequest) { - targetPhase = RightPanelPhases.EncryptionPanel; - refireParams = { - verificationRequest: pendingRequest, - member, - }; - } - } - if (!RightPanelPhases[targetPhase]) { - logger.warn(`Tried to switch right panel to unknown phase: ${targetPhase}`); - return; - } - - if (GROUP_PHASES.includes(targetPhase)) { - if (targetPhase === this.state.lastGroupPhase) { - this.setState({ - showGroupPanel: !this.state.showGroupPanel, - previousPhase: null, - }); - } else { - this.setState({ - lastGroupPhase: targetPhase, - showGroupPanel: true, - previousPhase: this.state.lastGroupPhase, - }); - } - } else { - if (targetPhase === this.state.lastRoomPhase && !refireParams && allowClose) { - this.setState({ - showRoomPanel: !this.state.showRoomPanel, - previousPhase: null, - }); - } else { - this.setState({ - lastRoomPhase: targetPhase, - showRoomPanel: true, - lastRoomPhaseParams: refireParams || {}, - previousPhase: this.state.lastRoomPhase, - }); - } - } - - // Let things like the member info panel actually open to the right member. - dis.dispatch({ - action: Action.AfterRightPanelPhaseChange, - phase: targetPhase, - ...(refireParams || {}), - }); - break; - } - - case Action.ToggleRightPanel: - if (payload.type === "room") { - this.setState({ showRoomPanel: !this.state.showRoomPanel }); - } else { // group - this.setState({ showGroupPanel: !this.state.showGroupPanel }); - } - break; - } - } - - static getSharedInstance(): RightPanelStore { - if (!RightPanelStore.instance) { - RightPanelStore.instance = new RightPanelStore(); - } - return RightPanelStore.instance; - } -} - -window.mxRightPanelStore = RightPanelStore.getSharedInstance(); diff --git a/src/stores/right-panel/RightPanelStore.ts b/src/stores/right-panel/RightPanelStore.ts new file mode 100644 index 0000000000..2aaee296d7 --- /dev/null +++ b/src/stores/right-panel/RightPanelStore.ts @@ -0,0 +1,370 @@ +/* +Copyright 2019-2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +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 { logger } from "matrix-js-sdk/src/logger"; +import { EventSubscription } from 'fbemitter'; + +import defaultDispatcher from '../../dispatcher/dispatcher'; +import { pendingVerificationRequestForUser } from '../../verification'; +import SettingsStore from "../../settings/SettingsStore"; +import { RightPanelPhases } from "./RightPanelStorePhases"; +import { ActionPayload } from "../../dispatcher/payloads"; +import { Action } from '../../dispatcher/actions'; +import { SettingLevel } from "../../settings/SettingLevel"; +import { UPDATE_EVENT } from '../AsyncStore'; +import { ReadyWatchingStore } from '../ReadyWatchingStore'; +import { + IRightPanelCard, + convertToStatePanel, + convertToStorePanel, + IRightPanelForRoom, + convertCardToStore, +} from './RightPanelStoreIPanelState'; +import { MatrixClientPeg } from "../../MatrixClientPeg"; +// import RoomViewStore from '../RoomViewStore'; + +const GROUP_PHASES = [ + RightPanelPhases.GroupMemberList, + RightPanelPhases.GroupRoomList, + RightPanelPhases.GroupRoomInfo, + RightPanelPhases.GroupMemberInfo, +]; + +const MEMBER_INFO_PHASES = [ + RightPanelPhases.RoomMemberInfo, + RightPanelPhases.Room3pidMemberInfo, + RightPanelPhases.EncryptionPanel, +]; + +/** + * A class for tracking the state of the right panel between layouts and + * sessions. This state includes a history for each room. Each history element + * contains the phase (e.g. RightPanelPhase.RoomMemberInfo) and the state (e.g. + * the member) associated with it. + * Groups are treated the same as rooms (they are also stored in the byRoom + * object). This is possible since the store keeps track of the opened + * room/group -> the store will provide the correct history for that group/room. +*/ +export default class RightPanelStore extends ReadyWatchingStore { + private static internalInstance: RightPanelStore; + private viewedRoomId: string; + private isViewingRoom?: boolean; + private dispatcherRefRightPanelStore: string; + private roomStoreToken: EventSubscription; + + private global?: IRightPanelForRoom = null; + private byRoom: { + [roomId: string]: IRightPanelForRoom; + } = {}; + + private constructor() { + super(defaultDispatcher); + this.dispatcherRefRightPanelStore = defaultDispatcher.register(this.onDispatch); + } + + protected async onReady(): Promise { + // TODO RightPanelStore (will be addressed when dropping groups): This should be used instead of the onDispatch callback when groups are removed. + // RoomViewStore.on(UPDATE_EVENT, this.onRoomViewStoreUpdate); + MatrixClientPeg.get().on("crypto.verification.request", this.onVerificationRequestUpdate); + this.loadCacheFromSettings(); + this.emitAndUpdateSettings(); + } + public destroy() { + if (this.dispatcherRefRightPanelStore) { + defaultDispatcher.unregister(this.dispatcherRefRightPanelStore); + } + super.destroy(); + } + + protected async onNotReady(): Promise { + if (this.roomStoreToken) { + this.roomStoreToken.remove(); + } + MatrixClientPeg.get().off("crypto.verification.request", this.onVerificationRequestUpdate); + // TODO RightPanelStore (will be addressed when dropping groups): User this instead of the dispatcher. + // RoomViewStore.off(UPDATE_EVENT, this.onRoomViewStoreUpdate); + } + + // Getters + public get isOpenForRoom(): boolean { + return this.byRoom[this.viewedRoomId]?.isOpen ?? false; + } + + public get roomPhaseHistory(): Array { + return this.byRoom[this.viewedRoomId]?.history ?? []; + } + + public get currentCard(): IRightPanelCard { + const hist = this.roomPhaseHistory; + if (hist.length >= 1) { + return hist[hist.length - 1]; + } + return { state: {}, phase: null }; + } + + public currentCardForRoom(roomId: string): IRightPanelCard { + const hist = this.byRoom[roomId]?.history ?? []; + if (hist.length > 0) { + return hist[hist.length - 1]; + } + return this.currentCard ?? { state: {}, phase: null }; + } + + public get previousCard(): IRightPanelCard { + const hist = this.roomPhaseHistory; + if (hist?.length >= 2) { + return hist[hist.length - 2]; + } + return { state: {}, phase: null }; + } + + // The Group associated getters are just for backwards compatibility. Can be removed when deprecating groups. + public get isOpenForGroup(): boolean { return this.isOpenForRoom; } + public get groupPhaseHistory(): Array { return this.roomPhaseHistory; } + public get currentGroup(): IRightPanelCard { return this.currentCard; } + public get previousGroup(): IRightPanelCard { return this.previousCard; } + + // Setters + public setCard(card: IRightPanelCard, allowClose = true, roomId?: string) { + const rId = roomId ?? this.viewedRoomId; + // this was previously a very multifunctional command: + // Toggle panel: if the same phase is send but without a state + // Update state: if the same phase is send but with a state + // Set right panel and erase history: if a "different to the current" phase is send (with or without a state) + const redirect = this.getVerificationRedirect(card); + const targetPhase = redirect?.phase ?? card.phase; + const cardState = redirect?.state ?? (Object.keys(card.state ?? {}).length === 0 ? null : card.state); + + // Checks for wrong SetRightPanelPhase requests + if (!this.isPhaseActionIsValid(targetPhase)) return; + + if (targetPhase === this.currentCard?.phase && + allowClose && + (this.compareCards({ phase: targetPhase, state: cardState }, this.currentCard) || !cardState) + ) { + // Toggle panel: a toggle command needs to fullfil the following: + // - the same phase + // - the panel can be closed + // - does not contain any state information (state) + if (targetPhase != RightPanelPhases.EncryptionPanel) { + this.togglePanel(rId); + } + return; + } else if ((targetPhase === this.currentCardForRoom(rId)?.phase && !!cardState)) { + // Update state: set right panel with a new state but keep the phase (dont know it this is ever needed...) + const hist = this.byRoom[rId]?.history ?? []; + hist[hist.length - 1].state = cardState; + this.emitAndUpdateSettings(); + return; + } else if (targetPhase !== this.currentCard?.phase) { + // Set right panel and erase history. + this.setRightPanelCache({ phase: targetPhase, state: cardState ?? {} }, rId); + } + } + + public pushCard( + card: IRightPanelCard, + allowClose = true, + roomId: string = null, + ) { + const rId = roomId ?? this.viewedRoomId; + const redirect = this.getVerificationRedirect(card); + const targetPhase = redirect?.phase ?? card.phase; + const pState = redirect?.state ?? (Object.keys(card.state ?? {}).length === 0 ? null : card.state); + + // Checks for wrong SetRightPanelPhase requests + if (!this.isPhaseActionIsValid(targetPhase)) return; + + let roomCache = this.byRoom[rId]; + if (!!roomCache) { + // append new phase + roomCache.history.push({ state: pState, phase: targetPhase }); + roomCache.isOpen = allowClose ? roomCache.isOpen : true; + } else { + // setup room panel cache with the new card + roomCache = { + history: [{ phase: targetPhase, state: pState ?? {} }], + // if there was no right panel store object the the panel was closed -> keep it closed, except if allowClose==false + isOpen: !allowClose, + }; + } + + this.emitAndUpdateSettings(); + } + + public popCard(roomId: string = null) { + const rId = roomId ?? this.viewedRoomId; + if (!this.byRoom[rId]) return; + + const removedCard = this.byRoom[rId].history.pop(); + this.emitAndUpdateSettings(); + return removedCard; + } + + public togglePanel(roomId: string = null) { + const rId = roomId ?? this.viewedRoomId; + if (!this.byRoom[rId]) return; + + this.byRoom[rId].isOpen = !this.byRoom[rId].isOpen; + this.emitAndUpdateSettings(); + } + + // Private + private loadCacheFromSettings() { + const room = this.mxClient?.getRoom(this.viewedRoomId); + if (!!room) { + this.global = this.global ?? + convertToStatePanel(SettingsStore.getValue("RightPanel.phasesGlobal"), room); + this.byRoom[this.viewedRoomId] = this.byRoom[this.viewedRoomId] ?? + convertToStatePanel(SettingsStore.getValue("RightPanel.phases", this.viewedRoomId), room); + } else { + console.warn("Could not restore the right panel after load because there was no associated room object." + + "The right panel can only be restored for rooms and spaces but not for groups"); + } + } + + private compareCards(a: IRightPanelCard, b: IRightPanelCard): boolean { + return JSON.stringify(convertCardToStore(a)) == JSON.stringify(convertCardToStore(b)); + } + + private emitAndUpdateSettings() { + const storePanelGlobal = convertToStorePanel(this.global); + SettingsStore.setValue("RightPanel.phasesGlobal", null, SettingLevel.DEVICE, storePanelGlobal); + + if (!!this.viewedRoomId) { + const storePanelThisRoom = convertToStorePanel(this.byRoom[this.viewedRoomId]); + SettingsStore.setValue( + "RightPanel.phases", + this.viewedRoomId, + SettingLevel.ROOM_DEVICE, + storePanelThisRoom, + ); + } + this.emit(UPDATE_EVENT, null); + } + + private setRightPanelCache(card: IRightPanelCard, roomId?: string) { + this.byRoom[roomId ?? this.viewedRoomId] = { + history: [{ phase: card.phase, state: card.state ?? {} }], + isOpen: true, + }; + this.emitAndUpdateSettings(); + } + + private getVerificationRedirect(card: IRightPanelCard): IRightPanelCard { + if (card.phase === RightPanelPhases.RoomMemberInfo && card.state) { + // RightPanelPhases.RoomMemberInfo -> needs to be changed to RightPanelPhases.EncryptionPanel if there is a pending verification request + const { member } = card.state; + const pendingRequest = pendingVerificationRequestForUser(member); + if (pendingRequest) { + return { + phase: RightPanelPhases.EncryptionPanel, + state: { + verificationRequest: pendingRequest, + member, + }, + }; + } + } + return null; + } + + private isPhaseActionIsValid(targetPhase) { + if (!RightPanelPhases[targetPhase]) { + logger.warn(`Tried to switch right panel to unknown phase: ${targetPhase}`); + return false; + } + if (GROUP_PHASES.includes(targetPhase) && this.isViewingRoom) { + logger.warn( + `Tried to switch right panel to a group phase: ${targetPhase}, ` + + `but we are currently not viewing a group`, + ); + return false; + } else if (!GROUP_PHASES.includes(targetPhase) && !this.isViewingRoom) { + logger.warn( + `Tried to switch right panel to a room phase: ${targetPhase}, ` + + `but we are currently not viewing a room`, + ); + return false; + } + return true; + } + + private onVerificationRequestUpdate = () => { + const { member } = this.currentCard.state; + const pendingRequest = pendingVerificationRequestForUser(member); + if (pendingRequest) { + this.currentCard.state.verificationRequest = pendingRequest; + this.emitAndUpdateSettings(); + } + }; + + onRoomViewStoreUpdate() { + // TODO: use this function instead of the onDispatch (the whole onDispatch can get removed!) as soon groups are removed + // this.viewedRoomId = RoomViewStore.getRoomId(); + // this.isViewingRoom = true; // Is viewing room will of course be removed when removing groups + // // load values from byRoomCache with the viewedRoomId. + // this.loadCacheFromSettings(); + } + + onDispatch(payload: ActionPayload) { + switch (payload.action) { + case 'view_group': + case Action.ViewRoom: { + const _this = RightPanelStore.instance; + if (payload.room_id === _this.viewedRoomId) break; // skip this transition, probably a permalink + + // Put group in the same/similar view to what was open from the previously viewed room + // Is contradictory to the new "per room" philosophy but it is the legacy behavior for groups. + if ((_this.isViewingRoom ? Action.ViewRoom : "view_group") != payload.action) { + if (payload.action == Action.ViewRoom && MEMBER_INFO_PHASES.includes(_this.currentCard?.phase)) { + // switch from group to room + _this.setRightPanelCache({ phase: RightPanelPhases.RoomMemberList, state: {} }); + } else if ( + payload.action == "view_group" && + _this.currentCard?.phase === RightPanelPhases.GroupMemberInfo + ) { + // switch from room to group + _this.setRightPanelCache({ phase: RightPanelPhases.GroupMemberList, state: {} }); + } + } + + // Update the current room here, so that all the other functions dont need to be room dependant. + // The right panel store always will return the state for the current room. + _this.viewedRoomId = payload.room_id; + _this.isViewingRoom = payload.action == Action.ViewRoom; + // load values from byRoomCache with the viewedRoomId. + if (!!_this.roomStoreToken) { + // skip loading here since we need the client to be ready to get the events form the ids of the settings + // this loading will be done in the onReady function + // all the logic in this case is not necessary anymore as soon as groups are dropped and we use: onRoomViewStoreUpdate + _this.loadCacheFromSettings(); + _this.emitAndUpdateSettings(); + } + break; + } + } + } + + public static get instance(): RightPanelStore { + if (!RightPanelStore.internalInstance) { + RightPanelStore.internalInstance = new RightPanelStore(); + } + return RightPanelStore.internalInstance; + } +} + +window.mxRightPanelStore = RightPanelStore.instance; diff --git a/src/stores/right-panel/RightPanelStoreIPanelState.ts b/src/stores/right-panel/RightPanelStoreIPanelState.ts new file mode 100644 index 0000000000..93df4bbd94 --- /dev/null +++ b/src/stores/right-panel/RightPanelStoreIPanelState.ts @@ -0,0 +1,136 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { User } from "matrix-js-sdk/src/models/user"; +import { Room } from "matrix-js-sdk/src/models/room"; +import { RoomMember } from "matrix-js-sdk/src/models/room-member"; +import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; + +import { GroupMember } from "../../components/views/right_panel/UserInfo"; +import { RightPanelPhases } from "./RightPanelStorePhases"; + +export interface IRightPanelCardState { + member?: RoomMember | User | GroupMember; + verificationRequest?: VerificationRequest; + verificationRequestPromise?: Promise; + // group + groupId?: string; + groupRoomId?: string; + widgetId?: string; + spaceId?: string; + // Room3pidMemberInfo, Space3pidMemberInfo, + memberInfoEvent?: MatrixEvent; + // threads + threadHeadEvent?: MatrixEvent; + initialEvent?: MatrixEvent; + isInitialEventHighlighted?: boolean; +} + +export interface IRightPanelCardStateStored { + memberId?: string; + // we do not store the things associated with verification + // group + groupId?: string; + groupRoomId?: string; + widgetId?: string; + spaceId?: string; + // 3pidMemberInfo + memberInfoEventId?: string; + // threads + threadHeadEventId?: string; + initialEventId?: string; + isInitialEventHighlighted?: boolean; +} + +export interface IRightPanelCard { + phase: RightPanelPhases; + state?: IRightPanelCardState; +} + +export interface IRightPanelCardStored { + phase: RightPanelPhases; + state?: IRightPanelCardStateStored; +} + +export interface IRightPanelForRoom { + isOpen: boolean; + history: Array; +} + +interface IRightPanelForRoomStored { + isOpen: boolean; + history: Array; +} + +export function convertToStorePanel(cacheRoom: IRightPanelForRoom): IRightPanelForRoomStored { + if (!cacheRoom) return cacheRoom; + const storeHistory = [...cacheRoom.history].map(panelState => convertCardToStore(panelState)); + return { isOpen: cacheRoom.isOpen, history: storeHistory }; +} + +export function convertToStatePanel(storeRoom: IRightPanelForRoomStored, room: Room): IRightPanelForRoom { + if (!storeRoom) return storeRoom; + const stateHistory = [...storeRoom.history].map(panelStateStore => convertStoreToCard(panelStateStore, room)); + return { history: stateHistory, isOpen: storeRoom.isOpen }; +} + +export function convertCardToStore(panelState: IRightPanelCard): IRightPanelCardStored { + const panelStateThisRoomStored = { ...panelState.state } as any; + if (!!panelState?.state?.threadHeadEvent?.getId()) { + panelStateThisRoomStored.threadHeadEventId = panelState.state.threadHeadEvent.getId(); + } + if (!!panelState?.state?.memberInfoEvent?.getId()) { + panelStateThisRoomStored.memberInfoEventId = panelState.state.memberInfoEvent.getId(); + } + if (!!panelState?.state?.initialEvent?.getId()) { + panelStateThisRoomStored.initialEventId = panelState.state.initialEvent.getId(); + } + if (!!panelState?.state?.member?.userId) { + panelStateThisRoomStored.memberId = panelState.state.member.userId; + } + delete panelStateThisRoomStored.threadHeadEvent; + delete panelStateThisRoomStored.initialEvent; + delete panelStateThisRoomStored.memberInfoEvent; + delete panelStateThisRoomStored.verificationRequest; + delete panelStateThisRoomStored.verificationRequestPromise; + delete panelStateThisRoomStored.member; + + const storedCard = { state: panelStateThisRoomStored as IRightPanelCardStored, phase: panelState.phase }; + return storedCard as IRightPanelCardStored; +} + +function convertStoreToCard(panelStateStore: IRightPanelCardStored, room: Room): IRightPanelCard { + const panelStateThisRoom = { ...panelStateStore?.state } as any; + if (!!panelStateThisRoom.threadHeadEventId) { + panelStateThisRoom.threadHeadEvent = room.findEventById(panelStateThisRoom.threadHeadEventId); + } + if (!!panelStateThisRoom.memberInfoEventId) { + panelStateThisRoom.memberInfoEvent = room.findEventById(panelStateThisRoom.memberInfoEventId); + } + if (!!panelStateThisRoom.initialEventId) { + panelStateThisRoom.initialEvent = room.findEventById(panelStateThisRoom.initialEventId); + } + if (!!panelStateThisRoom.memberId) { + panelStateThisRoom.member = room.getMember(panelStateThisRoom.memberId); + } + delete panelStateThisRoom.threadHeadEventId; + delete panelStateThisRoom.initialEventId; + delete panelStateThisRoom.memberInfoEventId; + delete panelStateThisRoom.memberId; + + return { state: panelStateThisRoom as IRightPanelCardState, phase: panelStateStore.phase } as IRightPanelCard; +} diff --git a/src/stores/RightPanelStorePhases.ts b/src/stores/right-panel/RightPanelStorePhases.ts similarity index 100% rename from src/stores/RightPanelStorePhases.ts rename to src/stores/right-panel/RightPanelStorePhases.ts diff --git a/src/verification.ts b/src/verification.ts index e17e0e4e09..2652733975 100644 --- a/src/verification.ts +++ b/src/verification.ts @@ -16,17 +16,18 @@ limitations under the License. import { User } from "matrix-js-sdk/src/models/user"; import { verificationMethods as VerificationMethods } from 'matrix-js-sdk/src/crypto'; +import { RoomMember } from "matrix-js-sdk/src/matrix"; import { MatrixClientPeg } from './MatrixClientPeg'; import dis from "./dispatcher/dispatcher"; import Modal from './Modal'; -import { RightPanelPhases } from "./stores/RightPanelStorePhases"; +import { RightPanelPhases } from "./stores/right-panel/RightPanelStorePhases"; import { findDMForUser } from './createRoom'; import { accessSecretStorage } from './SecurityManager'; -import { Action } from './dispatcher/actions'; import UntrustedDeviceDialog from "./components/views/dialogs/UntrustedDeviceDialog"; -import { IDevice } from "./components/views/right_panel/UserInfo"; +import { GroupMember, IDevice } from "./components/views/right_panel/UserInfo"; import ManualDeviceKeyVerificationDialog from "./components/views/dialogs/ManualDeviceKeyVerificationDialog"; +import RightPanelStore from "./stores/right-panel/RightPanelStore"; async function enable4SIfNeeded() { const cli = MatrixClientPeg.get(); @@ -65,10 +66,9 @@ export async function verifyDevice(user: User, device: IDevice) { device.deviceId, VerificationMethods.SAS, ); - dis.dispatch({ - action: Action.SetRightPanelPhase, + RightPanelStore.instance.setCard({ phase: RightPanelPhases.EncryptionPanel, - refireParams: { member: user, verificationRequestPromise }, + state: { member: user, verificationRequestPromise }, }); } else if (action === "legacy") { Modal.createTrackedDialog("Legacy verify session", "legacy verify session", @@ -96,10 +96,9 @@ export async function legacyVerifyUser(user: User) { } } const verificationRequestPromise = cli.requestVerification(user.userId); - dis.dispatch({ - action: Action.SetRightPanelPhase, + RightPanelStore.instance.setCard({ phase: RightPanelPhases.EncryptionPanel, - refireParams: { member: user, verificationRequestPromise }, + state: { member: user, verificationRequestPromise }, }); } @@ -113,17 +112,13 @@ export async function verifyUser(user: User) { return; } const existingRequest = pendingVerificationRequestForUser(user); - dis.dispatch({ - action: Action.SetRightPanelPhase, + RightPanelStore.instance.setCard({ phase: RightPanelPhases.EncryptionPanel, - refireParams: { - member: user, - verificationRequest: existingRequest, - }, + state: { member: user, verificationRequest: existingRequest }, }); } -export function pendingVerificationRequestForUser(user: User) { +export function pendingVerificationRequestForUser(user: User | RoomMember | GroupMember ) { const cli = MatrixClientPeg.get(); const dmRoom = findDMForUser(cli, user.userId); if (dmRoom) {