diff --git a/src/MatrixClientPeg.ts b/src/MatrixClientPeg.ts index 1a92f0f058..978939e707 100644 --- a/src/MatrixClientPeg.ts +++ b/src/MatrixClientPeg.ts @@ -295,6 +295,7 @@ class MatrixClientPegClass implements IMatrixClientPeg { const notifTimelineSet = new EventTimelineSet(null, { timelineSupport: true, + pendingEvents: false, }); // XXX: what is our initial pagination token?! it somehow needs to be synchronised with /sync. notifTimelineSet.getLiveTimeline().setPaginationToken("", EventTimeline.BACKWARDS); diff --git a/src/components/structures/RightPanel.tsx b/src/components/structures/RightPanel.tsx index 29aafd16ff..144f17ad1f 100644 --- a/src/components/structures/RightPanel.tsx +++ b/src/components/structures/RightPanel.tsx @@ -75,6 +75,8 @@ interface IState { groupRoomId?: string; groupId?: string; event: MatrixEvent; + initialEvent?: MatrixEvent; + initialEventHighlighted?: boolean; } @replaceableComponent("structures.RightPanel") @@ -209,6 +211,8 @@ export default class RightPanel extends React.Component { groupId: payload.groupId, member: payload.member, event: payload.event, + initialEvent: payload.initialEvent, + initialEventHighlighted: payload.highlighted, verificationRequest: payload.verificationRequest, verificationRequestPromise: payload.verificationRequestPromise, widgetId: payload.widgetId, @@ -244,7 +248,7 @@ export default class RightPanel extends React.Component { } }; - render() { + public render(): JSX.Element { let panel =
; const roomId = this.props.room ? this.props.room.roomId : undefined; @@ -327,6 +331,8 @@ export default class RightPanel extends React.Component { resizeNotifier={this.props.resizeNotifier} onClose={this.onClose} mxEvent={this.state.event} + initialEvent={this.state.initialEvent} + initialEventHighlighted={this.state.initialEventHighlighted} permalinkCreator={this.props.permalinkCreator} e2eStatus={this.props.e2eStatus} />; break; diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 7af67a8267..6338ce0c60 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -92,6 +92,8 @@ import SpaceStore from "../../stores/SpaceStore"; import { logger } from "matrix-js-sdk/src/logger"; import { EventTimeline } from 'matrix-js-sdk/src/models/event-timeline'; +import { dispatchShowThreadEvent } from '../../dispatcher/dispatch-actions/threads'; +import { fetchInitialEvent } from "../../utils/EventUtils"; const DEBUG = false; let debuglog = function(msg: string) {}; @@ -321,7 +323,7 @@ export class RoomView extends React.Component { }); }; - private onRoomViewStoreUpdate = (initial?: boolean) => { + private onRoomViewStoreUpdate = async (initial?: boolean): Promise => { if (this.unmounted) { return; } @@ -349,8 +351,6 @@ export class RoomView extends React.Component { roomLoading: RoomViewStore.isRoomLoading(), roomLoadError: RoomViewStore.getRoomLoadError(), joining: RoomViewStore.isJoining(), - initialEventId: RoomViewStore.getInitialEventId(), - isInitialEventHighlighted: RoomViewStore.isInitialEventHighlighted(), replyToEvent: RoomViewStore.getQuotingEvent(), // we should only peek once we have a ready client shouldPeek: this.state.matrixClientIsReady && RoomViewStore.shouldPeek(), @@ -362,6 +362,39 @@ export class RoomView extends React.Component { wasContextSwitch: RoomViewStore.getWasContextSwitch(), }; + const initialEventId = RoomViewStore.getInitialEventId(); + if (initialEventId) { + const room = this.context.getRoom(roomId); + let initialEvent = room?.findEventById(initialEventId); + // The event does not exist in the current sync data + // We need to fetch it to know whether to route this request + // to the main timeline or to a threaded one + // In the current state, if a thread does not exist in the sync data + // We will only display the event targeted by the `matrix.to` link + // and the root event. + // The rest will be lost for now, until the aggregation API on the server + // becomes available to fetch a whole thread + if (!initialEvent) { + initialEvent = await fetchInitialEvent( + this.context, + roomId, + initialEventId, + ); + } + + const thread = initialEvent?.getThread(); + if (thread && !initialEvent?.isThreadRoot) { + dispatchShowThreadEvent( + thread.rootEvent, + initialEvent, + RoomViewStore.isInitialEventHighlighted(), + ); + } else { + newState.initialEventId = initialEventId; + newState.isInitialEventHighlighted = RoomViewStore.isInitialEventHighlighted(); + } + } + // Add watchers for each of the settings we just looked up this.settingWatchers = this.settingWatchers.concat([ SettingsStore.watchSetting("showReadReceipts", roomId, (...[,,, value]) => diff --git a/src/components/structures/ThreadPanel.tsx b/src/components/structures/ThreadPanel.tsx index 4e278f5cce..c3db17e193 100644 --- a/src/components/structures/ThreadPanel.tsx +++ b/src/components/structures/ThreadPanel.tsx @@ -67,6 +67,7 @@ const useFilteredThreadsTimelinePanel = ({ const timelineSet = useMemo(() => new EventTimelineSet(null, { timelineSupport: true, unstableClientRelationAggregation: true, + pendingEvents: false, }), []); useEffect(() => { diff --git a/src/components/structures/ThreadView.tsx b/src/components/structures/ThreadView.tsx index b0851665a7..c7a4342449 100644 --- a/src/components/structures/ThreadView.tsx +++ b/src/components/structures/ThreadView.tsx @@ -45,6 +45,8 @@ interface IProps { mxEvent: MatrixEvent; permalinkCreator?: RoomPermalinkCreator; e2eStatus?: E2EStatus; + initialEvent?: MatrixEvent; + initialEventHighlighted?: boolean; } interface IState { @@ -102,19 +104,19 @@ export default class ThreadView extends React.Component { } } switch (payload.action) { - case Action.EditEvent: { + case Action.EditEvent: // Quit early if it's not a thread context if (payload.timelineRenderingType !== TimelineRenderingType.Thread) return; // Quit early if that's not a thread event if (payload.event && !payload.event.getThread()) return; - const editState = payload.event ? new EditorStateTransfer(payload.event) : null; - this.setState({ editState }, () => { + this.setState({ + editState: payload.event ? new EditorStateTransfer(payload.event) : null, + }, () => { if (payload.event) { this.timelinePanelRef.current?.scrollToEventIfNeeded(payload.event.getId()); } }); break; - } case 'reply_to_event': if (payload.context === TimelineRenderingType.Thread) { this.setState({ @@ -131,7 +133,11 @@ export default class ThreadView extends React.Component { let thread = mxEv.getThread(); if (!thread) { const client = MatrixClientPeg.get(); - thread = new Thread([mxEv], this.props.room, client); + thread = new Thread( + [mxEv], + this.props.room, + client, + ); mxEv.setThread(thread); } thread.on(ThreadEvent.Update, this.updateThread); @@ -163,7 +169,22 @@ export default class ThreadView extends React.Component { this.timelinePanelRef.current?.refreshTimeline(); }; + private onScroll = (): void => { + if (this.props.initialEvent && this.props.initialEventHighlighted) { + dis.dispatch({ + action: 'view_room', + room_id: this.props.room.roomId, + event_id: this.props.initialEvent?.getId(), + highlighted: false, + replyingToEvent: this.state.replyToEvent, + }); + } + }; + public render(): JSX.Element { + const highlightedEventId = this.props.initialEventHighlighted + ? this.props.initialEvent?.getId() + : null; return ( { permalinkCreator={this.props.permalinkCreator} membersLoaded={true} editState={this.state.editState} + eventId={this.props.initialEvent?.getId()} + highlightedEventId={highlightedEventId} + onUserScroll={this.onScroll} /> ) } diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index db76091025..26b25699c0 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -574,7 +574,9 @@ export default class EventTile extends React.Component {
{ - dispatchShowThreadEvent(this.props.mxEvent); + dispatchShowThreadEvent( + this.props.mxEvent, + ); }} > diff --git a/src/dispatcher/dispatch-actions/threads.ts b/src/dispatcher/dispatch-actions/threads.ts index cda2f55707..31d5ba5bf7 100644 --- a/src/dispatcher/dispatch-actions/threads.ts +++ b/src/dispatcher/dispatch-actions/threads.ts @@ -19,12 +19,18 @@ import { Action } from "../actions"; import dis from '../dispatcher'; import { SetRightPanelPhasePayload } from "../payloads/SetRightPanelPhasePayload"; -export const dispatchShowThreadEvent = (event: MatrixEvent) => { +export const dispatchShowThreadEvent = ( + rootEvent: MatrixEvent, + initialEvent?: MatrixEvent, + highlighted?: boolean, +) => { dis.dispatch({ action: Action.SetRightPanelPhase, phase: RightPanelPhases.ThreadView, refireParams: { - event, + event: rootEvent, + initialEvent, + highlighted, }, }); }; diff --git a/src/utils/EventUtils.ts b/src/utils/EventUtils.ts index e16e58e0d2..894dcb3955 100644 --- a/src/utils/EventUtils.ts +++ b/src/utils/EventUtils.ts @@ -21,6 +21,9 @@ import shouldHideEvent from "../shouldHideEvent"; import { getHandlerTile, haveTileForEvent } from "../components/views/rooms/EventTile"; import SettingsStore from "../settings/SettingsStore"; import { EventType } from "matrix-js-sdk/src/@types/event"; +import { MatrixClient } from 'matrix-js-sdk/src/client'; +import { Thread } from 'matrix-js-sdk/src/models/thread'; +import { logger } from 'matrix-js-sdk/src/logger'; /** * Returns whether an event should allow actions like reply, reactions, edit, etc. @@ -158,3 +161,37 @@ export function isVoiceMessage(mxEvent: MatrixEvent): boolean { !!content['org.matrix.msc3245.voice'] ); } + +export async function fetchInitialEvent( + client: MatrixClient, + roomId: string, + eventId: string): Promise { + let initialEvent: MatrixEvent; + + try { + const eventData = await client.fetchRoomEvent(roomId, eventId); + initialEvent = new MatrixEvent(eventData); + } catch (e) { + logger.warn("Could not find initial event: " + initialEvent.threadRootId); + initialEvent = null; + } + + if (initialEvent?.isThreadRelation) { + try { + const rootEventData = await client.fetchRoomEvent(roomId, initialEvent.threadRootId); + const rootEvent = new MatrixEvent(rootEventData); + const room = client.getRoom(roomId); + const thread = new Thread( + [rootEvent], + room, + client, + ); + thread.addEvent(initialEvent); + room.threads.set(thread.id, thread); + } catch (e) { + logger.warn("Could not find root event: " + initialEvent.threadRootId); + } + } + + return initialEvent; +}