From 4cbed99de33734f516518ca7ea418b96ac1aaa14 Mon Sep 17 00:00:00 2001 From: Timo <16718859+toger5@users.noreply.github.com> Date: Mon, 29 Nov 2021 17:06:15 +0100 Subject: [PATCH] Add right panel chat timeline (#7112) Co-authored-by: J. Ryan Stinnett --- res/css/_components.scss | 1 + res/css/structures/_RightPanel.scss | 7 ++ res/css/views/right_panel/_TimelineCard.scss | 36 ++++++ src/components/structures/RightPanel.tsx | 9 +- src/components/structures/RoomView.tsx | 28 +++++ src/components/structures/ThreadView.tsx | 18 ++- src/components/structures/TimelinePanel.tsx | 7 +- .../context_menus/MessageContextMenu.tsx | 6 +- .../context_menus/ThreadListContextMenu.tsx | 16 ++- src/components/views/elements/AppTile.tsx | 6 +- .../views/right_panel/RoomHeaderButtons.tsx | 20 ++++ .../views/right_panel/TimelineCard.tsx | 103 ++++++++++++++++++ src/i18n/strings/en_EN.json | 1 + src/stores/RightPanelStorePhases.ts | 2 + 14 files changed, 243 insertions(+), 17 deletions(-) create mode 100644 res/css/views/right_panel/_TimelineCard.scss create mode 100644 src/components/views/right_panel/TimelineCard.tsx diff --git a/res/css/_components.scss b/res/css/_components.scss index 731e20217b..6029a26080 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -205,6 +205,7 @@ @import "./views/right_panel/_PinnedMessagesCard.scss"; @import "./views/right_panel/_RoomSummaryCard.scss"; @import "./views/right_panel/_ThreadPanel.scss"; +@import "./views/right_panel/_TimelineCard.scss"; @import "./views/right_panel/_UserInfo.scss"; @import "./views/right_panel/_VerificationPanel.scss"; @import "./views/right_panel/_WidgetCard.scss"; diff --git a/res/css/structures/_RightPanel.scss b/res/css/structures/_RightPanel.scss index b08be355a9..ac727dab39 100644 --- a/res/css/structures/_RightPanel.scss +++ b/res/css/structures/_RightPanel.scss @@ -144,6 +144,13 @@ $pulse-color: $alert; } } +.mx_RightPanel_timelineCardButton { + &::before { + mask-image: url('$(res)/img/element-icons/feedback.svg'); + mask-position: center; + } +} + @keyframes mx_RightPanel_indicator_pulse { 0% { transform: scale(0.95); diff --git a/res/css/views/right_panel/_TimelineCard.scss b/res/css/views/right_panel/_TimelineCard.scss new file mode 100644 index 0000000000..c6e6f9d51b --- /dev/null +++ b/res/css/views/right_panel/_TimelineCard.scss @@ -0,0 +1,36 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_TimelineCard { + .mx_TimelineCard__header { + margin-left: 6px; + + span:first-of-type { + font-weight: 600; + font-size: 15px; + line-height: 18px; + color: $secondary-content; + } + } + + .mx_BaseCard_header { + margin: 5px 0 9px 0; + .mx_BaseCard_close { + margin: 8px; + right: 0; + } + } +} diff --git a/src/components/structures/RightPanel.tsx b/src/components/structures/RightPanel.tsx index 4da9e7f084..6e17a19e0d 100644 --- a/src/components/structures/RightPanel.tsx +++ b/src/components/structures/RightPanel.tsx @@ -54,6 +54,7 @@ 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'; interface IProps { room?: Room; // if showing panels for a given room, this is set @@ -334,7 +335,13 @@ export default class RightPanel extends React.Component { panel = ; } break; - + case RightPanelPhases.Timeline: + if (!SettingsStore.getValue("feature_maximised_widgets")) break; + panel = ; + break; case RightPanelPhases.FilePanel: panel = ; break; diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 962a646e3f..e6780fce72 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -96,6 +96,8 @@ 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'; const DEBUG = false; let debuglog = function(msg: string) {}; @@ -327,7 +329,15 @@ export class RoomView extends React.Component { private onWidgetLayoutChange = () => { if (!this.state.room) return; + 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, + }); + } this.checkWidgets(this.state.room); + this.checkRightPanel(this.state.room); }; private checkWidgets = (room) => { @@ -345,6 +355,22 @@ 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), @@ -1007,6 +1033,7 @@ export class RoomView extends React.Component { this.updateE2EStatus(room); this.updatePermissions(room); this.checkWidgets(room); + this.checkRightPanel(room); this.setState({ liveTimeline: room.getLiveTimeline(), @@ -2102,6 +2129,7 @@ export class RoomView extends React.Component { } const showRightPanel = this.state.room && this.state.showRightPanel; + const rightPanel = showRightPanel ? { event_id: this.state.thread?.id, }; + let previousPhase = RightPanelStore.getSharedInstance().previousPhase; + if (!SettingsStore.getValue("feature_maximised_widgets")) { + 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/structures/TimelinePanel.tsx b/src/components/structures/TimelinePanel.tsx index 71d0775e41..c29247bd92 100644 --- a/src/components/structures/TimelinePanel.tsx +++ b/src/components/structures/TimelinePanel.tsx @@ -476,10 +476,7 @@ class TimelinePanel extends React.Component { }; private onMessageListScroll = e => { - if (this.props.onScroll) { - this.props.onScroll(e); - } - + this.props.onScroll?.(e); if (this.props.manageReadMarkers) { this.doManageReadMarkers(); } @@ -594,7 +591,7 @@ class TimelinePanel extends React.Component { this.setState(updatedState, () => { this.messagePanel.current.updateTimelineMinHeight(); if (callRMUpdated) { - this.props.onReadMarkerUpdated(); + this.props.onReadMarkerUpdated?.(); } }); }); diff --git a/src/components/views/context_menus/MessageContextMenu.tsx b/src/components/views/context_menus/MessageContextMenu.tsx index b3a507702e..ef0b8c09a8 100644 --- a/src/components/views/context_menus/MessageContextMenu.tsx +++ b/src/components/views/context_menus/MessageContextMenu.tsx @@ -40,6 +40,7 @@ import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks"; import { IPosition, ChevronFace } from '../../structures/ContextMenu'; import RoomContext, { TimelineRenderingType } from '../../../contexts/RoomContext'; import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload"; +import { WidgetLayoutStore } from '../../../stores/widgets/WidgetLayoutStore'; export function canCancel(eventStatus: EventStatus): boolean { return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT; @@ -404,9 +405,12 @@ export default class MessageContextMenu extends React.Component ); const isThreadRootEvent = isThread && this.props.mxEvent?.getThread()?.rootEvent === this.props.mxEvent; + const isMainSplitTimelineShown = !WidgetLayoutStore.instance.hasMaximisedWidget( + MatrixClientPeg.get().getRoom(mxEvent.getRoomId()), + ); const commonItemsList = ( - { isThreadRootEvent && = ({ mxEvent, permalinkCreator, on } }, [optionsPosition, onMenuToggle]); + const isMainSplitTimelineShown = !WidgetLayoutStore.instance.hasMaximisedWidget( + MatrixClientPeg.get().getRoom(mxEvent.getRoomId()), + ); return = ({ mxEvent, permalinkCreator, on {...contextMenuBelow(optionsPosition)} > - viewInRoom(e)} - label={_t("View in room")} - iconClassName="mx_ThreadPanel_viewInRoom" - /> + { isMainSplitTimelineShown && + viewInRoom(e)} + label={_t("View in room")} + iconClassName="mx_ThreadPanel_viewInRoom" + /> } copyLinkToThread(e)} label={_t("Copy link to thread")} diff --git a/src/components/views/elements/AppTile.tsx b/src/components/views/elements/AppTile.tsx index a1b2933125..c341e9bec9 100644 --- a/src/components/views/elements/AppTile.tsx +++ b/src/components/views/elements/AppTile.tsx @@ -402,9 +402,9 @@ export default class AppTile extends React.Component { private onMaxMinWidgetClick = (): void => { const targetContainer = - WidgetLayoutStore.instance.isInContainer(this.props.room, this.props.app, Container.Center) - ? Container.Right - : Container.Center; + WidgetLayoutStore.instance.isInContainer(this.props.room, this.props.app, Container.Center) + ? Container.Right + : Container.Center; WidgetLayoutStore.instance.moveToContainer(this.props.room, this.props.app, targetContainer); }; diff --git a/src/components/views/right_panel/RoomHeaderButtons.tsx b/src/components/views/right_panel/RoomHeaderButtons.tsx index 3261cacc81..5405689fb3 100644 --- a/src/components/views/right_panel/RoomHeaderButtons.tsx +++ b/src/components/views/right_panel/RoomHeaderButtons.tsx @@ -67,6 +67,18 @@ const PinnedMessagesHeaderButton = ({ room, isHighlighted, onClick }) => { ; }; +const TimelineCardHeaderButton = ({ room, isHighlighted, onClick }) => { + if (!SettingsStore.getValue("feature_maximised_widgets")) return null; + + return ; +}; + interface IProps { room?: Room; } @@ -122,6 +134,9 @@ export default class RoomHeaderButtons extends HeaderButtons { // This toggles for us, if needed this.setPhase(RightPanelPhases.PinnedMessages); }; + private onTimelineCardClicked = () => { + this.setPhase(RightPanelPhases.Timeline); + }; private onThreadsPanelClicked = () => { if (RoomHeaderButtons.THREAD_PHASES.includes(this.state.phase)) { @@ -141,6 +156,11 @@ export default class RoomHeaderButtons extends HeaderButtons { isHighlighted={this.isPhase(RightPanelPhases.PinnedMessages)} onClick={this.onPinnedMessagesClicked} /> + { SettingsStore.getValue("feature_thread") && void; + resizeNotifier: ResizeNotifier; + permalinkCreator?: RoomPermalinkCreator; + e2eStatus?: E2EStatus; + initialEvent?: MatrixEvent; + initialEventHighlighted?: boolean; +} +interface IState { + thread?: Thread; + editState?: EditorStateTransfer; + replyToEvent?: MatrixEvent; +} + +@replaceableComponent("structures.TimelineCard") +export default class TimelineCard extends React.Component { + static contextType = RoomContext; + + constructor(props: IProps) { + super(props); + this.state = {}; + } + + private renderTimelineCardHeader = (): JSX.Element => { + return
+ { _t("Chat") } +
; + }; + + public render(): JSX.Element { + return ( + + + ); + } +} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index c12cd4161a..facd3999dd 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1886,6 +1886,7 @@ "Nothing pinned, yet": "Nothing pinned, yet", "If you have permissions, open the menu on any message and select Pin to stick them here.": "If you have permissions, open the menu on any message and select Pin to stick them here.", "Pinned messages": "Pinned messages", + "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", diff --git a/src/stores/RightPanelStorePhases.ts b/src/stores/RightPanelStorePhases.ts index 96a585b676..0b8d9c69ed 100644 --- a/src/stores/RightPanelStorePhases.ts +++ b/src/stores/RightPanelStorePhases.ts @@ -25,6 +25,7 @@ export enum RightPanelPhases { RoomSummary = 'RoomSummary', Widget = 'Widget', PinnedMessages = "PinnedMessages", + Timeline = "Timeline", Room3pidMemberInfo = 'Room3pidMemberInfo', // Group stuff @@ -53,6 +54,7 @@ export const RIGHT_PANEL_PHASES_NO_ARGS = [ RightPanelPhases.RoomMemberList, RightPanelPhases.GroupMemberList, RightPanelPhases.GroupRoomList, + RightPanelPhases.Timeline, ]; // Subset of phases visible in the Space View