mirror of
https://github.com/vector-im/element-web.git
synced 2024-11-15 20:54:59 +08:00
Fix thread summary layout for narrow right panel timeline (#7838)
This commit is contained in:
parent
5e76d988ca
commit
d8ac7cf202
@ -58,6 +58,12 @@ limitations under the License.
|
||||
padding-right: 36px;
|
||||
}
|
||||
|
||||
.mx_EventTile:not([data-layout="bubble"]) .mx_ThreadInfo {
|
||||
margin-left: 36px;
|
||||
margin-right: 0;
|
||||
max-width: min(calc(100% - 36px), 600px);
|
||||
}
|
||||
|
||||
.mx_GroupLayout .mx_EventTile > .mx_SenderProfile {
|
||||
margin-left: 36px;
|
||||
}
|
||||
|
@ -723,7 +723,7 @@ $left-gutter: 64px;
|
||||
.mx_ThreadInfo {
|
||||
min-width: 267px;
|
||||
max-width: min(calc(100% - 64px), 600px);
|
||||
width: auto;
|
||||
width: fit-content;
|
||||
height: 40px;
|
||||
position: relative;
|
||||
background-color: $system;
|
||||
@ -777,6 +777,12 @@ $left-gutter: 64px;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_MessagePanel_narrow .mx_ThreadInfo {
|
||||
min-width: initial;
|
||||
max-width: initial;
|
||||
width: initial;
|
||||
}
|
||||
|
||||
.mx_ThreadInfo_content {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -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>;
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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}
|
||||
|
71
src/components/views/elements/Measured.tsx
Normal file
71
src/components/views/elements/Measured.tsx
Normal file
@ -0,0 +1,71 @@
|
||||
/*
|
||||
Copyright 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.
|
||||
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 UIStore, { UI_EVENTS } from "../../../stores/UIStore";
|
||||
|
||||
interface IProps {
|
||||
sensor: Element;
|
||||
breakpoint: number;
|
||||
onMeasurement(narrow: boolean): void;
|
||||
}
|
||||
|
||||
export default class Measured extends React.PureComponent<IProps> {
|
||||
private static instanceCount = 0;
|
||||
private readonly instanceId: number;
|
||||
|
||||
static defaultProps = {
|
||||
breakpoint: 500,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.instanceId = Measured.instanceCount++;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
UIStore.instance.on(`Measured${this.instanceId}`, this.onResize);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Readonly<IProps>) {
|
||||
const previous = prevProps.sensor;
|
||||
const current = this.props.sensor;
|
||||
if (previous === current) return;
|
||||
if (previous) {
|
||||
UIStore.instance.stopTrackingElementDimensions(`Measured${this.instanceId}`);
|
||||
}
|
||||
if (current) {
|
||||
UIStore.instance.trackElementDimensions(`Measured${this.instanceId}`,
|
||||
this.props.sensor);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
UIStore.instance.off(`Measured${this.instanceId}`, this.onResize);
|
||||
UIStore.instance.stopTrackingElementDimensions(`Measured${this.instanceId}`);
|
||||
}
|
||||
|
||||
private onResize = (type: UI_EVENTS, entry: ResizeObserverEntry) => {
|
||||
if (type !== UI_EVENTS.Resize) return;
|
||||
this.props.onMeasurement(entry.contentRect.width <= this.props.breakpoint);
|
||||
};
|
||||
|
||||
render() {
|
||||
return null;
|
||||
}
|
||||
}
|
@ -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.
|
||||
@ -42,6 +42,7 @@ import UploadBar from '../../structures/UploadBar';
|
||||
import SettingsStore from '../../../settings/SettingsStore';
|
||||
import JumpToBottomButton from '../rooms/JumpToBottomButton';
|
||||
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
||||
import Measured from '../elements/Measured';
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
@ -55,6 +56,7 @@ interface IProps {
|
||||
showComposer?: boolean;
|
||||
composerRelation?: IEventRelation;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
thread?: Thread;
|
||||
editState?: EditorStateTransfer;
|
||||
@ -63,6 +65,7 @@ interface IState {
|
||||
isInitialEventHighlighted?: boolean;
|
||||
layout: Layout;
|
||||
atEndOfLiveTimeline: boolean;
|
||||
narrow: boolean;
|
||||
|
||||
// settings:
|
||||
showReadReceipts?: boolean;
|
||||
@ -74,7 +77,8 @@ export default class TimelineCard extends React.Component<IProps, IState> {
|
||||
|
||||
private dispatcherRef: string;
|
||||
private layoutWatcherRef: string;
|
||||
private timelinePanelRef: React.RefObject<TimelinePanel> = React.createRef();
|
||||
private timelinePanel = React.createRef<TimelinePanel>();
|
||||
private card = React.createRef<HTMLDivElement>();
|
||||
private roomStoreToken: EventSubscription;
|
||||
private readReceiptsSettingWatcher: string;
|
||||
|
||||
@ -84,6 +88,7 @@ export default class TimelineCard extends React.Component<IProps, IState> {
|
||||
showReadReceipts: SettingsStore.getValue("showReadReceipts", props.room.roomId),
|
||||
layout: SettingsStore.getValue("layout"),
|
||||
atEndOfLiveTimeline: true,
|
||||
narrow: false,
|
||||
};
|
||||
this.readReceiptsSettingWatcher = null;
|
||||
}
|
||||
@ -134,7 +139,7 @@ export default class TimelineCard 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;
|
||||
@ -157,7 +162,7 @@ export default class TimelineCard extends React.Component<IProps, IState> {
|
||||
};
|
||||
|
||||
private onScroll = (): void => {
|
||||
const timelinePanel = this.timelinePanelRef.current;
|
||||
const timelinePanel = this.timelinePanel.current;
|
||||
if (!timelinePanel) return;
|
||||
if (timelinePanel.isAtEndOfLiveTimeline()) {
|
||||
this.setState({
|
||||
@ -170,6 +175,10 @@ export default class TimelineCard extends React.Component<IProps, IState> {
|
||||
}
|
||||
};
|
||||
|
||||
private onMeasurement = (narrow: boolean): void => {
|
||||
this.setState({ narrow });
|
||||
};
|
||||
|
||||
private jumpToLiveTimeline = () => {
|
||||
if (this.state.initialEventId && this.state.isInitialEventHighlighted) {
|
||||
// If we were viewing a highlighted event, firing view_room without
|
||||
@ -181,7 +190,7 @@ export default class TimelineCard extends React.Component<IProps, IState> {
|
||||
});
|
||||
} else {
|
||||
// Otherwise we have to jump manually
|
||||
this.timelinePanelRef.current?.jumpToLiveTimeline();
|
||||
this.timelinePanel.current?.jumpToLiveTimeline();
|
||||
dis.fire(Action.FocusSendMessageComposer);
|
||||
}
|
||||
};
|
||||
@ -210,22 +219,30 @@ export default class TimelineCard extends React.Component<IProps, IState> {
|
||||
/>);
|
||||
}
|
||||
|
||||
const isUploading = ContentMessages.sharedInstance().getCurrentUploads(this.props.composerRelation).length > 0;
|
||||
|
||||
return (
|
||||
<RoomContext.Provider value={{
|
||||
...this.context,
|
||||
timelineRenderingType: this.props.timelineRenderingType ?? this.context.timelineRenderingType,
|
||||
liveTimeline: this.props.timelineSet.getLiveTimeline(),
|
||||
narrow: this.state.narrow,
|
||||
}}>
|
||||
<BaseCard
|
||||
className={this.props.classNames}
|
||||
onClose={this.props.onClose}
|
||||
withoutScrollContainer={true}
|
||||
header={this.renderTimelineCardHeader()}
|
||||
ref={this.card}
|
||||
>
|
||||
<Measured
|
||||
sensor={this.card.current}
|
||||
onMeasurement={this.onMeasurement}
|
||||
/>
|
||||
<div className="mx_TimelineCard_timeline">
|
||||
{ jumpToBottom }
|
||||
<TimelinePanel
|
||||
ref={this.timelinePanelRef}
|
||||
ref={this.timelinePanel}
|
||||
showReadReceipts={this.state.showReadReceipts}
|
||||
manageReadReceipts={true}
|
||||
manageReadMarkers={false} // No RM support in the TimelineCard
|
||||
@ -249,7 +266,7 @@ export default class TimelineCard extends React.Component<IProps, IState> {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{ ContentMessages.sharedInstance().getCurrentUploads(this.props.composerRelation).length > 0 && (
|
||||
{ isUploading && (
|
||||
<UploadBar room={this.props.room} relation={this.props.composerRelation} />
|
||||
) }
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2015-2021 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2015 - 2022 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@ -677,6 +677,12 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||
<p className="mx_ThreadSummaryIcon">{ _t("From a thread") }</p>
|
||||
);
|
||||
} else if (this.state.threadReplyCount && this.props.mxEvent.isThreadRoot) {
|
||||
let count: string | number = this.state.threadReplyCount;
|
||||
if (!this.context.narrow) {
|
||||
count = _t("%(count)s reply", {
|
||||
count: this.state.threadReplyCount,
|
||||
});
|
||||
}
|
||||
return (
|
||||
<CardContext.Consumer>
|
||||
{ context =>
|
||||
@ -687,9 +693,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||
}}
|
||||
>
|
||||
<span className="mx_ThreadInfo_threads-amount">
|
||||
{ _t("%(count)s reply", {
|
||||
count: this.state.threadReplyCount,
|
||||
}) }
|
||||
{ count }
|
||||
</span>
|
||||
{ this.renderThreadLastMessagePreview() }
|
||||
</div>
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2015-2021 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2015 - 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.
|
||||
@ -54,7 +54,6 @@ import { ButtonEvent } from '../elements/AccessibleButton';
|
||||
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
||||
|
||||
let instanceCount = 0;
|
||||
const NARROW_MODE_BREAKPOINT = 500;
|
||||
|
||||
interface ISendButtonProps {
|
||||
onClick: (ev: ButtonEvent) => void;
|
||||
@ -88,7 +87,6 @@ interface IState {
|
||||
haveRecording: boolean;
|
||||
recordingTimeLeftSeconds?: number;
|
||||
me?: RoomMember;
|
||||
narrowMode?: boolean;
|
||||
isMenuOpen: boolean;
|
||||
isStickerPickerOpen: boolean;
|
||||
showStickersButton: boolean;
|
||||
@ -165,10 +163,9 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
||||
|
||||
private onResize = (type: UI_EVENTS, entry: ResizeObserverEntry) => {
|
||||
if (type === UI_EVENTS.Resize) {
|
||||
const narrowMode = entry.contentRect.width <= NARROW_MODE_BREAKPOINT;
|
||||
const { narrow } = this.context;
|
||||
this.setState({
|
||||
narrowMode,
|
||||
isMenuOpen: !narrowMode ? false : this.state.isMenuOpen,
|
||||
isMenuOpen: !narrow ? false : this.state.isMenuOpen,
|
||||
isStickerPickerOpen: false,
|
||||
});
|
||||
}
|
||||
@ -476,11 +473,10 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
||||
isMenuOpen={this.state.isMenuOpen}
|
||||
isStickerPickerOpen={this.state.isStickerPickerOpen}
|
||||
menuPosition={menuPosition}
|
||||
narrowMode={this.state.narrowMode}
|
||||
relation={this.props.relation}
|
||||
onRecordStartEndClick={() => {
|
||||
this.voiceRecordingButton.current?.onRecordStartEndClick();
|
||||
if (this.state.narrowMode) {
|
||||
if (this.context.narrow) {
|
||||
this.toggleButtonMenu();
|
||||
}
|
||||
}}
|
||||
|
@ -44,7 +44,6 @@ interface IProps {
|
||||
isMenuOpen: boolean;
|
||||
isStickerPickerOpen: boolean;
|
||||
menuPosition: AboveLeftOf;
|
||||
narrowMode?: boolean;
|
||||
onRecordStartEndClick: () => void;
|
||||
relation?: IEventRelation;
|
||||
setStickerPickerOpen: (isStickerPickerOpen: boolean) => void;
|
||||
@ -58,7 +57,7 @@ export const OverflowMenuContext = createContext<OverflowMenuCloser | null>(null
|
||||
|
||||
const MessageComposerButtons: React.FC<IProps> = (props: IProps) => {
|
||||
const matrixClient: MatrixClient = useContext(MatrixClientContext);
|
||||
const { room, roomId } = useContext(RoomContext);
|
||||
const { room, roomId, narrow } = useContext(RoomContext);
|
||||
|
||||
if (props.haveRecording) {
|
||||
return null;
|
||||
@ -66,14 +65,14 @@ const MessageComposerButtons: React.FC<IProps> = (props: IProps) => {
|
||||
|
||||
let mainButtons: ReactElement[];
|
||||
let moreButtons: ReactElement[];
|
||||
if (props.narrowMode) {
|
||||
if (narrow) {
|
||||
mainButtons = [
|
||||
emojiButton(props),
|
||||
];
|
||||
moreButtons = [
|
||||
uploadButton(props, roomId),
|
||||
showStickersButton(props),
|
||||
voiceRecordingButton(props),
|
||||
voiceRecordingButton(props, narrow),
|
||||
pollButton(room, props.relation),
|
||||
showLocationButton(props, room, roomId, matrixClient),
|
||||
];
|
||||
@ -84,7 +83,7 @@ const MessageComposerButtons: React.FC<IProps> = (props: IProps) => {
|
||||
];
|
||||
moreButtons = [
|
||||
showStickersButton(props),
|
||||
voiceRecordingButton(props),
|
||||
voiceRecordingButton(props, narrow),
|
||||
pollButton(room, props.relation),
|
||||
showLocationButton(props, room, roomId, matrixClient),
|
||||
];
|
||||
@ -260,10 +259,10 @@ function showStickersButton(props: IProps): ReactElement {
|
||||
);
|
||||
}
|
||||
|
||||
function voiceRecordingButton(props: IProps): ReactElement {
|
||||
function voiceRecordingButton(props: IProps, narrow: boolean): ReactElement {
|
||||
// XXX: recording UI does not work well in narrow mode, so hide for now
|
||||
return (
|
||||
props.narrowMode
|
||||
narrow
|
||||
? null
|
||||
: <CollapsibleButton
|
||||
key="voice_message_send"
|
||||
|
@ -64,6 +64,7 @@ const RoomContext = createContext<IRoomState>({
|
||||
timelineRenderingType: TimelineRenderingType.Room,
|
||||
threadId: undefined,
|
||||
liveTimeline: undefined,
|
||||
narrow: false,
|
||||
});
|
||||
RoomContext.displayName = "RoomContext";
|
||||
export default RoomContext;
|
||||
|
@ -38,11 +38,11 @@ describe("MessageComposerButtons", () => {
|
||||
const buttons = wrapAndRender(
|
||||
<MessageComposerButtons
|
||||
isMenuOpen={false}
|
||||
narrowMode={false}
|
||||
showLocationButton={true}
|
||||
showStickersButton={true}
|
||||
toggleButtonMenu={() => {}}
|
||||
/>,
|
||||
false,
|
||||
);
|
||||
|
||||
expect(buttonLabels(buttons)).toEqual([
|
||||
@ -56,11 +56,11 @@ describe("MessageComposerButtons", () => {
|
||||
const buttons = wrapAndRender(
|
||||
<MessageComposerButtons
|
||||
isMenuOpen={true}
|
||||
narrowMode={false}
|
||||
showLocationButton={true}
|
||||
showStickersButton={true}
|
||||
toggleButtonMenu={() => {}}
|
||||
/>,
|
||||
false,
|
||||
);
|
||||
|
||||
expect(buttonLabels(buttons)).toEqual([
|
||||
@ -80,11 +80,11 @@ describe("MessageComposerButtons", () => {
|
||||
const buttons = wrapAndRender(
|
||||
<MessageComposerButtons
|
||||
isMenuOpen={false}
|
||||
narrowMode={true}
|
||||
showLocationButton={true}
|
||||
showStickersButton={true}
|
||||
toggleButtonMenu={() => {}}
|
||||
/>,
|
||||
true,
|
||||
);
|
||||
|
||||
expect(buttonLabels(buttons)).toEqual([
|
||||
@ -97,11 +97,11 @@ describe("MessageComposerButtons", () => {
|
||||
const buttons = wrapAndRender(
|
||||
<MessageComposerButtons
|
||||
isMenuOpen={true}
|
||||
narrowMode={true}
|
||||
showLocationButton={true}
|
||||
showStickersButton={true}
|
||||
toggleButtonMenu={() => {}}
|
||||
/>,
|
||||
true,
|
||||
);
|
||||
|
||||
expect(buttonLabels(buttons)).toEqual([
|
||||
@ -117,7 +117,7 @@ describe("MessageComposerButtons", () => {
|
||||
});
|
||||
});
|
||||
|
||||
function wrapAndRender(component: React.ReactElement): ReactWrapper {
|
||||
function wrapAndRender(component: React.ReactElement, narrow: boolean): ReactWrapper {
|
||||
const mockClient = MatrixClientPeg.matrixClient = createTestClient();
|
||||
const roomId = "myroomid";
|
||||
const mockRoom: any = {
|
||||
@ -128,7 +128,7 @@ function wrapAndRender(component: React.ReactElement): ReactWrapper {
|
||||
return new RoomMember(roomId, userId);
|
||||
},
|
||||
};
|
||||
const roomState = createRoomState(mockRoom);
|
||||
const roomState = createRoomState(mockRoom, narrow);
|
||||
|
||||
return mount(
|
||||
<MatrixClientContext.Provider value={mockClient}>
|
||||
@ -139,7 +139,7 @@ function wrapAndRender(component: React.ReactElement): ReactWrapper {
|
||||
);
|
||||
}
|
||||
|
||||
function createRoomState(room: Room): IRoomState {
|
||||
function createRoomState(room: Room, narrow: boolean): IRoomState {
|
||||
return {
|
||||
room: room,
|
||||
roomId: room.roomId,
|
||||
@ -176,6 +176,7 @@ function createRoomState(room: Room): IRoomState {
|
||||
matrixClientIsReady: false,
|
||||
timelineRenderingType: TimelineRenderingType.Room,
|
||||
liveTimeline: undefined,
|
||||
narrow,
|
||||
};
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user