/* Copyright 2024 New Vector Ltd. Copyright 2021, 2022 The Matrix.org Foundation C.I.C. SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ import React from "react"; import { IEventRelation, MatrixEvent, NotificationCountType, Room, EventTimelineSet, Thread, } from "matrix-js-sdk/src/matrix"; import { KnownMembership } from "matrix-js-sdk/src/types"; import BaseCard from "./BaseCard"; import ResizeNotifier from "../../../utils/ResizeNotifier"; import MessageComposer from "../rooms/MessageComposer"; import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks"; import { Layout } from "../../../settings/enums/Layout"; import TimelinePanel from "../../structures/TimelinePanel"; import { E2EStatus } from "../../../utils/ShieldUtils"; import EditorStateTransfer from "../../../utils/EditorStateTransfer"; import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext"; import dis from "../../../dispatcher/dispatcher"; import { _t } from "../../../languageHandler"; import { ActionPayload } from "../../../dispatcher/payloads"; import { Action } from "../../../dispatcher/actions"; import ContentMessages from "../../../ContentMessages"; 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"; import { UPDATE_EVENT } from "../../../stores/AsyncStore"; import { SdkContextClass } from "../../../contexts/SDKContext"; import { ScopedRoomContextProvider } from "../../../contexts/ScopedRoomContext.tsx"; interface IProps { room: Room; onClose: () => void; resizeNotifier: ResizeNotifier; permalinkCreator: RoomPermalinkCreator; e2eStatus?: E2EStatus; classNames?: string; timelineSet: EventTimelineSet; timelineRenderingType?: TimelineRenderingType; showComposer?: boolean; composerRelation?: IEventRelation; } interface IState { thread?: Thread; editState?: EditorStateTransfer; replyToEvent?: MatrixEvent; initialEventId?: string; isInitialEventHighlighted?: boolean; layout: Layout; atEndOfLiveTimeline: boolean; narrow: boolean; // settings: showReadReceipts?: boolean; } export default class TimelineCard extends React.Component { public static contextType = RoomContext; public declare context: React.ContextType; private dispatcherRef?: string; private layoutWatcherRef?: string; private timelinePanel = React.createRef(); private card = React.createRef(); private readReceiptsSettingWatcher: string | undefined; public constructor(props: IProps, context: React.ContextType) { super(props, context); this.state = { showReadReceipts: SettingsStore.getValue("showReadReceipts", props.room.roomId), layout: SettingsStore.getValue("layout"), atEndOfLiveTimeline: true, narrow: false, }; } public componentDidMount(): void { SdkContextClass.instance.roomViewStore.addListener(UPDATE_EVENT, this.onRoomViewStoreUpdate); this.dispatcherRef = dis.register(this.onAction); this.readReceiptsSettingWatcher = SettingsStore.watchSetting("showReadReceipts", null, (...[, , , value]) => this.setState({ showReadReceipts: value as boolean }), ); this.layoutWatcherRef = SettingsStore.watchSetting("layout", null, (...[, , , value]) => this.setState({ layout: value as Layout }), ); } public componentWillUnmount(): void { SdkContextClass.instance.roomViewStore.removeListener(UPDATE_EVENT, this.onRoomViewStoreUpdate); SettingsStore.unwatchSetting(this.readReceiptsSettingWatcher); SettingsStore.unwatchSetting(this.layoutWatcherRef); dis.unregister(this.dispatcherRef); } private onRoomViewStoreUpdate = async (_initial?: boolean): Promise => { const newState: Pick = { initialEventId: SdkContextClass.instance.roomViewStore.getInitialEventId(), isInitialEventHighlighted: SdkContextClass.instance.roomViewStore.isInitialEventHighlighted(), replyToEvent: SdkContextClass.instance.roomViewStore.getQuotingEvent(), }; this.setState(newState); }; private onAction = (payload: ActionPayload): void => { switch (payload.action) { case Action.EditEvent: this.setState( { editState: payload.event ? new EditorStateTransfer(payload.event) : undefined, }, () => { if (payload.event) { this.timelinePanel.current?.scrollToEventIfNeeded(payload.event.getId()); } }, ); break; default: break; } }; private onScroll = (): void => { const timelinePanel = this.timelinePanel.current; if (!timelinePanel) return; if (timelinePanel.isAtEndOfLiveTimeline()) { this.setState({ atEndOfLiveTimeline: true, }); } else { this.setState({ atEndOfLiveTimeline: false, }); } if (this.state.initialEventId && this.state.isInitialEventHighlighted) { dis.dispatch({ action: Action.ViewRoom, room_id: this.props.room.roomId, event_id: this.state.initialEventId, highlighted: false, replyingToEvent: this.state.replyToEvent, metricsTrigger: undefined, // room doesn't change }); } }; private onMeasurement = (narrow: boolean): void => { this.setState({ narrow }); }; private jumpToLiveTimeline = (): void => { if (this.state.initialEventId && this.state.isInitialEventHighlighted) { // If we were viewing a highlighted event, firing view_room without // an event will take care of both clearing the URL fragment and // jumping to the bottom dis.dispatch({ action: Action.ViewRoom, room_id: this.props.room.roomId, }); } else { // Otherwise we have to jump manually this.timelinePanel.current?.jumpToLiveTimeline(); dis.fire(Action.FocusSendMessageComposer); } }; public render(): React.ReactNode { const highlightedEventId = this.state.isInitialEventHighlighted ? this.state.initialEventId : undefined; let jumpToBottom; if (!this.state.atEndOfLiveTimeline) { jumpToBottom = ( 0} onScrollToBottomClick={this.jumpToLiveTimeline} /> ); } const isUploading = ContentMessages.sharedInstance().getCurrentUploads(this.props.composerRelation).length > 0; const myMembership = this.props.room.getMyMembership(); const showComposer = myMembership === KnownMembership.Join; return ( {this.card.current && }
{jumpToBottom}
{isUploading && } {showComposer && ( )}
); } }