Fix thread summary layout for narrow right panel timeline (#7838)

This commit is contained in:
J. Ryan Stinnett 2022-02-23 14:03:46 +00:00 committed by GitHub
parent 5e76d988ca
commit d8ac7cf202
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 248 additions and 68 deletions

View file

@ -1,6 +1,6 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2019 - 2022 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.
@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React, { createRef } from 'react';
import { Filter } from 'matrix-js-sdk/src/filter';
import { EventTimelineSet, IRoomTimelineData } from "matrix-js-sdk/src/models/event-timeline-set";
import { Direction } from "matrix-js-sdk/src/models/event-timeline";
@ -35,6 +35,7 @@ import TimelinePanel from "./TimelinePanel";
import Spinner from "../views/elements/Spinner";
import { Layout } from "../../settings/enums/Layout";
import RoomContext, { TimelineRenderingType } from '../../contexts/RoomContext';
import Measured from '../views/elements/Measured';
interface IProps {
roomId: string;
@ -44,6 +45,7 @@ interface IProps {
interface IState {
timelineSet: EventTimelineSet;
narrow: boolean;
}
/*
@ -51,14 +53,17 @@ interface IState {
*/
@replaceableComponent("structures.FilePanel")
class FilePanel extends React.Component<IProps, IState> {
static contextType = RoomContext;
// This is used to track if a decrypted event was a live event and should be
// added to the timeline.
private decryptingEvents = new Set<string>();
public noRoom: boolean;
static contextType = RoomContext;
private card = createRef<HTMLDivElement>();
state = {
timelineSet: null,
narrow: false,
};
private onRoomTimeline = (
@ -184,6 +189,10 @@ class FilePanel extends React.Component<IProps, IState> {
}
};
private onMeasurement = (narrow: boolean): void => {
this.setState({ narrow });
};
public async updateTimelineSet(roomId: string): Promise<void> {
const client = MatrixClientPeg.get();
const room = client.getRoom(roomId);
@ -256,12 +265,18 @@ class FilePanel extends React.Component<IProps, IState> {
<RoomContext.Provider value={{
...this.context,
timelineRenderingType: TimelineRenderingType.File,
narrow: this.state.narrow,
}}>
<BaseCard
className="mx_FilePanel"
onClose={this.props.onClose}
withoutScrollContainer
ref={this.card}
>
<Measured
sensor={this.card.current}
onMeasurement={this.onMeasurement}
/>
<DesktopBuildsNotice isRoomEncrypted={isRoomEncrypted} kind={WarningKind.Files} />
<TimelinePanel
manageReadReceipts={false}

View file

@ -1,5 +1,5 @@
/*
Copyright 2016 - 2021 The Matrix.org Foundation C.I.C.
Copyright 2016 - 2022 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.
@ -16,6 +16,7 @@ limitations under the License.
import React, { createRef, KeyboardEvent, ReactNode, SyntheticEvent, TransitionEvent } from 'react';
import ReactDOM from 'react-dom';
import classNames from 'classnames';
import { Room } from 'matrix-js-sdk/src/models/room';
import { EventType } from 'matrix-js-sdk/src/@types/event';
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
@ -1018,11 +1019,15 @@ export default class MessagePanel extends React.Component<IProps, IState> {
/>;
}
const classes = classNames(this.props.className, {
"mx_MessagePanel_narrow": this.context.narrow,
});
return (
<ErrorBoundary>
<ScrollPanel
ref={this.scrollPanel}
className={this.props.className}
className={classes}
onScroll={this.props.onScroll}
onUserScroll={this.props.onUserScroll}
onFillRequest={this.props.onFillRequest}

View file

@ -1,5 +1,5 @@
/*
Copyright 2016, 2019, 2021 The Matrix.org Foundation C.I.C.
Copyright 2016 - 2022 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.
@ -25,17 +25,37 @@ import TimelinePanel from "./TimelinePanel";
import Spinner from "../views/elements/Spinner";
import { Layout } from "../../settings/enums/Layout";
import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext";
import Measured from "../views/elements/Measured";
interface IProps {
onClose(): void;
}
interface IState {
narrow: boolean;
}
/*
* Component which shows the global notification list using a TimelinePanel
*/
@replaceableComponent("structures.NotificationPanel")
export default class NotificationPanel extends React.PureComponent<IProps> {
export default class NotificationPanel extends React.PureComponent<IProps, IState> {
static contextType = RoomContext;
private card = React.createRef<HTMLDivElement>();
constructor(props) {
super(props);
this.state = {
narrow: false,
};
}
private onMeasurement = (narrow: boolean): void => {
this.setState({ narrow });
};
render() {
const emptyState = (<div className="mx_RightPanel_empty mx_NotificationPanel_empty">
<h2>{ _t("You're all caught up") }</h2>
@ -65,8 +85,13 @@ export default class NotificationPanel extends React.PureComponent<IProps> {
return <RoomContext.Provider value={{
...this.context,
timelineRenderingType: TimelineRenderingType.Notification,
narrow: this.state.narrow,
}}>
<BaseCard className="mx_NotificationPanel" onClose={this.props.onClose} withoutScrollContainer>
<Measured
sensor={this.card.current}
onMeasurement={this.onMeasurement}
/>
{ content }
</BaseCard>
</RoomContext.Provider>;

View file

@ -2,7 +2,7 @@
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2018, 2019 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2019 - 2022 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.
@ -104,6 +104,7 @@ import { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload";
import { JoinRoomPayload } from "../../dispatcher/payloads/JoinRoomPayload";
import { DoAfterSyncPreparedPayload } from '../../dispatcher/payloads/DoAfterSyncPreparedPayload';
import FileDropTarget from './FileDropTarget';
import Measured from '../views/elements/Measured';
const DEBUG = false;
let debuglog = function(msg: string) {};
@ -211,6 +212,7 @@ export interface IRoomState {
timelineRenderingType: TimelineRenderingType;
threadId?: string;
liveTimeline?: EventTimeline;
narrow: boolean;
}
@replaceableComponent("structures.RoomView")
@ -226,6 +228,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
private roomView = createRef<HTMLElement>();
private searchResultsPanel = createRef<ScrollPanel>();
private messagePanel: TimelinePanel;
private roomViewBody = createRef<HTMLDivElement>();
static contextType = MatrixClientContext;
@ -271,6 +274,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
mainSplitContentType: MainSplitContentType.Timeline,
timelineRenderingType: TimelineRenderingType.Room,
liveTimeline: undefined,
narrow: false,
};
this.dispatcherRef = dis.register(this.onAction);
@ -1730,6 +1734,10 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
TimelineRenderingType.Room,
);
private onMeasurement = (narrow: boolean): void => {
this.setState({ narrow });
};
render() {
if (!this.state.room) {
const loading = !this.state.matrixClientIsReady || this.state.roomLoading || this.state.peekLoading;
@ -2084,6 +2092,10 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
// Decide what to show in the main split
let mainSplitBody = <React.Fragment>
<Measured
sensor={this.roomViewBody.current}
onMeasurement={this.onMeasurement}
/>
{ auxPanel }
<div className={timelineClasses}>
<FileDropTarget parent={this.roomView.current} onFileDrop={this.onFileDrop} />
@ -2148,7 +2160,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
excludedRightPanelPhaseButtons={excludedRightPanelPhaseButtons}
/>
<MainSplit panel={rightPanel} resizeNotifier={this.props.resizeNotifier}>
<div className="mx_RoomView_body" data-layout={this.state.layout}>
<div className="mx_RoomView_body" ref={this.roomViewBody} data-layout={this.state.layout}>
{ mainSplitBody }
</div>
</MainSplit>

View file

@ -1,5 +1,5 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Copyright 2021 - 2022 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.
@ -37,6 +37,7 @@ import RoomContext, { TimelineRenderingType } from '../../contexts/RoomContext';
import TimelinePanel from './TimelinePanel';
import { Layout } from '../../settings/enums/Layout';
import { RoomPermalinkCreator } from '../../utils/permalinks/Permalinks';
import Measured from '../views/elements/Measured';
async function getThreadTimelineSet(
client: MatrixClient,
@ -213,12 +214,14 @@ const EmptyThread: React.FC<EmptyThreadIProps> = ({ filterOption, showAllThreads
const ThreadPanel: React.FC<IProps> = ({ roomId, onClose, permalinkCreator }) => {
const mxClient = useContext(MatrixClientContext);
const roomContext = useContext(RoomContext);
const [filterOption, setFilterOption] = useState<ThreadFilterType>(ThreadFilterType.All);
const ref = useRef<TimelinePanel>();
const timelinePanel = useRef<TimelinePanel>();
const card = useRef<HTMLDivElement>();
const [filterOption, setFilterOption] = useState<ThreadFilterType>(ThreadFilterType.All);
const [room, setRoom] = useState(mxClient.getRoom(roomId));
const [threadCount, setThreadCount] = useState<number>(0);
const [timelineSet, setTimelineSet] = useState<EventTimelineSet | null>(null);
const [narrow, setNarrow] = useState<boolean>(false);
useEffect(() => {
setRoom(mxClient.getRoom(roomId));
@ -257,7 +260,7 @@ const ThreadPanel: React.FC<IProps> = ({ roomId, onClose, permalinkCreator }) =>
}
function refreshTimeline() {
if (timelineSet) ref.current.refreshTimeline();
if (timelineSet) timelinePanel.current.refreshTimeline();
}
setThreadCount(room.threads.size);
@ -278,14 +281,15 @@ const ThreadPanel: React.FC<IProps> = ({ roomId, onClose, permalinkCreator }) =>
}, [mxClient, room, filterOption]);
useEffect(() => {
if (timelineSet) ref.current.refreshTimeline();
}, [timelineSet, ref]);
if (timelineSet) timelinePanel.current.refreshTimeline();
}, [timelineSet, timelinePanel]);
return (
<RoomContext.Provider value={{
...roomContext,
timelineRenderingType: TimelineRenderingType.ThreadsList,
showHiddenEventsInTimeline: true,
narrow,
}}>
<BaseCard
header={<ThreadPanelHeader
@ -296,10 +300,15 @@ const ThreadPanel: React.FC<IProps> = ({ roomId, onClose, permalinkCreator }) =>
className="mx_ThreadPanel"
onClose={onClose}
withoutScrollContainer={true}
ref={card}
>
<Measured
sensor={card.current}
onMeasurement={setNarrow}
/>
{ timelineSet && (
<TimelinePanel
ref={ref}
ref={timelinePanel}
showReadReceipts={false} // No RR support in thread's MVP
manageReadReceipts={false} // No RR support in thread's MVP
manageReadMarkers={false} // No RM support in thread's MVP

View file

@ -1,5 +1,5 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Copyright 2021 - 2022 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.
@ -49,6 +49,7 @@ import { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload";
import FileDropTarget from "./FileDropTarget";
import { getKeyBindingsManager } from "../../KeyBindingsManager";
import { KeyBindingAction } from "../../accessibility/KeyboardShortcuts";
import Measured from '../views/elements/Measured';
interface IProps {
room: Room;
@ -60,12 +61,14 @@ interface IProps {
initialEvent?: MatrixEvent;
isInitialEventHighlighted?: boolean;
}
interface IState {
thread?: Thread;
lastThreadReply?: MatrixEvent;
layout: Layout;
editState?: EditorStateTransfer;
replyToEvent?: MatrixEvent;
narrow: boolean;
}
@replaceableComponent("structures.ThreadView")
@ -74,14 +77,16 @@ export default class ThreadView extends React.Component<IProps, IState> {
public context!: React.ContextType<typeof RoomContext>;
private dispatcherRef: string;
private timelinePanelRef = createRef<TimelinePanel>();
private cardRef = createRef<HTMLDivElement>();
private readonly layoutWatcherRef: string;
private timelinePanel = createRef<TimelinePanel>();
private card = createRef<HTMLDivElement>();
constructor(props: IProps) {
super(props);
this.state = {
layout: SettingsStore.getValue("layout"),
narrow: false,
};
this.layoutWatcherRef = SettingsStore.watchSetting("layout", null, (...[,,, value]) =>
@ -131,7 +136,7 @@ export default class ThreadView extends React.Component<IProps, IState> {
editState: payload.event ? new EditorStateTransfer(payload.event) : null,
}, () => {
if (payload.event) {
this.timelinePanelRef.current?.scrollToEventIfNeeded(payload.event.getId());
this.timelinePanel.current?.scrollToEventIfNeeded(payload.event.getId());
}
});
break;
@ -181,7 +186,7 @@ export default class ThreadView extends React.Component<IProps, IState> {
if (!thread.initialEventsFetched) {
await thread.fetchInitialEvents();
}
this.timelinePanelRef.current?.refreshTimeline();
this.timelinePanel.current?.refreshTimeline();
});
}
};
@ -209,6 +214,10 @@ export default class ThreadView extends React.Component<IProps, IState> {
}
};
private onMeasurement = (narrow: boolean): void => {
this.setState({ narrow });
};
private onKeyDown = (ev: KeyboardEvent) => {
let handled = false;
@ -230,15 +239,6 @@ export default class ThreadView extends React.Component<IProps, IState> {
}
};
private renderThreadViewHeader = (): JSX.Element => {
return <div className="mx_ThreadPanel__header">
<span>{ _t("Thread") }</span>
<ThreadListContextMenu
mxEvent={this.props.mxEvent}
permalinkCreator={this.props.permalinkCreator} />
</div>;
};
private onPaginationRequest = async (
timelineWindow: TimelineWindow | null,
direction = Direction.Backward,
@ -284,6 +284,15 @@ export default class ThreadView extends React.Component<IProps, IState> {
};
}
private renderThreadViewHeader = (): JSX.Element => {
return <div className="mx_ThreadPanel__header">
<span>{ _t("Thread") }</span>
<ThreadListContextMenu
mxEvent={this.props.mxEvent}
permalinkCreator={this.props.permalinkCreator} />
</div>;
};
public render(): JSX.Element {
const highlightedEventId = this.props.isInitialEventHighlighted
? this.props.initialEvent?.getId()
@ -303,20 +312,24 @@ export default class ThreadView extends React.Component<IProps, IState> {
timelineRenderingType: TimelineRenderingType.Thread,
threadId: this.state.thread?.id,
liveTimeline: this.state?.thread?.timelineSet?.getLiveTimeline(),
narrow: this.state.narrow,
}}>
<BaseCard
className="mx_ThreadView mx_ThreadPanel"
onClose={this.props.onClose}
withoutScrollContainer={true}
header={this.renderThreadViewHeader()}
ref={this.cardRef}
ref={this.card}
onKeyDown={this.onKeyDown}
>
<Measured
sensor={this.card.current}
onMeasurement={this.onMeasurement}
/>
{ this.state.thread && <div className="mx_ThreadView_timelinePanelWrapper">
<FileDropTarget parent={this.cardRef.current} onFileDrop={this.onFileDrop} />
<FileDropTarget parent={this.card.current} onFileDrop={this.onFileDrop} />
<TimelinePanel
ref={this.timelinePanelRef}
ref={this.timelinePanel}
showReadReceipts={false} // Hide the read receipts
// until homeservers speak threads language
manageReadReceipts={true}