From f325e83d811088dc805ccb28dee7c30f7941c259 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 17 Feb 2022 17:51:17 +0000 Subject: [PATCH 1/7] Fix thread summary layout for narrow right panel timeline --- res/css/views/right_panel/_TimelineCard.scss | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/res/css/views/right_panel/_TimelineCard.scss b/res/css/views/right_panel/_TimelineCard.scss index 5acdc1a8e91..43b9bd87b9c 100644 --- a/res/css/views/right_panel/_TimelineCard.scss +++ b/res/css/views/right_panel/_TimelineCard.scss @@ -58,6 +58,13 @@ limitations under the License. padding-right: 36px; } + .mx_EventTile:not([data-layout="bubble"]) .mx_ThreadInfo { + margin-left: 36px; + margin-right: 0; + min-width: initial; + max-width: initial; + } + .mx_GroupLayout .mx_EventTile > .mx_SenderProfile { margin-left: 36px; } From cc26daae5db73376fb3aee511f03b0f29aeade65 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 18 Feb 2022 15:48:40 +0000 Subject: [PATCH 2/7] Hide replies label when narrow --- src/components/structures/MessagePanel.tsx | 21 +++++++++++++++++++++ src/components/views/rooms/EventTile.tsx | 13 ++++++++++--- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/src/components/structures/MessagePanel.tsx b/src/components/structures/MessagePanel.tsx index 689d02155f4..17748612800 100644 --- a/src/components/structures/MessagePanel.tsx +++ b/src/components/structures/MessagePanel.tsx @@ -52,8 +52,10 @@ import EditorStateTransfer from "../../utils/EditorStateTransfer"; import { Action } from '../../dispatcher/actions'; import { getEventDisplayInfo } from "../../utils/EventUtils"; import { IReadReceiptInfo } from "../views/rooms/ReadReceiptMarker"; +import UIStore, { UI_EVENTS } from '../../stores/UIStore'; const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes +const NARROW_MODE_BREAKPOINT = 400; const continuedTypes = [EventType.Sticker, EventType.RoomMessage]; const groupedEvents = [ EventType.RoomMember, @@ -190,6 +192,7 @@ interface IState { ghostReadMarkers: string[]; showTypingNotifications: boolean; hideSender: boolean; + narrowMode?: boolean; } interface IReadReceiptForUser { @@ -255,6 +258,9 @@ export default class MessagePanel extends React.Component { private readonly showTypingNotificationsWatcherRef: string; private eventTiles: Record = {}; + private static instanceCount = 0; + private readonly instanceId: number; + constructor(props, context) { super(props, context); @@ -273,17 +279,24 @@ export default class MessagePanel extends React.Component { this.showTypingNotificationsWatcherRef = SettingsStore.watchSetting("showTypingNotifications", null, this.onShowTypingNotificationsChange); + + this.instanceId = MessagePanel.instanceCount++; } componentDidMount() { this.calculateRoomMembersCount(); this.props.room?.on("RoomState.members", this.calculateRoomMembersCount); + UIStore.instance.on(`MessagePanel${this.instanceId}`, this.onResize); + UIStore.instance.trackElementDimensions(`MessagePanel${this.instanceId}`, + ReactDOM.findDOMNode(this.scrollPanel.current) as Element); this.isMounted = true; } componentWillUnmount() { this.isMounted = false; this.props.room?.off("RoomState.members", this.calculateRoomMembersCount); + UIStore.instance.off(`MessagePanel${this.instanceId}`, this.onResize); + UIStore.instance.stopTrackingElementDimensions(`MessagePanel${this.instanceId}`); SettingsStore.unwatchSetting(this.showTypingNotificationsWatcherRef); } @@ -817,6 +830,7 @@ export default class MessagePanel extends React.Component { callEventGrouper={callEventGrouper} hideSender={this.state.hideSender} timelineRenderingType={this.context.timelineRenderingType} + narrowMode={this.state.narrowMode} /> , ); @@ -926,6 +940,13 @@ export default class MessagePanel extends React.Component { this.eventTiles[eventId] = node; }; + private onResize = (type: UI_EVENTS, entry: ResizeObserverEntry) => { + if (type !== UI_EVENTS.Resize) return; + this.setState({ + narrowMode: entry.contentRect.width <= NARROW_MODE_BREAKPOINT, + }); + }; + // once dynamic content in the events load, make the scrollPanel check the // scroll offsets. public onHeightChanged = (): void => { diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index f1f5a4f36db..fc86f376770 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -340,6 +340,9 @@ interface IProps { // displayed to the current user either because they're // the author or they are a moderator isSeeingThroughMessageHiddenForModeration?: boolean; + + // Whether we should assume a smaller width and adjust layout to match + narrowMode?: boolean; } interface IState { @@ -690,6 +693,12 @@ export default class EventTile extends React.Component {

{ _t("From a thread") }

); } else if (this.state.threadReplyCount && this.props.mxEvent.isThreadRoot) { + let count: string | number = this.state.threadReplyCount; + if (!this.props.narrowMode) { + count = _t("%(count)s reply", { + count: this.state.threadReplyCount, + }); + } return ( { context => @@ -700,9 +709,7 @@ export default class EventTile extends React.Component { }} > - { _t("%(count)s reply", { - count: this.state.threadReplyCount, - }) } + { count } { this.renderThreadLastMessagePreview() } From 24ae2f9fc66b4de3c52ecd2b7376ad4dff9ac250 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Mon, 21 Feb 2022 10:34:58 +0000 Subject: [PATCH 3/7] Apply narrow mode to thread summaries in all timelines --- res/css/views/right_panel/_TimelineCard.scss | 3 +-- res/css/views/rooms/_EventTile.scss | 5 +++++ src/components/structures/MessagePanel.tsx | 7 ++++++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/res/css/views/right_panel/_TimelineCard.scss b/res/css/views/right_panel/_TimelineCard.scss index 43b9bd87b9c..58220e97d76 100644 --- a/res/css/views/right_panel/_TimelineCard.scss +++ b/res/css/views/right_panel/_TimelineCard.scss @@ -61,8 +61,7 @@ limitations under the License. .mx_EventTile:not([data-layout="bubble"]) .mx_ThreadInfo { margin-left: 36px; margin-right: 0; - min-width: initial; - max-width: initial; + max-width: min(100%, 600px); } .mx_GroupLayout .mx_EventTile > .mx_SenderProfile { diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index deb71fa678d..b4385e11f2a 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -776,6 +776,11 @@ $left-gutter: 64px; } } +.mx_MessagePanel_narrow .mx_ThreadInfo { + min-width: initial; + max-width: initial; +} + .mx_ThreadInfo_content { text-overflow: ellipsis; overflow: hidden; diff --git a/src/components/structures/MessagePanel.tsx b/src/components/structures/MessagePanel.tsx index 17748612800..3460c505cb1 100644 --- a/src/components/structures/MessagePanel.tsx +++ b/src/components/structures/MessagePanel.tsx @@ -53,6 +53,7 @@ import { Action } from '../../dispatcher/actions'; import { getEventDisplayInfo } from "../../utils/EventUtils"; import { IReadReceiptInfo } from "../views/rooms/ReadReceiptMarker"; import UIStore, { UI_EVENTS } from '../../stores/UIStore'; +import classNames from 'classnames'; const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes const NARROW_MODE_BREAKPOINT = 400; @@ -1033,11 +1034,15 @@ export default class MessagePanel extends React.Component { />; } + const classes = classNames(this.props.className, { + "mx_MessagePanel_narrow": this.state.narrowMode, + }); + return ( Date: Mon, 21 Feb 2022 10:42:07 +0000 Subject: [PATCH 4/7] Fix import order --- src/components/structures/MessagePanel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/MessagePanel.tsx b/src/components/structures/MessagePanel.tsx index 3460c505cb1..29c3650137e 100644 --- a/src/components/structures/MessagePanel.tsx +++ b/src/components/structures/MessagePanel.tsx @@ -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'; @@ -53,7 +54,6 @@ import { Action } from '../../dispatcher/actions'; import { getEventDisplayInfo } from "../../utils/EventUtils"; import { IReadReceiptInfo } from "../views/rooms/ReadReceiptMarker"; import UIStore, { UI_EVENTS } from '../../stores/UIStore'; -import classNames from 'classnames'; const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes const NARROW_MODE_BREAKPOINT = 400; From 59d2b10f47b575f37d247b1dd752201bee86be52 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Tue, 22 Feb 2022 11:09:28 +0000 Subject: [PATCH 5/7] Measure timeline width once and share via context --- src/components/structures/FilePanel.tsx | 27 +++--- src/components/structures/MessagePanel.tsx | 25 +----- .../structures/NotificationPanel.tsx | 24 +++--- src/components/structures/RoomView.tsx | 9 +- src/components/structures/ThreadPanel.tsx | 49 ++++++----- src/components/structures/ThreadView.tsx | 83 ++++++++++--------- src/components/views/elements/Measured.tsx | 73 ++++++++++++++++ .../views/right_panel/TimelineCard.tsx | 83 ++++++++++--------- src/components/views/rooms/EventTile.tsx | 7 +- .../views/rooms/MessageComposer.tsx | 12 +-- .../views/rooms/MessageComposerButtons.tsx | 13 ++- src/contexts/RoomContext.ts | 1 + .../rooms/MessageComposerButtons-test.tsx | 15 ++-- 13 files changed, 244 insertions(+), 177 deletions(-) create mode 100644 src/components/views/elements/Measured.tsx diff --git a/src/components/structures/FilePanel.tsx b/src/components/structures/FilePanel.tsx index 6a7db3dc0be..f12a292871f 100644 --- a/src/components/structures/FilePanel.tsx +++ b/src/components/structures/FilePanel.tsx @@ -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. @@ -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; @@ -262,17 +263,19 @@ class FilePanel extends React.Component { onClose={this.props.onClose} withoutScrollContainer > - - + + + + ); diff --git a/src/components/structures/MessagePanel.tsx b/src/components/structures/MessagePanel.tsx index efaf2e9b141..471e8b1d192 100644 --- a/src/components/structures/MessagePanel.tsx +++ b/src/components/structures/MessagePanel.tsx @@ -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. @@ -53,10 +53,8 @@ import EditorStateTransfer from "../../utils/EditorStateTransfer"; import { Action } from '../../dispatcher/actions'; import { getEventDisplayInfo } from "../../utils/EventUtils"; import { IReadReceiptInfo } from "../views/rooms/ReadReceiptMarker"; -import UIStore, { UI_EVENTS } from '../../stores/UIStore'; const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes -const NARROW_MODE_BREAKPOINT = 400; const continuedTypes = [EventType.Sticker, EventType.RoomMessage]; const groupedEvents = [ EventType.RoomMember, @@ -190,7 +188,6 @@ interface IState { ghostReadMarkers: string[]; showTypingNotifications: boolean; hideSender: boolean; - narrowMode?: boolean; } interface IReadReceiptForUser { @@ -256,9 +253,6 @@ export default class MessagePanel extends React.Component { private readonly showTypingNotificationsWatcherRef: string; private eventTiles: Record = {}; - private static instanceCount = 0; - private readonly instanceId: number; - constructor(props, context) { super(props, context); @@ -277,24 +271,17 @@ export default class MessagePanel extends React.Component { this.showTypingNotificationsWatcherRef = SettingsStore.watchSetting("showTypingNotifications", null, this.onShowTypingNotificationsChange); - - this.instanceId = MessagePanel.instanceCount++; } componentDidMount() { this.calculateRoomMembersCount(); this.props.room?.on("RoomState.members", this.calculateRoomMembersCount); - UIStore.instance.on(`MessagePanel${this.instanceId}`, this.onResize); - UIStore.instance.trackElementDimensions(`MessagePanel${this.instanceId}`, - ReactDOM.findDOMNode(this.scrollPanel.current) as Element); this.isMounted = true; } componentWillUnmount() { this.isMounted = false; this.props.room?.off("RoomState.members", this.calculateRoomMembersCount); - UIStore.instance.off(`MessagePanel${this.instanceId}`, this.onResize); - UIStore.instance.stopTrackingElementDimensions(`MessagePanel${this.instanceId}`); SettingsStore.unwatchSetting(this.showTypingNotificationsWatcherRef); } @@ -827,7 +814,6 @@ export default class MessagePanel extends React.Component { callEventGrouper={callEventGrouper} hideSender={this.state.hideSender} timelineRenderingType={this.context.timelineRenderingType} - narrowMode={this.state.narrowMode} /> , ); @@ -937,13 +923,6 @@ export default class MessagePanel extends React.Component { this.eventTiles[eventId] = node; }; - private onResize = (type: UI_EVENTS, entry: ResizeObserverEntry) => { - if (type !== UI_EVENTS.Resize) return; - this.setState({ - narrowMode: entry.contentRect.width <= NARROW_MODE_BREAKPOINT, - }); - }; - // once dynamic content in the events load, make the scrollPanel check the // scroll offsets. public onHeightChanged = (): void => { @@ -1034,7 +1013,7 @@ export default class MessagePanel extends React.Component { } const classes = classNames(this.props.className, { - "mx_MessagePanel_narrow": this.state.narrowMode, + "mx_MessagePanel_narrow": this.context.narrow, }); return ( diff --git a/src/components/structures/NotificationPanel.tsx b/src/components/structures/NotificationPanel.tsx index d7eddb4ebbf..2cae2298d6e 100644 --- a/src/components/structures/NotificationPanel.tsx +++ b/src/components/structures/NotificationPanel.tsx @@ -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,6 +25,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 { onClose(): void; @@ -36,6 +37,7 @@ interface IProps { @replaceableComponent("structures.NotificationPanel") export default class NotificationPanel extends React.PureComponent { static contextType = RoomContext; + render() { const emptyState = (

{ _t("You're all caught up") }

@@ -47,15 +49,17 @@ export default class NotificationPanel extends React.PureComponent { if (timelineSet) { // wrap a TimelinePanel with the jump-to-event bits turned off. content = ( - + + + ); } else { logger.error("No notifTimelineSet available!"); diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index b49b02d7b0a..9d13c9c3a74 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -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") @@ -271,6 +273,7 @@ export class RoomView extends React.Component { mainSplitContentType: MainSplitContentType.Timeline, timelineRenderingType: TimelineRenderingType.Room, liveTimeline: undefined, + narrow: false, }; this.dispatcherRef = dis.register(this.onAction); @@ -2083,7 +2086,7 @@ export class RoomView extends React.Component { const showChatEffects = SettingsStore.getValue('showChatEffects'); // Decide what to show in the main split - let mainSplitBody = + let mainSplitBody = { auxPanel }
@@ -2095,7 +2098,7 @@ export class RoomView extends React.Component { { statusBarArea } { previewBar } { messageComposer } - ; + ; switch (this.state.mainSplitContentType) { case MainSplitContentType.Timeline: diff --git a/src/components/structures/ThreadPanel.tsx b/src/components/structures/ThreadPanel.tsx index 151af8377ad..9781973cf75 100644 --- a/src/components/structures/ThreadPanel.tsx +++ b/src/components/structures/ThreadPanel.tsx @@ -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. @@ -38,6 +38,7 @@ import TimelinePanel from './TimelinePanel'; import { Layout } from '../../settings/enums/Layout'; import { RoomPermalinkCreator } from '../../utils/permalinks/Permalinks'; import { useEventEmitter } from '../../hooks/useEventEmitter'; +import Measured from '../views/elements/Measured'; async function getThreadTimelineSet( client: MatrixClient, @@ -281,28 +282,30 @@ const ThreadPanel: React.FC = ({ roomId, onClose, permalinkCreator }) => withoutScrollContainer={true} > { timelineSet && ( - setFilterOption(ThreadFilterType.All)} - />} - alwaysShowTimestamps={true} - layout={Layout.Group} - hideThreadedMessages={false} - hidden={false} - showReactions={false} - className="mx_RoomView_messagePanel mx_GroupLayout" - membersLoaded={true} - permalinkCreator={permalinkCreator} - disableGrouping={true} - /> + + setFilterOption(ThreadFilterType.All)} + />} + alwaysShowTimestamps={true} + layout={Layout.Group} + hideThreadedMessages={false} + hidden={false} + showReactions={false} + className="mx_RoomView_messagePanel mx_GroupLayout" + membersLoaded={true} + permalinkCreator={permalinkCreator} + disableGrouping={true} + /> + ) } diff --git a/src/components/structures/ThreadView.tsx b/src/components/structures/ThreadView.tsx index f66842792f3..b70ca289647 100644 --- a/src/components/structures/ThreadView.tsx +++ b/src/components/structures/ThreadView.tsx @@ -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; @@ -315,46 +316,48 @@ export default class ThreadView extends React.Component { ref={this.cardRef} onKeyDown={this.onKeyDown} > - { this.state.thread &&
- -
} - - { ContentMessages.sharedInstance().getCurrentUploads(threadRelation).length > 0 && ( - - ) } - - { this.state?.thread?.timelineSet && () } + e2eStatus={this.props.e2eStatus} + compact={true} + />) } + ); diff --git a/src/components/views/elements/Measured.tsx b/src/components/views/elements/Measured.tsx new file mode 100644 index 00000000000..af4709b1e59 --- /dev/null +++ b/src/components/views/elements/Measured.tsx @@ -0,0 +1,73 @@ +/* +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, { createRef } from "react"; + +import RoomContext from "../../../contexts/RoomContext"; +import UIStore, { UI_EVENTS } from "../../../stores/UIStore"; + +const NARROW_BREAKPOINT = 500; + +interface IState { + narrow: boolean; +} + +export default class Measured extends React.Component<{}, IState> { + static contextType = RoomContext; + + private static instanceCount = 0; + private readonly instanceId: number; + + private sensor = createRef(); + + constructor(props, context) { + super(props, context); + + this.instanceId = Measured.instanceCount++; + + this.state = { + narrow: false, + }; + } + + componentDidMount() { + UIStore.instance.on(`Measured${this.instanceId}`, this.onResize); + UIStore.instance.trackElementDimensions(`Measured${this.instanceId}`, + this.sensor.current); + } + + 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.setState({ + narrow: entry.contentRect.width <= NARROW_BREAKPOINT, + }); + }; + + render() { + return + { this.props.children } +
+ ; + } +} diff --git a/src/components/views/right_panel/TimelineCard.tsx b/src/components/views/right_panel/TimelineCard.tsx index b03dfa4debc..c0c0447111b 100644 --- a/src/components/views/right_panel/TimelineCard.tsx +++ b/src/components/views/right_panel/TimelineCard.tsx @@ -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; @@ -210,6 +211,8 @@ export default class TimelineCard extends React.Component { />); } + const isUploading = ContentMessages.sharedInstance().getCurrentUploads(this.props.composerRelation).length > 0; + return ( { withoutScrollContainer={true} header={this.renderTimelineCardHeader()} > -
- { jumpToBottom } -
+ +
+ { jumpToBottom } +
- { ContentMessages.sharedInstance().getCurrentUploads(this.props.composerRelation).length > 0 && ( - - ) } + { isUploading && ( + + ) } - + +
); diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index b5b1a9fe0dd..28bb6a2f796 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -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"); @@ -327,9 +327,6 @@ interface IProps { // displayed to the current user either because they're // the author or they are a moderator isSeeingThroughMessageHiddenForModeration?: boolean; - - // Whether we should assume a smaller width and adjust layout to match - narrowMode?: boolean; } interface IState { @@ -681,7 +678,7 @@ export default class EventTile extends React.Component { ); } else if (this.state.threadReplyCount && this.props.mxEvent.isThreadRoot) { let count: string | number = this.state.threadReplyCount; - if (!this.props.narrowMode) { + if (!this.context.narrow) { count = _t("%(count)s reply", { count: this.state.threadReplyCount, }); diff --git a/src/components/views/rooms/MessageComposer.tsx b/src/components/views/rooms/MessageComposer.tsx index 721ca461529..4bd1fea383a 100644 --- a/src/components/views/rooms/MessageComposer.tsx +++ b/src/components/views/rooms/MessageComposer.tsx @@ -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. @@ -53,7 +53,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; @@ -87,7 +86,6 @@ interface IState { haveRecording: boolean; recordingTimeLeftSeconds?: number; me?: RoomMember; - narrowMode?: boolean; isMenuOpen: boolean; isStickerPickerOpen: boolean; showStickersButton: boolean; @@ -164,10 +162,9 @@ export default class MessageComposer extends React.Component { 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, }); } @@ -475,11 +472,10 @@ export default class MessageComposer extends React.Component { 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(); } }} diff --git a/src/components/views/rooms/MessageComposerButtons.tsx b/src/components/views/rooms/MessageComposerButtons.tsx index 4e016cd2efe..6375519621e 100644 --- a/src/components/views/rooms/MessageComposerButtons.tsx +++ b/src/components/views/rooms/MessageComposerButtons.tsx @@ -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(null const MessageComposerButtons: React.FC = (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 = (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 = (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 : ({ timelineRenderingType: TimelineRenderingType.Room, threadId: undefined, liveTimeline: undefined, + narrow: false, }); RoomContext.displayName = "RoomContext"; export default RoomContext; diff --git a/test/components/views/rooms/MessageComposerButtons-test.tsx b/test/components/views/rooms/MessageComposerButtons-test.tsx index 7eba7004224..b6fb8bbdab6 100644 --- a/test/components/views/rooms/MessageComposerButtons-test.tsx +++ b/test/components/views/rooms/MessageComposerButtons-test.tsx @@ -38,11 +38,11 @@ describe("MessageComposerButtons", () => { const buttons = wrapAndRender( {}} />, + false, ); expect(buttonLabels(buttons)).toEqual([ @@ -56,11 +56,11 @@ describe("MessageComposerButtons", () => { const buttons = wrapAndRender( {}} />, + false, ); expect(buttonLabels(buttons)).toEqual([ @@ -80,11 +80,11 @@ describe("MessageComposerButtons", () => { const buttons = wrapAndRender( {}} />, + true, ); expect(buttonLabels(buttons)).toEqual([ @@ -97,11 +97,11 @@ describe("MessageComposerButtons", () => { const buttons = wrapAndRender( {}} />, + 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( @@ -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, }; } From 26f7358541c2b1b2db87e8424bbe02c71356e369 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Tue, 22 Feb 2022 18:00:47 +0000 Subject: [PATCH 6/7] Use content width for wide timelines --- res/css/views/right_panel/_TimelineCard.scss | 2 +- res/css/views/rooms/_EventTile.scss | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/res/css/views/right_panel/_TimelineCard.scss b/res/css/views/right_panel/_TimelineCard.scss index 58220e97d76..2e42aeedfe9 100644 --- a/res/css/views/right_panel/_TimelineCard.scss +++ b/res/css/views/right_panel/_TimelineCard.scss @@ -61,7 +61,7 @@ limitations under the License. .mx_EventTile:not([data-layout="bubble"]) .mx_ThreadInfo { margin-left: 36px; margin-right: 0; - max-width: min(100%, 600px); + max-width: min(calc(100% - 36px), 600px); } .mx_GroupLayout .mx_EventTile > .mx_SenderProfile { diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 1f693badb5a..d37ef79a6f4 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -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; @@ -780,6 +780,7 @@ $left-gutter: 64px; .mx_MessagePanel_narrow .mx_ThreadInfo { min-width: initial; max-width: initial; + width: initial; } .mx_ThreadInfo_content { From 57cbcfd658add4addf1e5acb60c429953bbea007 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Wed, 23 Feb 2022 13:43:15 +0000 Subject: [PATCH 7/7] Rework Measured for better composability --- src/components/structures/FilePanel.tsx | 42 +++--- .../structures/NotificationPanel.tsx | 45 +++++-- src/components/structures/RoomView.tsx | 15 ++- src/components/structures/ThreadPanel.tsx | 64 ++++----- src/components/structures/ThreadView.tsx | 122 ++++++++++-------- src/components/views/elements/Measured.tsx | 54 ++++---- .../views/right_panel/TimelineCard.tsx | 100 +++++++------- 7 files changed, 255 insertions(+), 187 deletions(-) diff --git a/src/components/structures/FilePanel.tsx b/src/components/structures/FilePanel.tsx index c8f2ed78023..8466c26bacc 100644 --- a/src/components/structures/FilePanel.tsx +++ b/src/components/structures/FilePanel.tsx @@ -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"; @@ -45,6 +45,7 @@ interface IProps { interface IState { timelineSet: EventTimelineSet; + narrow: boolean; } /* @@ -52,14 +53,17 @@ interface IState { */ @replaceableComponent("structures.FilePanel") class FilePanel extends React.Component { + 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(); public noRoom: boolean; - static contextType = RoomContext; + private card = createRef(); state = { timelineSet: null, + narrow: false, }; private onRoomTimeline = ( @@ -185,6 +189,10 @@ class FilePanel extends React.Component { } }; + private onMeasurement = (narrow: boolean): void => { + this.setState({ narrow }); + }; + public async updateTimelineSet(roomId: string): Promise { const client = MatrixClientPeg.get(); const room = client.getRoom(roomId); @@ -257,25 +265,29 @@ class FilePanel extends React.Component { - - - - + + + ); diff --git a/src/components/structures/NotificationPanel.tsx b/src/components/structures/NotificationPanel.tsx index 2cae2298d6e..6af271bcc20 100644 --- a/src/components/structures/NotificationPanel.tsx +++ b/src/components/structures/NotificationPanel.tsx @@ -31,13 +31,31 @@ 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 { +export default class NotificationPanel extends React.PureComponent { static contextType = RoomContext; + private card = React.createRef(); + + constructor(props) { + super(props); + + this.state = { + narrow: false, + }; + } + + private onMeasurement = (narrow: boolean): void => { + this.setState({ narrow }); + }; + render() { const emptyState = (

{ _t("You're all caught up") }

@@ -49,17 +67,15 @@ export default class NotificationPanel extends React.PureComponent { if (timelineSet) { // wrap a TimelinePanel with the jump-to-event bits turned off. content = ( - - - + ); } else { logger.error("No notifTimelineSet available!"); @@ -69,8 +85,13 @@ export default class NotificationPanel extends React.PureComponent { return + { content } ; diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 9d13c9c3a74..8b7f8708e58 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -228,6 +228,7 @@ export class RoomView extends React.Component { private roomView = createRef(); private searchResultsPanel = createRef(); private messagePanel: TimelinePanel; + private roomViewBody = createRef(); static contextType = MatrixClientContext; @@ -1733,6 +1734,10 @@ export class RoomView extends React.Component { 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; @@ -2086,7 +2091,11 @@ export class RoomView extends React.Component { const showChatEffects = SettingsStore.getValue('showChatEffects'); // Decide what to show in the main split - let mainSplitBody = + let mainSplitBody = + { auxPanel }
@@ -2098,7 +2107,7 @@ export class RoomView extends React.Component { { statusBarArea } { previewBar } { messageComposer } - ; + ; switch (this.state.mainSplitContentType) { case MainSplitContentType.Timeline: @@ -2151,7 +2160,7 @@ export class RoomView extends React.Component { excludedRightPanelPhaseButtons={excludedRightPanelPhaseButtons} /> -
+
{ mainSplitBody }
diff --git a/src/components/structures/ThreadPanel.tsx b/src/components/structures/ThreadPanel.tsx index cd46527480a..d10b7c8fca3 100644 --- a/src/components/structures/ThreadPanel.tsx +++ b/src/components/structures/ThreadPanel.tsx @@ -214,12 +214,14 @@ const EmptyThread: React.FC = ({ filterOption, showAllThreads const ThreadPanel: React.FC = ({ roomId, onClose, permalinkCreator }) => { const mxClient = useContext(MatrixClientContext); const roomContext = useContext(RoomContext); - const [filterOption, setFilterOption] = useState(ThreadFilterType.All); - const ref = useRef(); + const timelinePanel = useRef(); + const card = useRef(); + const [filterOption, setFilterOption] = useState(ThreadFilterType.All); const [room, setRoom] = useState(mxClient.getRoom(roomId)); const [threadCount, setThreadCount] = useState(0); const [timelineSet, setTimelineSet] = useState(null); + const [narrow, setNarrow] = useState(false); useEffect(() => { setRoom(mxClient.getRoom(roomId)); @@ -258,7 +260,7 @@ const ThreadPanel: React.FC = ({ roomId, onClose, permalinkCreator }) => } function refreshTimeline() { - if (timelineSet) ref.current.refreshTimeline(); + if (timelineSet) timelinePanel.current.refreshTimeline(); } setThreadCount(room.threads.size); @@ -279,14 +281,15 @@ const ThreadPanel: React.FC = ({ roomId, onClose, permalinkCreator }) => }, [mxClient, room, filterOption]); useEffect(() => { - if (timelineSet) ref.current.refreshTimeline(); - }, [timelineSet, ref]); + if (timelineSet) timelinePanel.current.refreshTimeline(); + }, [timelineSet, timelinePanel]); return ( = ({ roomId, onClose, permalinkCreator }) => className="mx_ThreadPanel" onClose={onClose} withoutScrollContainer={true} + ref={card} > + { timelineSet && ( - - setFilterOption(ThreadFilterType.All)} - />} - alwaysShowTimestamps={true} - layout={Layout.Group} - hideThreadedMessages={false} - hidden={false} - showReactions={false} - className="mx_RoomView_messagePanel mx_GroupLayout" - membersLoaded={true} - permalinkCreator={permalinkCreator} - disableGrouping={true} - /> - + setFilterOption(ThreadFilterType.All)} + />} + alwaysShowTimestamps={true} + layout={Layout.Group} + hideThreadedMessages={false} + hidden={false} + showReactions={false} + className="mx_RoomView_messagePanel mx_GroupLayout" + membersLoaded={true} + permalinkCreator={permalinkCreator} + disableGrouping={true} + /> ) } diff --git a/src/components/structures/ThreadView.tsx b/src/components/structures/ThreadView.tsx index 6f8f8a44e29..1abbf2aa494 100644 --- a/src/components/structures/ThreadView.tsx +++ b/src/components/structures/ThreadView.tsx @@ -61,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") @@ -75,14 +77,16 @@ export default class ThreadView extends React.Component { public context!: React.ContextType; private dispatcherRef: string; - private timelinePanelRef = createRef(); - private cardRef = createRef(); private readonly layoutWatcherRef: string; + private timelinePanel = createRef(); + private card = createRef(); constructor(props: IProps) { super(props); + this.state = { layout: SettingsStore.getValue("layout"), + narrow: false, }; this.layoutWatcherRef = SettingsStore.watchSetting("layout", null, (...[,,, value]) => @@ -132,7 +136,7 @@ export default class ThreadView extends React.Component { 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; @@ -182,7 +186,7 @@ export default class ThreadView extends React.Component { if (!thread.initialEventsFetched) { await thread.fetchInitialEvents(); } - this.timelinePanelRef.current?.refreshTimeline(); + this.timelinePanel.current?.refreshTimeline(); }); } }; @@ -210,6 +214,10 @@ export default class ThreadView extends React.Component { } }; + private onMeasurement = (narrow: boolean): void => { + this.setState({ narrow }); + }; + private onKeyDown = (ev: KeyboardEvent) => { let handled = false; @@ -231,15 +239,6 @@ export default class ThreadView extends React.Component { } }; - private renderThreadViewHeader = (): JSX.Element => { - return
- { _t("Thread") } - -
; - }; - private onPaginationRequest = async ( timelineWindow: TimelineWindow | null, direction = Direction.Backward, @@ -285,6 +284,15 @@ export default class ThreadView extends React.Component { }; } + private renderThreadViewHeader = (): JSX.Element => { + return
+ { _t("Thread") } + +
; + }; + public render(): JSX.Element { const highlightedEventId = this.props.isInitialEventHighlighted ? this.props.initialEvent?.getId() @@ -304,58 +312,60 @@ export default class ThreadView extends React.Component { timelineRenderingType: TimelineRenderingType.Thread, threadId: this.state.thread?.id, liveTimeline: this.state?.thread?.timelineSet?.getLiveTimeline(), + narrow: this.state.narrow, }}> - - - { this.state.thread &&
- -
} - - { ContentMessages.sharedInstance().getCurrentUploads(threadRelation).length > 0 && ( - - ) } - - { this.state?.thread?.timelineSet && ( + { this.state.thread &&
+ +
} + + { ContentMessages.sharedInstance().getCurrentUploads(threadRelation).length > 0 && ( + + ) } + + { this.state?.thread?.timelineSet && () }
); diff --git a/src/components/views/elements/Measured.tsx b/src/components/views/elements/Measured.tsx index af4709b1e59..d6c35d91eb3 100644 --- a/src/components/views/elements/Measured.tsx +++ b/src/components/views/elements/Measured.tsx @@ -14,39 +14,45 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { createRef } from "react"; +import React from "react"; -import RoomContext from "../../../contexts/RoomContext"; import UIStore, { UI_EVENTS } from "../../../stores/UIStore"; -const NARROW_BREAKPOINT = 500; - -interface IState { - narrow: boolean; +interface IProps { + sensor: Element; + breakpoint: number; + onMeasurement(narrow: boolean): void; } -export default class Measured extends React.Component<{}, IState> { - static contextType = RoomContext; - +export default class Measured extends React.PureComponent { private static instanceCount = 0; private readonly instanceId: number; - private sensor = createRef(); + static defaultProps = { + breakpoint: 500, + }; - constructor(props, context) { - super(props, context); + constructor(props) { + super(props); this.instanceId = Measured.instanceCount++; - - this.state = { - narrow: false, - }; } componentDidMount() { UIStore.instance.on(`Measured${this.instanceId}`, this.onResize); - UIStore.instance.trackElementDimensions(`Measured${this.instanceId}`, - this.sensor.current); + } + + componentDidUpdate(prevProps: Readonly) { + 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() { @@ -56,18 +62,10 @@ export default class Measured extends React.Component<{}, IState> { private onResize = (type: UI_EVENTS, entry: ResizeObserverEntry) => { if (type !== UI_EVENTS.Resize) return; - this.setState({ - narrow: entry.contentRect.width <= NARROW_BREAKPOINT, - }); + this.props.onMeasurement(entry.contentRect.width <= this.props.breakpoint); }; render() { - return - { this.props.children } -
- ; + return null; } } diff --git a/src/components/views/right_panel/TimelineCard.tsx b/src/components/views/right_panel/TimelineCard.tsx index c0c0447111b..72f3d429694 100644 --- a/src/components/views/right_panel/TimelineCard.tsx +++ b/src/components/views/right_panel/TimelineCard.tsx @@ -56,6 +56,7 @@ interface IProps { showComposer?: boolean; composerRelation?: IEventRelation; } + interface IState { thread?: Thread; editState?: EditorStateTransfer; @@ -64,6 +65,7 @@ interface IState { isInitialEventHighlighted?: boolean; layout: Layout; atEndOfLiveTimeline: boolean; + narrow: boolean; // settings: showReadReceipts?: boolean; @@ -75,7 +77,8 @@ export default class TimelineCard extends React.Component { private dispatcherRef: string; private layoutWatcherRef: string; - private timelinePanelRef: React.RefObject = React.createRef(); + private timelinePanel = React.createRef(); + private card = React.createRef(); private roomStoreToken: EventSubscription; private readReceiptsSettingWatcher: string; @@ -85,6 +88,7 @@ export default class TimelineCard extends React.Component { showReadReceipts: SettingsStore.getValue("showReadReceipts", props.room.roomId), layout: SettingsStore.getValue("layout"), atEndOfLiveTimeline: true, + narrow: false, }; this.readReceiptsSettingWatcher = null; } @@ -135,7 +139,7 @@ export default class TimelineCard extends React.Component { 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; @@ -158,7 +162,7 @@ export default class TimelineCard extends React.Component { }; private onScroll = (): void => { - const timelinePanel = this.timelinePanelRef.current; + const timelinePanel = this.timelinePanel.current; if (!timelinePanel) return; if (timelinePanel.isAtEndOfLiveTimeline()) { this.setState({ @@ -171,6 +175,10 @@ export default class TimelineCard extends React.Component { } }; + 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 @@ -182,7 +190,7 @@ export default class TimelineCard extends React.Component { }); } else { // Otherwise we have to jump manually - this.timelinePanelRef.current?.jumpToLiveTimeline(); + this.timelinePanel.current?.jumpToLiveTimeline(); dis.fire(Action.FocusSendMessageComposer); } }; @@ -218,55 +226,59 @@ export default class TimelineCard extends React.Component { ...this.context, timelineRenderingType: this.props.timelineRenderingType ?? this.context.timelineRenderingType, liveTimeline: this.props.timelineSet.getLiveTimeline(), + narrow: this.state.narrow, }}> - -
- { jumpToBottom } -
- - { isUploading && ( - - ) } - - +
+ { jumpToBottom } +
+ + { isUploading && ( + + ) } + +
);