Add right panel chat timeline (#7112)

Co-authored-by: J. Ryan Stinnett <jryans@gmail.com>
This commit is contained in:
Timo 2021-11-29 17:06:15 +01:00 committed by GitHub
parent f5f1f18007
commit 4cbed99de3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 243 additions and 17 deletions

View file

@ -205,6 +205,7 @@
@import "./views/right_panel/_PinnedMessagesCard.scss"; @import "./views/right_panel/_PinnedMessagesCard.scss";
@import "./views/right_panel/_RoomSummaryCard.scss"; @import "./views/right_panel/_RoomSummaryCard.scss";
@import "./views/right_panel/_ThreadPanel.scss"; @import "./views/right_panel/_ThreadPanel.scss";
@import "./views/right_panel/_TimelineCard.scss";
@import "./views/right_panel/_UserInfo.scss"; @import "./views/right_panel/_UserInfo.scss";
@import "./views/right_panel/_VerificationPanel.scss"; @import "./views/right_panel/_VerificationPanel.scss";
@import "./views/right_panel/_WidgetCard.scss"; @import "./views/right_panel/_WidgetCard.scss";

View file

@ -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 { @keyframes mx_RightPanel_indicator_pulse {
0% { 0% {
transform: scale(0.95); transform: scale(0.95);

View file

@ -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;
}
}
}

View file

@ -54,6 +54,7 @@ import SpaceStore from "../../stores/spaces/SpaceStore";
import { RoomPermalinkCreator } from '../../utils/permalinks/Permalinks'; import { RoomPermalinkCreator } from '../../utils/permalinks/Permalinks';
import { E2EStatus } from '../../utils/ShieldUtils'; import { E2EStatus } from '../../utils/ShieldUtils';
import { dispatchShowThreadsPanelEvent } from '../../dispatcher/dispatch-actions/threads'; import { dispatchShowThreadsPanelEvent } from '../../dispatcher/dispatch-actions/threads';
import TimelineCard from '../views/right_panel/TimelineCard';
interface IProps { interface IProps {
room?: Room; // if showing panels for a given room, this is set room?: Room; // if showing panels for a given room, this is set
@ -334,7 +335,13 @@ export default class RightPanel extends React.Component<IProps, IState> {
panel = <PinnedMessagesCard room={this.props.room} onClose={this.onClose} />; panel = <PinnedMessagesCard room={this.props.room} onClose={this.onClose} />;
} }
break; break;
case RightPanelPhases.Timeline:
if (!SettingsStore.getValue("feature_maximised_widgets")) break;
panel = <TimelineCard
room={this.props.room}
resizeNotifier={this.props.resizeNotifier}
onClose={this.onClose} />;
break;
case RightPanelPhases.FilePanel: case RightPanelPhases.FilePanel:
panel = <FilePanel roomId={roomId} resizeNotifier={this.props.resizeNotifier} onClose={this.onClose} />; panel = <FilePanel roomId={roomId} resizeNotifier={this.props.resizeNotifier} onClose={this.onClose} />;
break; break;

View file

@ -96,6 +96,8 @@ import { dispatchShowThreadEvent } from '../../dispatcher/dispatch-actions/threa
import { fetchInitialEvent } from "../../utils/EventUtils"; import { fetchInitialEvent } from "../../utils/EventUtils";
import { ComposerType } from "../../dispatcher/payloads/ComposerInsertPayload"; import { ComposerType } from "../../dispatcher/payloads/ComposerInsertPayload";
import AppsDrawer from '../views/rooms/AppsDrawer'; import AppsDrawer from '../views/rooms/AppsDrawer';
import { SetRightPanelPhasePayload } from '../../dispatcher/payloads/SetRightPanelPhasePayload';
import { RightPanelPhases } from '../../stores/RightPanelStorePhases';
const DEBUG = false; const DEBUG = false;
let debuglog = function(msg: string) {}; let debuglog = function(msg: string) {};
@ -327,7 +329,15 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
private onWidgetLayoutChange = () => { private onWidgetLayoutChange = () => {
if (!this.state.room) return; if (!this.state.room) return;
if (WidgetLayoutStore.instance.hasMaximisedWidget(this.state.room)) {
// Show chat in right panel when a widget is maximised
dis.dispatch<SetRightPanelPhasePayload>({
action: Action.SetRightPanelPhase,
phase: RightPanelPhases.Timeline,
});
}
this.checkWidgets(this.state.room); this.checkWidgets(this.state.room);
this.checkRightPanel(this.state.room);
}; };
private checkWidgets = (room) => { private checkWidgets = (room) => {
@ -345,6 +355,22 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
: MainSplitContentType.Timeline; : 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 = () => { private onReadReceiptsChange = () => {
this.setState({ this.setState({
showReadReceipts: SettingsStore.getValue("showReadReceipts", this.state.roomId), showReadReceipts: SettingsStore.getValue("showReadReceipts", this.state.roomId),
@ -1007,6 +1033,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
this.updateE2EStatus(room); this.updateE2EStatus(room);
this.updatePermissions(room); this.updatePermissions(room);
this.checkWidgets(room); this.checkWidgets(room);
this.checkRightPanel(room);
this.setState({ this.setState({
liveTimeline: room.getLiveTimeline(), liveTimeline: room.getLiveTimeline(),
@ -2102,6 +2129,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
} }
const showRightPanel = this.state.room && this.state.showRightPanel; const showRightPanel = this.state.room && this.state.showRightPanel;
const rightPanel = showRightPanel const rightPanel = showRightPanel
? <RightPanel ? <RightPanel
room={this.state.room} room={this.state.room}

View file

@ -41,6 +41,8 @@ import ContentMessages from '../../ContentMessages';
import UploadBar from './UploadBar'; import UploadBar from './UploadBar';
import { _t } from '../../languageHandler'; import { _t } from '../../languageHandler';
import ThreadListContextMenu from '../views/context_menus/ThreadListContextMenu'; import ThreadListContextMenu from '../views/context_menus/ThreadListContextMenu';
import RightPanelStore from '../../stores/RightPanelStore';
import SettingsStore from '../../settings/SettingsStore';
interface IProps { interface IProps {
room: Room; room: Room;
@ -203,6 +205,18 @@ export default class ThreadView extends React.Component<IProps, IState> {
event_id: this.state.thread?.id, 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 ( return (
<RoomContext.Provider value={{ <RoomContext.Provider value={{
...this.context, ...this.context,
@ -213,8 +227,8 @@ export default class ThreadView extends React.Component<IProps, IState> {
<BaseCard <BaseCard
className="mx_ThreadView mx_ThreadPanel" className="mx_ThreadView mx_ThreadPanel"
onClose={this.props.onClose} onClose={this.props.onClose}
previousPhase={RightPanelPhases.ThreadPanel} previousPhase={previousPhase}
previousPhaseLabel={_t("All threads")} previousPhaseLabel={previousPhaseLabels[previousPhase]}
withoutScrollContainer={true} withoutScrollContainer={true}
header={this.renderThreadViewHeader()} header={this.renderThreadViewHeader()}
> >

View file

@ -476,10 +476,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
}; };
private onMessageListScroll = e => { private onMessageListScroll = e => {
if (this.props.onScroll) { this.props.onScroll?.(e);
this.props.onScroll(e);
}
if (this.props.manageReadMarkers) { if (this.props.manageReadMarkers) {
this.doManageReadMarkers(); this.doManageReadMarkers();
} }
@ -594,7 +591,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
this.setState<null>(updatedState, () => { this.setState<null>(updatedState, () => {
this.messagePanel.current.updateTimelineMinHeight(); this.messagePanel.current.updateTimelineMinHeight();
if (callRMUpdated) { if (callRMUpdated) {
this.props.onReadMarkerUpdated(); this.props.onReadMarkerUpdated?.();
} }
}); });
}); });

View file

@ -40,6 +40,7 @@ import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
import { IPosition, ChevronFace } from '../../structures/ContextMenu'; import { IPosition, ChevronFace } from '../../structures/ContextMenu';
import RoomContext, { TimelineRenderingType } from '../../../contexts/RoomContext'; import RoomContext, { TimelineRenderingType } from '../../../contexts/RoomContext';
import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload"; import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload";
import { WidgetLayoutStore } from '../../../stores/widgets/WidgetLayoutStore';
export function canCancel(eventStatus: EventStatus): boolean { export function canCancel(eventStatus: EventStatus): boolean {
return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT; return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT;
@ -404,9 +405,12 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
); );
const isThreadRootEvent = isThread && this.props.mxEvent?.getThread()?.rootEvent === this.props.mxEvent; const isThreadRootEvent = isThread && this.props.mxEvent?.getThread()?.rootEvent === this.props.mxEvent;
const isMainSplitTimelineShown = !WidgetLayoutStore.instance.hasMaximisedWidget(
MatrixClientPeg.get().getRoom(mxEvent.getRoomId()),
);
const commonItemsList = ( const commonItemsList = (
<IconizedContextMenuOptionList> <IconizedContextMenuOptionList>
{ isThreadRootEvent && <IconizedContextMenuOption { (isThreadRootEvent && isMainSplitTimelineShown) && <IconizedContextMenuOption
iconClassName="mx_MessageContextMenu_iconViewInRoom" iconClassName="mx_MessageContextMenu_iconViewInRoom"
label={_t("View in room")} label={_t("View in room")}
onClick={this.viewInRoom} onClick={this.viewInRoom}

View file

@ -24,6 +24,8 @@ import { copyPlaintext } from "../../../utils/strings";
import { ChevronFace, ContextMenuTooltipButton } from "../../structures/ContextMenu"; import { ChevronFace, ContextMenuTooltipButton } from "../../structures/ContextMenu";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import IconizedContextMenu, { IconizedContextMenuOption, IconizedContextMenuOptionList } from "./IconizedContextMenu"; import IconizedContextMenu, { IconizedContextMenuOption, IconizedContextMenuOptionList } from "./IconizedContextMenu";
import { WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
interface IProps { interface IProps {
mxEvent: MatrixEvent; mxEvent: MatrixEvent;
@ -80,6 +82,9 @@ const ThreadListContextMenu: React.FC<IProps> = ({ mxEvent, permalinkCreator, on
} }
}, [optionsPosition, onMenuToggle]); }, [optionsPosition, onMenuToggle]);
const isMainSplitTimelineShown = !WidgetLayoutStore.instance.hasMaximisedWidget(
MatrixClientPeg.get().getRoom(mxEvent.getRoomId()),
);
return <React.Fragment> return <React.Fragment>
<ContextMenuTooltipButton <ContextMenuTooltipButton
className="mx_MessageActionBar_maskButton mx_MessageActionBar_optionsButton" className="mx_MessageActionBar_maskButton mx_MessageActionBar_optionsButton"
@ -95,11 +100,12 @@ const ThreadListContextMenu: React.FC<IProps> = ({ mxEvent, permalinkCreator, on
{...contextMenuBelow(optionsPosition)} {...contextMenuBelow(optionsPosition)}
> >
<IconizedContextMenuOptionList> <IconizedContextMenuOptionList>
<IconizedContextMenuOption { isMainSplitTimelineShown &&
onClick={(e) => viewInRoom(e)} <IconizedContextMenuOption
label={_t("View in room")} onClick={(e) => viewInRoom(e)}
iconClassName="mx_ThreadPanel_viewInRoom" label={_t("View in room")}
/> iconClassName="mx_ThreadPanel_viewInRoom"
/> }
<IconizedContextMenuOption <IconizedContextMenuOption
onClick={(e) => copyLinkToThread(e)} onClick={(e) => copyLinkToThread(e)}
label={_t("Copy link to thread")} label={_t("Copy link to thread")}

View file

@ -402,9 +402,9 @@ export default class AppTile extends React.Component<IProps, IState> {
private onMaxMinWidgetClick = (): void => { private onMaxMinWidgetClick = (): void => {
const targetContainer = const targetContainer =
WidgetLayoutStore.instance.isInContainer(this.props.room, this.props.app, Container.Center) WidgetLayoutStore.instance.isInContainer(this.props.room, this.props.app, Container.Center)
? Container.Right ? Container.Right
: Container.Center; : Container.Center;
WidgetLayoutStore.instance.moveToContainer(this.props.room, this.props.app, targetContainer); WidgetLayoutStore.instance.moveToContainer(this.props.room, this.props.app, targetContainer);
}; };

View file

@ -67,6 +67,18 @@ const PinnedMessagesHeaderButton = ({ room, isHighlighted, onClick }) => {
</HeaderButton>; </HeaderButton>;
}; };
const TimelineCardHeaderButton = ({ room, isHighlighted, onClick }) => {
if (!SettingsStore.getValue("feature_maximised_widgets")) return null;
return <HeaderButton
name="timelineCardButton"
title={_t("Chat")}
isHighlighted={isHighlighted}
onClick={onClick}
analytics={["Right Panel", "Timeline Panel Button", "click"]}
/>;
};
interface IProps { interface IProps {
room?: Room; room?: Room;
} }
@ -122,6 +134,9 @@ export default class RoomHeaderButtons extends HeaderButtons<IProps> {
// This toggles for us, if needed // This toggles for us, if needed
this.setPhase(RightPanelPhases.PinnedMessages); this.setPhase(RightPanelPhases.PinnedMessages);
}; };
private onTimelineCardClicked = () => {
this.setPhase(RightPanelPhases.Timeline);
};
private onThreadsPanelClicked = () => { private onThreadsPanelClicked = () => {
if (RoomHeaderButtons.THREAD_PHASES.includes(this.state.phase)) { if (RoomHeaderButtons.THREAD_PHASES.includes(this.state.phase)) {
@ -141,6 +156,11 @@ export default class RoomHeaderButtons extends HeaderButtons<IProps> {
isHighlighted={this.isPhase(RightPanelPhases.PinnedMessages)} isHighlighted={this.isPhase(RightPanelPhases.PinnedMessages)}
onClick={this.onPinnedMessagesClicked} onClick={this.onPinnedMessagesClicked}
/> />
<TimelineCardHeaderButton
room={this.props.room}
isHighlighted={this.isPhase(RightPanelPhases.Timeline)}
onClick={this.onTimelineCardClicked}
/>
{ SettingsStore.getValue("feature_thread") && <HeaderButton { SettingsStore.getValue("feature_thread") && <HeaderButton
name="threadsButton" name="threadsButton"
title={_t("Threads")} title={_t("Threads")}

View file

@ -0,0 +1,103 @@
/*
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 React from 'react';
import { MatrixEvent, Room } from 'matrix-js-sdk/src';
import { Thread } from 'matrix-js-sdk/src/models/thread';
import BaseCard from "./BaseCard";
import ResizeNotifier from '../../../utils/ResizeNotifier';
import MessageComposer from '../rooms/MessageComposer';
import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks';
import { Layout } from '../../../settings/enums/Layout';
import TimelinePanel from '../../structures/TimelinePanel';
import { E2EStatus } from '../../../utils/ShieldUtils';
import EditorStateTransfer from '../../../utils/EditorStateTransfer';
import RoomContext from '../../../contexts/RoomContext';
import { _t } from '../../../languageHandler';
import { replaceableComponent } from '../../../utils/replaceableComponent';
interface IProps {
room: Room;
onClose: () => 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<IProps, IState> {
static contextType = RoomContext;
constructor(props: IProps) {
super(props);
this.state = {};
}
private renderTimelineCardHeader = (): JSX.Element => {
return <div className="mx_TimelineCard__header">
<span>{ _t("Chat") }</span>
</div>;
};
public render(): JSX.Element {
return (
<BaseCard
className="mx_ThreadPanel mx_TimelineCard"
onClose={this.props.onClose}
withoutScrollContainer={true}
header={this.renderTimelineCardHeader()}
>
<TimelinePanel
showReadReceipts={false} // TODO: RR's cause issues with limited horizontal space
manageReadReceipts={true}
manageReadMarkers={false} // No RM support in the TimelineCard
sendReadReceiptOnLoad={true}
timelineSet={this.props.room.getUnfilteredTimelineSet()}
showUrlPreview={true}
layout={Layout.Group}
hideThreadedMessages={false}
hidden={false}
showReactions={true}
className="mx_RoomView_messagePanel mx_GroupLayout"
permalinkCreator={this.props.permalinkCreator}
membersLoaded={true}
editState={this.state.editState}
eventId={this.props.initialEvent?.getId()}
resizeNotifier={this.props.resizeNotifier}
/>
<MessageComposer
room={this.props.room}
resizeNotifier={this.props.resizeNotifier}
replyToEvent={this.state.replyToEvent}
permalinkCreator={this.props.permalinkCreator}
e2eStatus={this.props.e2eStatus}
compact={true}
/>
</BaseCard>
);
}
}

View file

@ -1886,6 +1886,7 @@
"Nothing pinned, yet": "Nothing pinned, yet", "Nothing pinned, yet": "Nothing pinned, yet",
"If you have permissions, open the menu on any message and select <b>Pin</b> to stick them here.": "If you have permissions, open the menu on any message and select <b>Pin</b> to stick them here.", "If you have permissions, open the menu on any message and select <b>Pin</b> to stick them here.": "If you have permissions, open the menu on any message and select <b>Pin</b> to stick them here.",
"Pinned messages": "Pinned messages", "Pinned messages": "Pinned messages",
"Chat": "Chat",
"Threads": "Threads", "Threads": "Threads",
"Room Info": "Room Info", "Room Info": "Room Info",
"You can only pin up to %(count)s widgets|other": "You can only pin up to %(count)s widgets", "You can only pin up to %(count)s widgets|other": "You can only pin up to %(count)s widgets",

View file

@ -25,6 +25,7 @@ export enum RightPanelPhases {
RoomSummary = 'RoomSummary', RoomSummary = 'RoomSummary',
Widget = 'Widget', Widget = 'Widget',
PinnedMessages = "PinnedMessages", PinnedMessages = "PinnedMessages",
Timeline = "Timeline",
Room3pidMemberInfo = 'Room3pidMemberInfo', Room3pidMemberInfo = 'Room3pidMemberInfo',
// Group stuff // Group stuff
@ -53,6 +54,7 @@ export const RIGHT_PANEL_PHASES_NO_ARGS = [
RightPanelPhases.RoomMemberList, RightPanelPhases.RoomMemberList,
RightPanelPhases.GroupMemberList, RightPanelPhases.GroupMemberList,
RightPanelPhases.GroupRoomList, RightPanelPhases.GroupRoomList,
RightPanelPhases.Timeline,
]; ];
// Subset of phases visible in the Space View // Subset of phases visible in the Space View