diff --git a/apps/web/src/components/structures/TimelinePanel.tsx b/apps/web/src/components/structures/TimelinePanel.tsx index aeda2237e0..939c16d96e 100644 --- a/apps/web/src/components/structures/TimelinePanel.tsx +++ b/apps/web/src/components/structures/TimelinePanel.tsx @@ -24,7 +24,7 @@ import { type MatrixClient, type Relations, type MatrixError, - type SyncState, + SyncState, TimelineWindow, Thread, ThreadEvent, @@ -192,9 +192,6 @@ interface IState { backPaginating: boolean; forwardPaginating: boolean; - // cache of matrixClient.getSyncState() (but from the 'sync' event) - clientSyncState: SyncState | null; - // should the event tiles have twelve hour times isTwelveHour: boolean; @@ -251,12 +248,17 @@ class TimelinePanel extends React.Component { // A map of private callEventGroupers = new Map(); private initialReadMarkerId: string | null = null; + private syncImpliesForwardPaginating: boolean; public constructor(props: IProps, context: React.ContextType) { super(props, context); debuglog("mounting"); + this.syncImpliesForwardPaginating = TimelinePanel.isSyncForwardPaginating( + MatrixClientPeg.safeGet().getSyncState(), + ); + // XXX: we could track RM per TimelineSet rather than per Room. // but for now we just do it per room for simplicity. if (this.props.manageReadMarkers) { @@ -278,7 +280,6 @@ class TimelinePanel extends React.Component { readMarkerEventId: this.initialReadMarkerId, backPaginating: false, forwardPaginating: false, - clientSyncState: MatrixClientPeg.safeGet().getSyncState(), isTwelveHour: SettingsStore.getValue("showTwelveHourTimestamps"), alwaysShowTimestamps: SettingsStore.getValue("alwaysShowTimestamps"), readMarkerInViewThresholdMs: SettingsStore.getValue("readMarkerInViewThresholdMs"), @@ -899,9 +900,17 @@ class TimelinePanel extends React.Component { private onSync = (clientSyncState: SyncState, prevState: SyncState | null, data?: object): void => { if (this.unmounted) return; - this.setState({ clientSyncState }); + const nextSyncImpliesForwardPaginating = TimelinePanel.isSyncForwardPaginating(clientSyncState); + if (nextSyncImpliesForwardPaginating === this.syncImpliesForwardPaginating) return; + + this.syncImpliesForwardPaginating = nextSyncImpliesForwardPaginating; + this.forceUpdate(); }; + private static isSyncForwardPaginating(syncState: SyncState | null): boolean { + return syncState === SyncState.Prepared || syncState === SyncState.Catchup; + } + private readMarkerTimeout(readMarkerPosition: number | null): number { return readMarkerPosition === 0 ? (this.context?.readMarkerInViewThresholdMs ?? this.state.readMarkerInViewThresholdMs) @@ -1832,8 +1841,7 @@ class TimelinePanel extends React.Component { // If the state is PREPARED or CATCHUP, we're still waiting for the js-sdk to sync with // the HS and fetch the latest events, so we are effectively forward paginating. - const forwardPaginating = - this.state.forwardPaginating || ["PREPARED", "CATCHUP"].includes(this.state.clientSyncState!); + const forwardPaginating = this.state.forwardPaginating || this.syncImpliesForwardPaginating; const events = this.state.events; return ( { }); }); + it("only re-renders when sync changes forward pagination state", async () => { + const [client, room, events] = setupTestData(); + let timelinePanel: TimelinePanel | null = null; + + render( + { + timelinePanel = ref; + }} + />, + clientAndSDKContextRenderOptions(client, sdkContext), + ); + await flushPromises(); + await waitFor(() => expect(timelinePanel).toBeTruthy()); + + const forceUpdateSpy = jest.spyOn(timelinePanel!, "forceUpdate"); + + await act(async () => { + client.emit(ClientEvent.Sync, SyncState.Syncing, SyncState.Syncing); + await flushPromises(); + }); + + expect(forceUpdateSpy).not.toHaveBeenCalled(); + + await act(async () => { + client.emit(ClientEvent.Sync, SyncState.Prepared, SyncState.Syncing); + await flushPromises(); + }); + + expect(forceUpdateSpy).toHaveBeenCalledTimes(1); + }); + describe("onRoomTimeline", () => { it("ignores events for other timelines", () => { const [client, room, events] = setupTestData();