From 4ab347018415e5fc6d10828962619634eab4e159 Mon Sep 17 00:00:00 2001 From: Timo <16718859+toger5@users.noreply.github.com> Date: Wed, 5 Jan 2022 17:25:41 +0100 Subject: [PATCH] History based navigation with new right panel store (#7398) Co-authored-by: J. Ryan Stinnett --- src/components/structures/FilePanel.tsx | 5 -- src/components/structures/RightPanel.tsx | 15 ---- src/components/structures/RoomView.tsx | 25 ++++--- src/components/structures/ThreadView.tsx | 22 ------ src/components/views/avatars/MemberAvatar.tsx | 8 +- .../messages/MKeyVerificationRequest.tsx | 9 ++- .../views/messages/MessageActionBar.tsx | 32 ++++---- src/components/views/right_panel/BaseCard.tsx | 37 +++++----- .../views/right_panel/EncryptionPanel.tsx | 11 ++- .../views/right_panel/GroupHeaderButtons.tsx | 7 +- .../views/right_panel/HeaderButtons.tsx | 8 +- .../views/right_panel/RoomHeaderButtons.tsx | 16 +++- .../views/right_panel/RoomSummaryCard.tsx | 9 +-- src/components/views/right_panel/UserInfo.tsx | 12 +-- .../views/right_panel/WidgetCard.tsx | 6 +- src/components/views/rooms/EventTile.tsx | 39 +++++----- src/components/views/rooms/MemberList.tsx | 6 -- src/components/views/rooms/MemberTile.tsx | 1 + .../views/toasts/VerificationRequestToast.tsx | 12 +-- src/dispatcher/dispatch-actions/threads.ts | 33 ++++++--- src/i18n/strings/en_EN.json | 7 +- src/stores/right-panel/RightPanelStore.ts | 64 +++++++--------- .../right-panel/RightPanelStoreIPanelState.ts | 73 +++++++++---------- .../right-panel/RightPanelStorePhases.ts | 13 ++++ src/verification.ts | 30 +++++--- 25 files changed, 248 insertions(+), 252 deletions(-) diff --git a/src/components/structures/FilePanel.tsx b/src/components/structures/FilePanel.tsx index 63b044b6e5..c77734db39 100644 --- a/src/components/structures/FilePanel.tsx +++ b/src/components/structures/FilePanel.tsx @@ -27,7 +27,6 @@ import { logger } from "matrix-js-sdk/src/logger"; import { MatrixClientPeg } from '../../MatrixClientPeg'; import EventIndexPeg from "../../indexing/EventIndexPeg"; import { _t } from '../../languageHandler'; -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"; @@ -221,7 +220,6 @@ class FilePanel extends React.Component { return
{ _t("You must register to use this functionality", @@ -234,7 +232,6 @@ class FilePanel extends React.Component { return
{ _t("You must join the room to see its files") }
; @@ -258,7 +255,6 @@ class FilePanel extends React.Component { @@ -285,7 +281,6 @@ class FilePanel extends React.Component { diff --git a/src/components/structures/RightPanel.tsx b/src/components/structures/RightPanel.tsx index b01f7b9010..6effdc7dd4 100644 --- a/src/components/structures/RightPanel.tsx +++ b/src/components/structures/RightPanel.tsx @@ -27,12 +27,10 @@ import GroupStore from '../../stores/GroupStore'; 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"; import WidgetCard from "../views/right_panel/WidgetCard"; import { replaceableComponent } from "../../utils/replaceableComponent"; import SettingsStore from "../../settings/SettingsStore"; -import { ActionPayload } from "../../dispatcher/payloads"; import MemberList from "../views/rooms/MemberList"; import GroupMemberList from "../views/groups/GroupMemberList"; import GroupRoomList from "../views/groups/GroupRoomList"; @@ -47,7 +45,6 @@ import ResizeNotifier from "../../utils/ResizeNotifier"; import PinnedMessagesCard from "../views/right_panel/PinnedMessagesCard"; 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'; @@ -72,8 +69,6 @@ interface IState { export default class RightPanel extends React.Component { static contextType = MatrixClientContext; - private dispatcherRef: string; - constructor(props, context) { super(props, context); @@ -90,7 +85,6 @@ export default class RightPanel extends React.Component { }, 500, { leading: true, trailing: true }); 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); @@ -98,7 +92,6 @@ export default class RightPanel extends React.Component { } public componentWillUnmount(): void { - dis.unregister(this.dispatcherRef); if (this.context) { this.context.removeListener("RoomState.members", this.onRoomStateMember); } @@ -153,14 +146,6 @@ export default class RightPanel extends React.Component { }); }; - 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(); - } - }; - 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 diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index a20f0e55ae..080e09bb95 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -94,7 +94,7 @@ import MessageComposer from '../views/rooms/MessageComposer'; import JumpToBottomButton from "../views/rooms/JumpToBottomButton"; import TopUnreadMessagesBar from "../views/rooms/TopUnreadMessagesBar"; import SpaceStore from "../../stores/spaces/SpaceStore"; -import { dispatchShowThreadEvent } from '../../dispatcher/dispatch-actions/threads'; +import { showThread } from '../../dispatcher/dispatch-actions/threads'; import { fetchInitialEvent } from "../../utils/EventUtils"; import { ComposerType } from "../../dispatcher/payloads/ComposerInsertPayload"; import AppsDrawer from '../views/rooms/AppsDrawer'; @@ -338,6 +338,13 @@ export class RoomView extends React.Component { if (WidgetLayoutStore.instance.hasMaximisedWidget(this.state.room)) { // Show chat in right panel when a widget is maximised RightPanelStore.instance.setCard({ phase: RightPanelPhases.Timeline }); + } else if ( + RightPanelStore.instance.isOpenForRoom && + RightPanelStore.instance.roomPhaseHistory.some(card => (card.phase === RightPanelPhases.Timeline)) + ) { + // hide chat in right panel when the widget is minimized + RightPanelStore.instance.setCard({ phase: RightPanelPhases.RoomSummary }); + RightPanelStore.instance.togglePanel(); } this.checkWidgets(this.state.room); }; @@ -424,21 +431,21 @@ export class RoomView extends React.Component { const thread = initialEvent?.getThread(); if (thread && !initialEvent?.isThreadRoot) { - dispatchShowThreadEvent( - thread.rootEvent, + showThread({ + rootEvent: thread.rootEvent, initialEvent, - RoomViewStore.isInitialEventHighlighted(), - ); + highlighted: RoomViewStore.isInitialEventHighlighted(), + }); } else { newState.initialEventId = initialEventId; newState.isInitialEventHighlighted = RoomViewStore.isInitialEventHighlighted(); if (thread && initialEvent?.isThreadRoot) { - dispatchShowThreadEvent( - thread.rootEvent, + showThread({ + rootEvent: thread.rootEvent, initialEvent, - RoomViewStore.isInitialEventHighlighted(), - ); + highlighted: RoomViewStore.isInitialEventHighlighted(), + }); } } } diff --git a/src/components/structures/ThreadView.tsx b/src/components/structures/ThreadView.tsx index cb46b37fc7..42cd3851e8 100644 --- a/src/components/structures/ThreadView.tsx +++ b/src/components/structures/ThreadView.tsx @@ -40,8 +40,6 @@ import UploadBar from './UploadBar'; import { _t } from '../../languageHandler'; import ThreadListContextMenu from '../views/context_menus/ThreadListContextMenu'; import RightPanelStore from '../../stores/right-panel/RightPanelStore'; -import SettingsStore from '../../settings/SettingsStore'; -import { WidgetLayoutStore } from '../../stores/widgets/WidgetLayoutStore'; interface IProps { room: Room; @@ -194,24 +192,6 @@ export default class ThreadView extends React.Component { event_id: this.state.thread?.id, }; - let previousPhase = RightPanelStore.instance.previousCard.phase; - if (!SettingsStore.getValue("feature_maximised_widgets")) { - previousPhase = RightPanelPhases.ThreadPanel; - } - - // change the previous phase to the threadPanel in case there is no maximised widget anymore - if (!WidgetLayoutStore.instance.hasMaximisedWidget(this.props.room)) { - previousPhase = RightPanelPhases.ThreadPanel; - } - - // Make sure the previous Phase is always one of the two: Timeline or ThreadPanel - if (![RightPanelPhases.ThreadPanel, RightPanelPhases.Timeline].includes(previousPhase)) { - previousPhase = RightPanelPhases.ThreadPanel; - } - const previousPhaseLabels = {}; - previousPhaseLabels[RightPanelPhases.ThreadPanel] = _t("All threads"); - previousPhaseLabels[RightPanelPhases.Timeline] = _t("Chat"); - return ( { diff --git a/src/components/views/avatars/MemberAvatar.tsx b/src/components/views/avatars/MemberAvatar.tsx index 9ecaf8030b..dfd526b1a2 100644 --- a/src/components/views/avatars/MemberAvatar.tsx +++ b/src/components/views/avatars/MemberAvatar.tsx @@ -25,6 +25,7 @@ import { Action } from "../../../dispatcher/actions"; import BaseAvatar from "./BaseAvatar"; import { replaceableComponent } from "../../../utils/replaceableComponent"; import { mediaFromMxc } from "../../../customisations/Media"; +import { CardContext } from '../right_panel/BaseCard'; interface IProps extends Omit, "name" | "idName" | "url"> { member: RoomMember; @@ -36,6 +37,7 @@ interface IProps extends Omit, "name" | onClick?: React.MouseEventHandler; // Whether the onClick of the avatar should be overridden to dispatch `Action.ViewUser` viewUserOnClick?: boolean; + pushUserOnClick?: boolean; title?: string; style?: any; } @@ -99,6 +101,7 @@ export default class MemberAvatar extends React.Component { dis.dispatch({ action: Action.ViewUser, member: this.props.member, + push: this.context.isCard, }); }; } @@ -109,7 +112,10 @@ export default class MemberAvatar extends React.Component { title={this.state.title} idName={userId} url={this.state.imageUrl} - onClick={onClick} /> + onClick={onClick} + /> ); } } + +MemberAvatar.contextType = CardContext; diff --git a/src/components/views/messages/MKeyVerificationRequest.tsx b/src/components/views/messages/MKeyVerificationRequest.tsx index 0361258833..0243ea0200 100644 --- a/src/components/views/messages/MKeyVerificationRequest.tsx +++ b/src/components/views/messages/MKeyVerificationRequest.tsx @@ -51,10 +51,11 @@ export default class MKeyVerificationRequest extends React.Component { private openRequest = () => { const { verificationRequest } = this.props.mxEvent; const member = MatrixClientPeg.get().getUser(verificationRequest.otherUserId); - RightPanelStore.instance.setCard({ - phase: RightPanelPhases.EncryptionPanel, - state: { verificationRequest, member }, - }); + RightPanelStore.instance.setCards([ + { phase: RightPanelPhases.RoomSummary }, + { phase: RightPanelPhases.RoomMemberInfo, state: { member } }, + { phase: RightPanelPhases.EncryptionPanel, state: { verificationRequest, member } }, + ]); }; private onRequestChanged = () => { diff --git a/src/components/views/messages/MessageActionBar.tsx b/src/components/views/messages/MessageActionBar.tsx index 87433fb4de..6213e3b050 100644 --- a/src/components/views/messages/MessageActionBar.tsx +++ b/src/components/views/messages/MessageActionBar.tsx @@ -39,8 +39,9 @@ import DownloadActionButton from "./DownloadActionButton"; import SettingsStore from '../../../settings/SettingsStore'; import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks'; import ReplyChain from '../elements/ReplyChain'; -import { dispatchShowThreadEvent } from '../../../dispatcher/dispatch-actions/threads'; +import { showThread } from '../../../dispatcher/dispatch-actions/threads'; import ReactionPicker from "../emojipicker/ReactionPicker"; +import { CardContext } from '../right_panel/BaseCard'; interface IOptionsButtonProps { mxEvent: MatrixEvent; @@ -219,8 +220,8 @@ export default class MessageActionBar extends React.PureComponent { - dispatchShowThreadEvent(this.props.mxEvent); + private onThreadClick = (isCard: boolean): void => { + showThread({ rootEvent: this.props.mxEvent, push: isCard }); dis.dispatch({ action: Action.FocusSendMessageComposer, context: TimelineRenderingType.Thread, @@ -303,6 +304,17 @@ export default class MessageActionBar extends React.PureComponent; + const threadTooltipButton = + { context => + + } + ; + // We show a different toolbar for failed events, so detect that first. const mxEvent = this.props.mxEvent; const editStatus = mxEvent.replacingEvent() && mxEvent.replacingEvent().status; @@ -335,12 +347,7 @@ export default class MessageActionBar extends React.PureComponent { (this.showReplyInThreadAction) && ( - + threadTooltipButton ) } ); } @@ -368,12 +375,7 @@ export default class MessageActionBar extends React.PureComponent); + toolbarOpts.unshift(threadTooltipButton); } if (allowCancel) { diff --git a/src/components/views/right_panel/BaseCard.tsx b/src/components/views/right_panel/BaseCard.tsx index 94cfed71ed..8226b42072 100644 --- a/src/components/views/right_panel/BaseCard.tsx +++ b/src/components/views/right_panel/BaseCard.tsx @@ -20,16 +20,15 @@ import classNames from 'classnames'; import AutoHideScrollbar from "../../structures/AutoHideScrollbar"; import { _t } from "../../../languageHandler"; import AccessibleButton from "../elements/AccessibleButton"; -import { RightPanelPhases } from '../../../stores/right-panel/RightPanelStorePhases'; import RightPanelStore from '../../../stores/right-panel/RightPanelStore'; +import { backLabelForPhase } from '../../../stores/right-panel/RightPanelStorePhases'; +export const CardContext = React.createContext({ isCard: false }); interface IProps { header?: ReactNode; footer?: ReactNode; className?: string; withoutScrollContainer?: boolean; - previousPhase?: RightPanelPhases; - previousPhaseLabel?: string; closeLabel?: string; onClose?(): void; cardState?; @@ -54,20 +53,16 @@ const BaseCard: React.FC = ({ header, footer, withoutScrollContainer, - previousPhase, - previousPhaseLabel, children, - cardState, }) => { let backButton; - if (previousPhase) { + const cardHistory = RightPanelStore.instance.roomPhaseHistory; + if (cardHistory.length > 1) { + const prevCard = cardHistory[cardHistory.length - 2]; const onBackClick = () => { - // TODO RightPanelStore (will be addressed in a follow up PR): this should ideally be: - // RightPanelStore.instance.popRightPanel(); - - RightPanelStore.instance.setCard({ phase: previousPhase, state: cardState }); + RightPanelStore.instance.popCard(); }; - const label = previousPhaseLabel ?? _t("Back"); + const label = backLabelForPhase(prevCard.phase) ?? _t("Back"); backButton = ; } @@ -87,15 +82,17 @@ const BaseCard: React.FC = ({ } return ( -
-
- { backButton } - { closeButton } - { header } + +
+
+ { backButton } + { closeButton } + { header } +
+ { children } + { footer &&
{ footer }
}
- { children } - { footer &&
{ footer }
} -
+ ); }; diff --git a/src/components/views/right_panel/EncryptionPanel.tsx b/src/components/views/right_panel/EncryptionPanel.tsx index a87c3c925d..a5e34ad94e 100644 --- a/src/components/views/right_panel/EncryptionPanel.tsx +++ b/src/components/views/right_panel/EncryptionPanel.tsx @@ -116,10 +116,13 @@ const EncryptionPanel: React.FC = (props: IProps) => { setRequest(verificationRequest_); setPhase(verificationRequest_.phase); // Notify the RightPanelStore about this - RightPanelStore.instance.setCard({ - phase: RightPanelPhases.EncryptionPanel, - state: { member, verificationRequest: verificationRequest_ }, - }); + if (RightPanelStore.instance.currentCard.phase != RightPanelPhases.EncryptionPanel) { + RightPanelStore.instance.pushCard({ + phase: RightPanelPhases.EncryptionPanel, + state: { member, verificationRequest: verificationRequest_ }, + }); + } + if (!RightPanelStore.instance.isOpenForRoom) RightPanelStore.instance.togglePanel(); }, [member]); const requested = diff --git a/src/components/views/right_panel/GroupHeaderButtons.tsx b/src/components/views/right_panel/GroupHeaderButtons.tsx index 723e5440b0..e2f3779c4c 100644 --- a/src/components/views/right_panel/GroupHeaderButtons.tsx +++ b/src/components/views/right_panel/GroupHeaderButtons.tsx @@ -28,6 +28,7 @@ import { Action } from "../../../dispatcher/actions"; import { ActionPayload } from "../../../dispatcher/payloads"; import { ViewUserPayload } from "../../../dispatcher/payloads/ViewUserPayload"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import RightPanelStore from '../../../stores/right-panel/RightPanelStore'; const GROUP_PHASES = [ RightPanelPhases.GroupMemberInfo, @@ -49,7 +50,11 @@ export default class GroupHeaderButtons extends HeaderButtons { protected onAction(payload: ActionPayload) { if (payload.action === Action.ViewUser) { if ((payload as ViewUserPayload).member) { - this.setPhase(RightPanelPhases.RoomMemberInfo, { member: payload.member }); + RightPanelStore.instance.setCards([ + { phase: RightPanelPhases.GroupRoomInfo }, + { phase: RightPanelPhases.GroupMemberList }, + { phase: RightPanelPhases.RoomMemberInfo, state: { member: payload.member } }, + ]); } else { this.setPhase(RightPanelPhases.GroupMemberList); } diff --git a/src/components/views/right_panel/HeaderButtons.tsx b/src/components/views/right_panel/HeaderButtons.tsx index ef2b03af39..b3ae8e7503 100644 --- a/src/components/views/right_panel/HeaderButtons.tsx +++ b/src/components/views/right_panel/HeaderButtons.tsx @@ -71,7 +71,13 @@ export default abstract class HeaderButtons

extends React.Component) { - RightPanelStore.instance.setCard({ phase, state: cardState }); + const rps = RightPanelStore.instance; + if (rps.currentCard.phase == phase && !cardState && rps.isOpenForRoom) { + rps.togglePanel(); + } else { + RightPanelStore.instance.setCard({ phase, state: cardState }); + if (!rps.isOpenForRoom) rps.togglePanel(); + } } public isPhase(phases: string | string[]) { diff --git a/src/components/views/right_panel/RoomHeaderButtons.tsx b/src/components/views/right_panel/RoomHeaderButtons.tsx index 0cf4c9a5ee..4fa6c5585f 100644 --- a/src/components/views/right_panel/RoomHeaderButtons.tsx +++ b/src/components/views/right_panel/RoomHeaderButtons.tsx @@ -32,7 +32,7 @@ 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 { showThreadPanel } from "../../../dispatcher/dispatch-actions/threads"; import SettingsStore from "../../../settings/SettingsStore"; import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore"; import { NotificationColor } from "../../../stores/notifications/NotificationColor"; @@ -154,7 +154,17 @@ export default class RoomHeaderButtons extends HeaderButtons { protected onAction(payload: ActionPayload) { if (payload.action === Action.ViewUser) { if (payload.member) { - this.setPhase(RightPanelPhases.RoomMemberInfo, { member: payload.member }); + if (payload.push) { + RightPanelStore.instance.pushCard( + { phase: RightPanelPhases.RoomMemberInfo, state: { member: payload.member } }, + ); + } else { + RightPanelStore.instance.setCards([ + { phase: RightPanelPhases.RoomSummary }, + { phase: RightPanelPhases.RoomMemberList }, + { phase: RightPanelPhases.RoomMemberInfo, state: { member: payload.member } }, + ]); + } } else { this.setPhase(RightPanelPhases.RoomMemberList); } @@ -199,7 +209,7 @@ export default class RoomHeaderButtons extends HeaderButtons { if (RoomHeaderButtons.THREAD_PHASES.includes(this.state.phase)) { RightPanelStore.instance.togglePanel(); } else { - dispatchShowThreadsPanelEvent(); + showThreadPanel(); } }; diff --git a/src/components/views/right_panel/RoomSummaryCard.tsx b/src/components/views/right_panel/RoomSummaryCard.tsx index c3aeb6fe48..5f0eca0551 100644 --- a/src/components/views/right_panel/RoomSummaryCard.tsx +++ b/src/components/views/right_panel/RoomSummaryCard.tsx @@ -102,8 +102,7 @@ const AppRow: React.FC = ({ app, room }) => { }, [room.roomId]); const onOpenWidgetClick = () => { - // TODO RightPanelStore (will be addressed in a follow up PR): should push the widget - RightPanelStore.instance.setCard({ + RightPanelStore.instance.pushCard({ phase: RightPanelPhases.Widget, state: { widgetId: app.id }, }); @@ -234,13 +233,11 @@ const AppsSection: React.FC = ({ room }) => { }; export const onRoomMembersClick = (allowClose = true) => { - // TODO RightPanelStore (will be addressed in a follow up PR): should push the phase - RightPanelStore.instance.setCard({ phase: RightPanelPhases.RoomMemberList }, allowClose); + RightPanelStore.instance.pushCard({ phase: RightPanelPhases.RoomMemberList }, allowClose); }; export const onRoomFilesClick = (allowClose = true) => { - // TODO RightPanelStore (will be addressed in a follow up PR): should push the phase - RightPanelStore.instance.setCard({ phase: RightPanelPhases.FilePanel }, allowClose); + RightPanelStore.instance.pushCard({ phase: RightPanelPhases.FilePanel }, allowClose); }; const onRoomSettingsClick = () => { diff --git a/src/components/views/right_panel/UserInfo.tsx b/src/components/views/right_panel/UserInfo.tsx index 09b015e97a..d7911fc9d6 100644 --- a/src/components/views/right_panel/UserInfo.tsx +++ b/src/components/views/right_panel/UserInfo.tsx @@ -1651,21 +1651,15 @@ const UserInfo: React.FC = ({ const classes = ["mx_UserInfo"]; 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; cardState = { member }; } else if (room?.isSpaceRoom() && SpaceStore.spacesEnabled) { - previousPhase = RightPanelPhases.SpaceMemberList; cardState = { spaceId: room.roomId }; - } else if (room) { - previousPhase = RightPanelPhases.RoomMemberList; } const onEncryptionPanelClose = () => { - // TODO RightPanelStore (will be addressed in a follow up PR): here we want to pop the panel - RightPanelStore.instance.setCard({ phase: previousPhase, state: cardState }); + RightPanelStore.instance.popCard(); }; let content; @@ -1679,7 +1673,8 @@ const UserInfo: React.FC = ({ member={member as User} groupId={groupId as string} devices={devices} - isRoomEncrypted={isRoomEncrypted} /> + isRoomEncrypted={isRoomEncrypted} + /> ); break; case RightPanelPhases.EncryptionPanel: @@ -1720,7 +1715,6 @@ const UserInfo: React.FC = ({ header={header} onClose={onClose} closeLabel={closeLabel} - previousPhase={previousPhase} cardState={cardState} > { content } diff --git a/src/components/views/right_panel/WidgetCard.tsx b/src/components/views/right_panel/WidgetCard.tsx index 4f86494545..0bdbf8aa66 100644 --- a/src/components/views/right_panel/WidgetCard.tsx +++ b/src/components/views/right_panel/WidgetCard.tsx @@ -23,7 +23,6 @@ import WidgetUtils from "../../../utils/WidgetUtils"; import AppTile from "../elements/AppTile"; import { _t } from "../../../languageHandler"; import { useWidgets } from "./RoomSummaryCard"; -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"; @@ -48,9 +47,7 @@ const WidgetCard: React.FC = ({ room, widgetId, onClose }) => { useEffect(() => { if (!app || isPinned) { // stop showing this card - - //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 }); + RightPanelStore.instance.popCard(); } }, [app, isPinned]); @@ -88,7 +85,6 @@ const WidgetCard: React.FC = ({ room, widgetId, onClose }) => { header={header} className="mx_WidgetCard" onClose={onClose} - previousPhase={RightPanelPhases.RoomSummary} withoutScrollContainer > { } return ( -

{ - dispatchShowThreadEvent( - this.props.mxEvent, - ); - }} - > - - { _t("%(count)s reply", { - count: this.thread.length, - }) } - - { this.renderThreadLastMessagePreview() } -
+ + { context => +
{ + showThread({ rootEvent: this.props.mxEvent, push: context.isCard }); + }} + > + + { _t("%(count)s reply", { + count: this.thread.length, + }) } + + { this.renderThreadLastMessagePreview() } +
+ } +
); } @@ -1411,7 +1414,7 @@ export default class EventTile extends React.Component { "data-notification": this.state.threadNotification, "onMouseEnter": () => this.setState({ hover: true }), "onMouseLeave": () => this.setState({ hover: false }), - "onClick": () => dispatchShowThreadEvent(this.props.mxEvent), + "onClick": () => showThread({ rootEvent: this.props.mxEvent, push: true }), }, <> { sender } { avatar } @@ -1430,7 +1433,7 @@ export default class EventTile extends React.Component { dispatchShowThreadEvent(this.props.mxEvent)} + onClick={() => showThread({ rootEvent: this.props.mxEvent, push: true })} key="thread" /> { return ; @@ -567,11 +565,8 @@ export default class MemberList extends React.Component { /> ); - let previousPhase = RightPanelPhases.RoomSummary; - // We have no previousPhase for when viewing a MemberList from a Space let scopeHeader; if (SpaceStore.spacesEnabled && room?.isSpaceRoom()) { - previousPhase = undefined; scopeHeader =
@@ -586,7 +581,6 @@ export default class MemberList extends React.Component { } footer={footer} onClose={this.props.onClose} - previousPhase={previousPhase} >
{ dis.dispatch({ action: Action.ViewUser, member: this.props.member, + push: true, }); }; diff --git a/src/components/views/toasts/VerificationRequestToast.tsx b/src/components/views/toasts/VerificationRequestToast.tsx index 3239c8d1e8..a90727f038 100644 --- a/src/components/views/toasts/VerificationRequestToast.tsx +++ b/src/components/views/toasts/VerificationRequestToast.tsx @@ -115,11 +115,13 @@ export default class VerificationRequestToast extends React.PureComponent { - // TODO RightPanelStore (will be addressed in a follow up PR): this should really be a push! - RightPanelStore.instance.setCard({ +export const showThread = (props: { + rootEvent: MatrixEvent; + initialEvent?: MatrixEvent; + highlighted?: boolean; + push?: boolean; +}) => { + const push = props.push ?? false; + const threadViewCard = { phase: RightPanelPhases.ThreadView, state: { - threadHeadEvent: rootEvent, - initialEvent, - isInitialEventHighlighted: highlighted, + threadHeadEvent: props.rootEvent, + initialEvent: props.initialEvent, + isInitialEventHighlighted: props.highlighted, }, - }); + }; + if (push) { + RightPanelStore.instance.pushCard(threadViewCard); + } else { + RightPanelStore.instance.setCards([ + { phase: RightPanelPhases.ThreadPanel }, + threadViewCard, + ]); + } }; -export const dispatchShowThreadsPanelEvent = () => { +export const showThreadPanel = () => { RightPanelStore.instance.setCard({ phase: RightPanelPhases.ThreadPanel }); }; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 7eec988c61..ba50922187 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -843,6 +843,11 @@ "%(senderName)s: %(message)s": "%(senderName)s: %(message)s", "%(senderName)s: %(reaction)s": "%(senderName)s: %(reaction)s", "%(senderName)s: %(stickerName)s": "%(senderName)s: %(stickerName)s", + "Threads": "Threads", + "Back to chat": "Back to chat", + "Room information": "Room information", + "Room members": "Room members", + "Back to thread": "Back to thread", "Change notification settings": "Change notification settings", "Messaging": "Messaging", "Profile": "Profile", @@ -1507,7 +1512,6 @@ "this room": "this room", "View older messages in %(roomName)s.": "View older messages in %(roomName)s.", "Space information": "Space information", - "Room information": "Room information", "Internal room ID:": "Internal room ID:", "Room version": "Room version", "Room version:": "Room version:", @@ -1920,7 +1924,6 @@ "If you have permissions, open the menu on any message and select Pin to stick them here.": "If you have permissions, open the menu on any message and select Pin to stick them here.", "Pinned messages": "Pinned messages", "Chat": "Chat", - "Threads": "Threads", "Room Info": "Room Info", "You can only pin up to %(count)s widgets|other": "You can only pin up to %(count)s widgets", "Maximise widget": "Maximise widget", diff --git a/src/stores/right-panel/RightPanelStore.ts b/src/stores/right-panel/RightPanelStore.ts index 2aaee296d7..ac7d13898c 100644 --- a/src/stores/right-panel/RightPanelStore.ts +++ b/src/stores/right-panel/RightPanelStore.ts @@ -15,7 +15,6 @@ 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'; @@ -31,7 +30,6 @@ import { convertToStatePanel, convertToStorePanel, IRightPanelForRoom, - convertCardToStore, } from './RightPanelStoreIPanelState'; import { MatrixClientPeg } from "../../MatrixClientPeg"; // import RoomViewStore from '../RoomViewStore'; @@ -63,7 +61,7 @@ export default class RightPanelStore extends ReadyWatchingStore { private viewedRoomId: string; private isViewingRoom?: boolean; private dispatcherRefRightPanelStore: string; - private roomStoreToken: EventSubscription; + private isReady = false; private global?: IRightPanelForRoom = null; private byRoom: { @@ -76,6 +74,7 @@ export default class RightPanelStore extends ReadyWatchingStore { } protected async onReady(): Promise { + this.isReady = true; // 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); @@ -90,9 +89,7 @@ export default class RightPanelStore extends ReadyWatchingStore { } protected async onNotReady(): Promise { - if (this.roomStoreToken) { - this.roomStoreToken.remove(); - } + this.isReady = false; 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); @@ -140,8 +137,7 @@ export default class RightPanelStore extends ReadyWatchingStore { // 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 + // This function behaves as following: // 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); @@ -149,32 +145,29 @@ export default class RightPanelStore extends ReadyWatchingStore { const cardState = redirect?.state ?? (Object.keys(card.state ?? {}).length === 0 ? null : card.state); // Checks for wrong SetRightPanelPhase requests - if (!this.isPhaseActionIsValid(targetPhase)) return; + if (!this.isPhaseActionValid(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)) { + 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) { + } + + if (targetPhase !== this.currentCard?.phase) { // Set right panel and erase history. this.setRightPanelCache({ phase: targetPhase, state: cardState ?? {} }, rId); } } + public setCards(cards: IRightPanelCard[], allowClose = true, roomId: string = null) { + const rId = roomId ?? this.viewedRoomId; + const history = cards.map(c => ({ phase: c.phase, state: c.state ?? {} })); + this.byRoom[rId] = { history, isOpen: true }; + this.emitAndUpdateSettings(); + } + public pushCard( card: IRightPanelCard, allowClose = true, @@ -186,7 +179,7 @@ export default class RightPanelStore extends ReadyWatchingStore { const pState = redirect?.state ?? (Object.keys(card.state ?? {}).length === 0 ? null : card.state); // Checks for wrong SetRightPanelPhase requests - if (!this.isPhaseActionIsValid(targetPhase)) return; + if (!this.isPhaseActionValid(targetPhase)) return; let roomCache = this.byRoom[rId]; if (!!roomCache) { @@ -236,10 +229,6 @@ export default class RightPanelStore extends ReadyWatchingStore { } } - 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); @@ -257,10 +246,8 @@ export default class RightPanelStore extends ReadyWatchingStore { } private setRightPanelCache(card: IRightPanelCard, roomId?: string) { - this.byRoom[roomId ?? this.viewedRoomId] = { - history: [{ phase: card.phase, state: card.state ?? {} }], - isOpen: true, - }; + const history = [{ phase: card.phase, state: card.state ?? {} }]; + this.byRoom[roomId ?? this.viewedRoomId] = { history, isOpen: true }; this.emitAndUpdateSettings(); } @@ -282,7 +269,7 @@ export default class RightPanelStore extends ReadyWatchingStore { return null; } - private isPhaseActionIsValid(targetPhase) { + private isPhaseActionValid(targetPhase) { if (!RightPanelPhases[targetPhase]) { logger.warn(`Tried to switch right panel to unknown phase: ${targetPhase}`); return false; @@ -305,6 +292,7 @@ export default class RightPanelStore extends ReadyWatchingStore { private onVerificationRequestUpdate = () => { const { member } = this.currentCard.state; + if (!member) return; const pendingRequest = pendingVerificationRequestForUser(member); if (pendingRequest) { this.currentCard.state.verificationRequest = pendingRequest; @@ -312,13 +300,13 @@ export default class RightPanelStore extends ReadyWatchingStore { } }; - onRoomViewStoreUpdate() { + 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) { @@ -347,9 +335,9 @@ export default class RightPanelStore extends ReadyWatchingStore { _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 + if (_this.isReady) { + // we need the client to be ready to get the events form the ids of the settings + // the loading will be done in the onReady function (to catch up with the changes done here before it was ready) // all the logic in this case is not necessary anymore as soon as groups are dropped and we use: onRoomViewStoreUpdate _this.loadCacheFromSettings(); _this.emitAndUpdateSettings(); diff --git a/src/stores/right-panel/RightPanelStoreIPanelState.ts b/src/stores/right-panel/RightPanelStoreIPanelState.ts index 93df4bbd94..34fecf018d 100644 --- a/src/stores/right-panel/RightPanelStoreIPanelState.ts +++ b/src/stores/right-panel/RightPanelStoreIPanelState.ts @@ -89,48 +89,43 @@ export function convertToStatePanel(storeRoom: IRightPanelForRoomStored, room: R } 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 state = panelState.state ?? {}; + const stateStored: IRightPanelCardStateStored = { + groupId: state.groupId, + groupRoomId: state.groupRoomId, + widgetId: state.widgetId, + spaceId: state.spaceId, + isInitialEventHighlighted: state.isInitialEventHighlighted, + threadHeadEventId: !!state?.threadHeadEvent?.getId() ? + panelState.state.threadHeadEvent.getId() : undefined, + memberInfoEventId: !!state?.memberInfoEvent?.getId() ? + panelState.state.memberInfoEvent.getId() : undefined, + initialEventId: !!state?.initialEvent?.getId() ? + panelState.state.initialEvent.getId() : undefined, + memberId: !!state?.member?.userId ? + panelState.state.member.userId : undefined, + }; - const storedCard = { state: panelStateThisRoomStored as IRightPanelCardStored, phase: panelState.phase }; - return storedCard as IRightPanelCardStored; + return { state: stateStored, phase: panelState.phase }; } 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; + const stateStored = panelStateStore.state ?? {}; + const state: IRightPanelCardState = { + groupId: stateStored.groupId, + groupRoomId: stateStored.groupRoomId, + widgetId: stateStored.widgetId, + spaceId: stateStored.spaceId, + isInitialEventHighlighted: stateStored.isInitialEventHighlighted, + threadHeadEvent: !!stateStored?.threadHeadEventId ? + room.findEventById(stateStored.threadHeadEventId) : undefined, + memberInfoEvent: !!stateStored?.memberInfoEventId ? + room.findEventById(stateStored.memberInfoEventId) : undefined, + initialEvent: !!stateStored?.initialEventId ? + room.findEventById(stateStored.initialEventId) : undefined, + member: !!stateStored?.memberId ? + room.getMember(stateStored.memberId) : undefined, + }; - return { state: panelStateThisRoom as IRightPanelCardState, phase: panelStateStore.phase } as IRightPanelCard; + return { state: state, phase: panelStateStore.phase }; } diff --git a/src/stores/right-panel/RightPanelStorePhases.ts b/src/stores/right-panel/RightPanelStorePhases.ts index 0b8d9c69ed..10e7b64918 100644 --- a/src/stores/right-panel/RightPanelStorePhases.ts +++ b/src/stores/right-panel/RightPanelStorePhases.ts @@ -14,6 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { _t } from "../../languageHandler"; + // These are in their own file because of circular imports being a problem. export enum RightPanelPhases { // Room stuff @@ -44,6 +46,17 @@ export enum RightPanelPhases { ThreadPanel = "ThreadPanel", } +export function backLabelForPhase(phase: RightPanelPhases) { + switch (phase) { + case RightPanelPhases.ThreadPanel: return _t("Threads"); + case RightPanelPhases.Timeline: return _t("Back to chat"); + case RightPanelPhases.RoomSummary: return _t("Room information"); + case RightPanelPhases.RoomMemberList: return _t("Room members"); + case RightPanelPhases.ThreadView: return _t("Back to thread"); + } + return null; +} + // These are the phases that are safe to persist (the ones that don't require additional // arguments). export const RIGHT_PANEL_PHASES_NO_ARGS = [ diff --git a/src/verification.ts b/src/verification.ts index 2652733975..80747e3b00 100644 --- a/src/verification.ts +++ b/src/verification.ts @@ -28,6 +28,7 @@ import UntrustedDeviceDialog from "./components/views/dialogs/UntrustedDeviceDia import { GroupMember, IDevice } from "./components/views/right_panel/UserInfo"; import ManualDeviceKeyVerificationDialog from "./components/views/dialogs/ManualDeviceKeyVerificationDialog"; import RightPanelStore from "./stores/right-panel/RightPanelStore"; +import { IRightPanelCardState } from "./stores/right-panel/RightPanelStoreIPanelState"; async function enable4SIfNeeded() { const cli = MatrixClientPeg.get(); @@ -66,10 +67,7 @@ export async function verifyDevice(user: User, device: IDevice) { device.deviceId, VerificationMethods.SAS, ); - RightPanelStore.instance.setCard({ - phase: RightPanelPhases.EncryptionPanel, - state: { member: user, verificationRequestPromise }, - }); + setRightPanel({ member: user, verificationRequestPromise }); } else if (action === "legacy") { Modal.createTrackedDialog("Legacy verify session", "legacy verify session", ManualDeviceKeyVerificationDialog, @@ -96,10 +94,7 @@ export async function legacyVerifyUser(user: User) { } } const verificationRequestPromise = cli.requestVerification(user.userId); - RightPanelStore.instance.setCard({ - phase: RightPanelPhases.EncryptionPanel, - state: { member: user, verificationRequestPromise }, - }); + setRightPanel({ member: user, verificationRequestPromise }); } export async function verifyUser(user: User) { @@ -112,10 +107,21 @@ export async function verifyUser(user: User) { return; } const existingRequest = pendingVerificationRequestForUser(user); - RightPanelStore.instance.setCard({ - phase: RightPanelPhases.EncryptionPanel, - state: { member: user, verificationRequest: existingRequest }, - }); + setRightPanel({ member: user, verificationRequest: existingRequest }); +} + +function setRightPanel(state: IRightPanelCardState) { + if (RightPanelStore.instance.roomPhaseHistory.some((card) => (card.phase == RightPanelPhases.RoomSummary))) { + RightPanelStore.instance.pushCard( + { phase: RightPanelPhases.EncryptionPanel, state }, + ); + } else { + RightPanelStore.instance.setCards([ + { phase: RightPanelPhases.RoomSummary }, + { phase: RightPanelPhases.RoomMemberInfo, state: { member: state.member } }, + { phase: RightPanelPhases.EncryptionPanel, state }, + ]); + } } export function pendingVerificationRequestForUser(user: User | RoomMember | GroupMember ) {