Add right panel chat timeline (#7112)
Co-authored-by: J. Ryan Stinnett <jryans@gmail.com>
This commit is contained in:
parent
f5f1f18007
commit
4cbed99de3
14 changed files with 243 additions and 17 deletions
|
@ -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";
|
||||
|
|
|
@ -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);
|
||||
|
|
36
res/css/views/right_panel/_TimelineCard.scss
Normal file
36
res/css/views/right_panel/_TimelineCard.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<IProps, IState> {
|
|||
panel = <PinnedMessagesCard room={this.props.room} onClose={this.onClose} />;
|
||||
}
|
||||
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:
|
||||
panel = <FilePanel roomId={roomId} resizeNotifier={this.props.resizeNotifier} onClose={this.onClose} />;
|
||||
break;
|
||||
|
|
|
@ -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<IRoomProps, IRoomState> {
|
|||
|
||||
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<SetRightPanelPhasePayload>({
|
||||
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<IRoomProps, IRoomState> {
|
|||
: 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<IRoomProps, IRoomState> {
|
|||
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<IRoomProps, IRoomState> {
|
|||
}
|
||||
|
||||
const showRightPanel = this.state.room && this.state.showRightPanel;
|
||||
|
||||
const rightPanel = showRightPanel
|
||||
? <RightPanel
|
||||
room={this.state.room}
|
||||
|
|
|
@ -41,6 +41,8 @@ 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 SettingsStore from '../../settings/SettingsStore';
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
|
@ -203,6 +205,18 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
|||
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 (
|
||||
<RoomContext.Provider value={{
|
||||
...this.context,
|
||||
|
@ -213,8 +227,8 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
|||
<BaseCard
|
||||
className="mx_ThreadView mx_ThreadPanel"
|
||||
onClose={this.props.onClose}
|
||||
previousPhase={RightPanelPhases.ThreadPanel}
|
||||
previousPhaseLabel={_t("All threads")}
|
||||
previousPhase={previousPhase}
|
||||
previousPhaseLabel={previousPhaseLabels[previousPhase]}
|
||||
withoutScrollContainer={true}
|
||||
header={this.renderThreadViewHeader()}
|
||||
>
|
||||
|
|
|
@ -476,10 +476,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
|||
};
|
||||
|
||||
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<IProps, IState> {
|
|||
this.setState<null>(updatedState, () => {
|
||||
this.messagePanel.current.updateTimelineMinHeight();
|
||||
if (callRMUpdated) {
|
||||
this.props.onReadMarkerUpdated();
|
||||
this.props.onReadMarkerUpdated?.();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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<IProps, IState>
|
|||
);
|
||||
const isThreadRootEvent = isThread && this.props.mxEvent?.getThread()?.rootEvent === this.props.mxEvent;
|
||||
|
||||
const isMainSplitTimelineShown = !WidgetLayoutStore.instance.hasMaximisedWidget(
|
||||
MatrixClientPeg.get().getRoom(mxEvent.getRoomId()),
|
||||
);
|
||||
const commonItemsList = (
|
||||
<IconizedContextMenuOptionList>
|
||||
{ isThreadRootEvent && <IconizedContextMenuOption
|
||||
{ (isThreadRootEvent && isMainSplitTimelineShown) && <IconizedContextMenuOption
|
||||
iconClassName="mx_MessageContextMenu_iconViewInRoom"
|
||||
label={_t("View in room")}
|
||||
onClick={this.viewInRoom}
|
||||
|
|
|
@ -24,6 +24,8 @@ import { copyPlaintext } from "../../../utils/strings";
|
|||
import { ChevronFace, ContextMenuTooltipButton } from "../../structures/ContextMenu";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import IconizedContextMenu, { IconizedContextMenuOption, IconizedContextMenuOptionList } from "./IconizedContextMenu";
|
||||
import { WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
|
||||
interface IProps {
|
||||
mxEvent: MatrixEvent;
|
||||
|
@ -80,6 +82,9 @@ const ThreadListContextMenu: React.FC<IProps> = ({ mxEvent, permalinkCreator, on
|
|||
}
|
||||
}, [optionsPosition, onMenuToggle]);
|
||||
|
||||
const isMainSplitTimelineShown = !WidgetLayoutStore.instance.hasMaximisedWidget(
|
||||
MatrixClientPeg.get().getRoom(mxEvent.getRoomId()),
|
||||
);
|
||||
return <React.Fragment>
|
||||
<ContextMenuTooltipButton
|
||||
className="mx_MessageActionBar_maskButton mx_MessageActionBar_optionsButton"
|
||||
|
@ -95,11 +100,12 @@ const ThreadListContextMenu: React.FC<IProps> = ({ mxEvent, permalinkCreator, on
|
|||
{...contextMenuBelow(optionsPosition)}
|
||||
>
|
||||
<IconizedContextMenuOptionList>
|
||||
<IconizedContextMenuOption
|
||||
onClick={(e) => viewInRoom(e)}
|
||||
label={_t("View in room")}
|
||||
iconClassName="mx_ThreadPanel_viewInRoom"
|
||||
/>
|
||||
{ isMainSplitTimelineShown &&
|
||||
<IconizedContextMenuOption
|
||||
onClick={(e) => viewInRoom(e)}
|
||||
label={_t("View in room")}
|
||||
iconClassName="mx_ThreadPanel_viewInRoom"
|
||||
/> }
|
||||
<IconizedContextMenuOption
|
||||
onClick={(e) => copyLinkToThread(e)}
|
||||
label={_t("Copy link to thread")}
|
||||
|
|
|
@ -402,9 +402,9 @@ export default class AppTile extends React.Component<IProps, IState> {
|
|||
|
||||
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);
|
||||
};
|
||||
|
||||
|
|
|
@ -67,6 +67,18 @@ const PinnedMessagesHeaderButton = ({ room, isHighlighted, onClick }) => {
|
|||
</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 {
|
||||
room?: Room;
|
||||
}
|
||||
|
@ -122,6 +134,9 @@ export default class RoomHeaderButtons extends HeaderButtons<IProps> {
|
|||
// 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<IProps> {
|
|||
isHighlighted={this.isPhase(RightPanelPhases.PinnedMessages)}
|
||||
onClick={this.onPinnedMessagesClicked}
|
||||
/>
|
||||
<TimelineCardHeaderButton
|
||||
room={this.props.room}
|
||||
isHighlighted={this.isPhase(RightPanelPhases.Timeline)}
|
||||
onClick={this.onTimelineCardClicked}
|
||||
/>
|
||||
{ SettingsStore.getValue("feature_thread") && <HeaderButton
|
||||
name="threadsButton"
|
||||
title={_t("Threads")}
|
||||
|
|
103
src/components/views/right_panel/TimelineCard.tsx
Normal file
103
src/components/views/right_panel/TimelineCard.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1886,6 +1886,7 @@
|
|||
"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.",
|
||||
"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",
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue