Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit 79edd9a

Browse files
committed
Enable pagination for overlay timelines
1 parent a81940b commit 79edd9a

File tree

3 files changed

+224
-52
lines changed

3 files changed

+224
-52
lines changed

Diff for: src/components/structures/TimelinePanel.tsx

+137-34
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import { TimelineWindow } from "matrix-js-sdk/src/timeline-window";
2424
import { EventType, RelationType } from "matrix-js-sdk/src/@types/event";
2525
import { SyncState } from "matrix-js-sdk/src/sync";
2626
import { RoomMember, RoomMemberEvent } from "matrix-js-sdk/src/models/room-member";
27-
import { debounce, throttle } from "lodash";
27+
import { debounce, findLastIndex, throttle } from "lodash";
2828
import { logger } from "matrix-js-sdk/src/logger";
2929
import { ClientEvent } from "matrix-js-sdk/src/client";
3030
import { Thread, ThreadEvent } from "matrix-js-sdk/src/models/thread";
@@ -73,6 +73,12 @@ const debuglog = (...args: any[]): void => {
7373
}
7474
};
7575

76+
const overlaysBefore = (overlayEvent: MatrixEvent, mainEvent: MatrixEvent): boolean =>
77+
overlayEvent.localTimestamp < mainEvent.localTimestamp;
78+
79+
const overlaysAfter = (overlayEvent: MatrixEvent, mainEvent: MatrixEvent): boolean =>
80+
overlayEvent.localTimestamp >= mainEvent.localTimestamp;
81+
7682
interface IProps {
7783
// The js-sdk EventTimelineSet object for the timeline sequence we are
7884
// representing. This may or may not have a room, depending on what it's
@@ -83,7 +89,6 @@ interface IProps {
8389
// added to support virtual rooms
8490
// events from the overlay timeline set will be added by localTimestamp
8591
// into the main timeline
86-
// back paging not yet supported
8792
overlayTimelineSet?: EventTimelineSet;
8893
// filter events from overlay timeline
8994
overlayTimelineSetFilter?: (event: MatrixEvent) => boolean;
@@ -506,16 +511,53 @@ class TimelinePanel extends React.Component<IProps, IState> {
506511
// this particular event should be the first or last to be unpaginated.
507512
const eventId = scrollToken;
508513

509-
const marker = this.state.events.findIndex((ev) => {
510-
return ev.getId() === eventId;
511-
});
514+
// The event in question could belong to either the main timeline or
515+
// overlay timeline; let's check both
516+
const mainEvents = this.timelineWindow?.getEvents() ?? [];
517+
const overlayEvents = this.overlayTimelineWindow?.getEvents() ?? [];
518+
519+
let marker = mainEvents.findIndex((ev) => ev.getId() === eventId);
520+
let overlayMarker: number;
521+
if (marker === -1) {
522+
// The event must be from the overlay timeline instead
523+
overlayMarker = overlayEvents.findIndex((ev) => ev.getId() === eventId);
524+
marker = backwards
525+
? findLastIndex(mainEvents, (ev) => overlaysAfter(overlayEvents[overlayMarker], ev))
526+
: mainEvents.findIndex((ev) => overlaysBefore(overlayEvents[overlayMarker], ev));
527+
} else {
528+
overlayMarker = backwards
529+
? findLastIndex(overlayEvents, (ev) => overlaysBefore(ev, mainEvents[marker]))
530+
: overlayEvents.findIndex((ev) => overlaysAfter(ev, mainEvents[marker]));
531+
}
532+
533+
// The number of events to unpaginate from the main timeline
534+
let count: number;
535+
if (marker === -1) {
536+
count = 0;
537+
} else {
538+
count = backwards ? marker + 1 : mainEvents.length - marker;
539+
}
512540

513-
const count = backwards ? marker + 1 : this.state.events.length - marker;
541+
// The number of events to unpaginate from the overlay timeline
542+
let overlayCount: number;
543+
if (overlayMarker === -1) {
544+
overlayCount = 0;
545+
} else {
546+
overlayCount = backwards ? overlayMarker + 1 : overlayEvents.length - overlayMarker;
547+
}
514548

515549
if (count > 0) {
516550
debuglog("Unpaginating", count, "in direction", dir);
517551
this.timelineWindow?.unpaginate(count, backwards);
552+
}
518553

554+
if (overlayCount > 0) {
555+
debuglog("Unpaginating", count, "from overlay timeline in direction", dir);
556+
this.overlayTimelineWindow?.unpaginate(overlayCount, backwards);
557+
}
558+
559+
// If either timeline window shrunk
560+
if (count > 0 || overlayCount > 0) {
519561
const { events, liveEvents, firstVisibleEventIndex } = this.getEvents();
520562
this.buildLegacyCallEventGroupers(events);
521563
this.setState({
@@ -572,11 +614,15 @@ class TimelinePanel extends React.Component<IProps, IState> {
572614
debuglog("Initiating paginate; backwards:" + backwards);
573615
this.setState<null>({ [paginatingKey]: true });
574616

575-
return this.onPaginationRequest(this.timelineWindow, dir, PAGINATE_SIZE).then((r) => {
617+
return this.onPaginationRequest(this.timelineWindow, dir, PAGINATE_SIZE).then(async (r) => {
576618
if (this.unmounted) {
577619
return false;
578620
}
579621

622+
if (this.overlayTimelineWindow) {
623+
await this.extendOverlayWindowToCoverMainWindow();
624+
}
625+
580626
debuglog("paginate complete backwards:" + backwards + "; success:" + r);
581627

582628
const { events, liveEvents, firstVisibleEventIndex } = this.getEvents();
@@ -769,8 +815,12 @@ class TimelinePanel extends React.Component<IProps, IState> {
769815
});
770816
};
771817

818+
private hasTimelineSetFor(roomId: string): boolean {
819+
return roomId === this.props.timelineSet.room?.roomId || roomId === this.props.overlayTimelineSet?.room?.roomId;
820+
}
821+
772822
private onRoomTimelineReset = (room: Room, timelineSet: EventTimelineSet): void => {
773-
if (timelineSet !== this.props.timelineSet) return;
823+
if (timelineSet !== this.props.timelineSet && timelineSet !== this.props.overlayTimelineSet) return;
774824

775825
if (this.canResetTimeline()) {
776826
this.loadTimeline();
@@ -783,7 +833,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
783833
if (this.unmounted) return;
784834

785835
// ignore events for other rooms
786-
if (room !== this.props.timelineSet.room) return;
836+
if (!this.hasTimelineSetFor(room.roomId)) return;
787837

788838
// we could skip an update if the event isn't in our timeline,
789839
// but that's probably an early optimisation.
@@ -796,10 +846,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
796846
}
797847

798848
// ignore events for other rooms
799-
const roomId = thread.roomId;
800-
if (roomId !== this.props.timelineSet.room?.roomId) {
801-
return;
802-
}
849+
if (!this.hasTimelineSetFor(thread.roomId)) return;
803850

804851
// we could skip an update if the event isn't in our timeline,
805852
// but that's probably an early optimisation.
@@ -818,9 +865,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
818865

819866
// ignore events for other rooms
820867
const roomId = ev.getRoomId();
821-
if (roomId !== this.props.timelineSet.room?.roomId) {
822-
return;
823-
}
868+
if (roomId === undefined || !this.hasTimelineSetFor(roomId)) return;
824869

825870
// we could skip an update if the event isn't in our timeline,
826871
// but that's probably an early optimisation.
@@ -834,7 +879,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
834879
if (this.unmounted) return;
835880

836881
// ignore events for other rooms
837-
if (member.roomId !== this.props.timelineSet.room?.roomId) return;
882+
if (!this.hasTimelineSetFor(member.roomId)) return;
838883

839884
// ignore events for other users
840885
if (member.userId != MatrixClientPeg.get().credentials?.userId) return;
@@ -857,7 +902,8 @@ class TimelinePanel extends React.Component<IProps, IState> {
857902
if (this.unmounted) return;
858903

859904
// ignore events for other rooms
860-
if (replacedEvent.getRoomId() !== this.props.timelineSet.room?.roomId) return;
905+
const roomId = replacedEvent.getRoomId();
906+
if (roomId === undefined || !this.hasTimelineSetFor(roomId)) return;
861907

862908
// we could skip an update if the event isn't in our timeline,
863909
// but that's probably an early optimisation.
@@ -877,7 +923,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
877923
if (this.unmounted) return;
878924

879925
// ignore events for other rooms
880-
if (room !== this.props.timelineSet.room) return;
926+
if (!this.hasTimelineSetFor(room.roomId)) return;
881927

882928
this.reloadEvents();
883929
};
@@ -905,7 +951,8 @@ class TimelinePanel extends React.Component<IProps, IState> {
905951
// Can be null for the notification timeline, etc.
906952
if (!this.props.timelineSet.room) return;
907953

908-
if (ev.getRoomId() !== this.props.timelineSet.room.roomId) return;
954+
const roomId = ev.getRoomId();
955+
if (roomId === undefined || !this.hasTimelineSetFor(roomId)) return;
909956

910957
if (!this.state.events.includes(ev)) return;
911958

@@ -1380,6 +1427,48 @@ class TimelinePanel extends React.Component<IProps, IState> {
13801427
});
13811428
}
13821429

1430+
private async extendOverlayWindowToCoverMainWindow(): Promise<void> {
1431+
const mainWindow = this.timelineWindow!;
1432+
const overlayWindow = this.overlayTimelineWindow!;
1433+
const mainEvents = mainWindow.getEvents();
1434+
1435+
if (mainEvents.length > 0) {
1436+
let paginationRequests: Promise<unknown>[];
1437+
1438+
// Keep paginating until the main window is covered
1439+
do {
1440+
paginationRequests = [];
1441+
const overlayEvents = overlayWindow.getEvents();
1442+
1443+
if (
1444+
overlayWindow.canPaginate(EventTimeline.BACKWARDS) &&
1445+
(overlayEvents.length === 0 ||
1446+
overlaysAfter(overlayEvents[0], mainEvents[0]) ||
1447+
!mainWindow.canPaginate(EventTimeline.BACKWARDS))
1448+
) {
1449+
// Paginating backwards could reveal more events to be overlaid in the main window
1450+
paginationRequests.push(
1451+
this.onPaginationRequest(overlayWindow, EventTimeline.BACKWARDS, PAGINATE_SIZE),
1452+
);
1453+
}
1454+
1455+
if (
1456+
overlayWindow.canPaginate(EventTimeline.FORWARDS) &&
1457+
(overlayEvents.length === 0 ||
1458+
overlaysBefore(overlayEvents.at(-1)!, mainEvents.at(-1)!) ||
1459+
!mainWindow.canPaginate(EventTimeline.FORWARDS))
1460+
) {
1461+
// Paginating forwards could reveal more events to be overlaid in the main window
1462+
paginationRequests.push(
1463+
this.onPaginationRequest(overlayWindow, EventTimeline.FORWARDS, PAGINATE_SIZE),
1464+
);
1465+
}
1466+
1467+
await Promise.all(paginationRequests);
1468+
} while (paginationRequests.length > 0);
1469+
}
1470+
}
1471+
13831472
/**
13841473
* (re)-load the event timeline, and initialise the scroll state, centered
13851474
* around the given event.
@@ -1417,8 +1506,14 @@ class TimelinePanel extends React.Component<IProps, IState> {
14171506

14181507
this.setState(
14191508
{
1420-
canBackPaginate: !!this.timelineWindow?.canPaginate(EventTimeline.BACKWARDS),
1421-
canForwardPaginate: !!this.timelineWindow?.canPaginate(EventTimeline.FORWARDS),
1509+
canBackPaginate:
1510+
(this.timelineWindow?.canPaginate(EventTimeline.BACKWARDS) ||
1511+
this.overlayTimelineWindow?.canPaginate(EventTimeline.BACKWARDS)) ??
1512+
false,
1513+
canForwardPaginate:
1514+
(this.timelineWindow?.canPaginate(EventTimeline.FORWARDS) ||
1515+
this.overlayTimelineWindow?.canPaginate(EventTimeline.FORWARDS)) ??
1516+
false,
14221517
timelineLoading: false,
14231518
},
14241519
() => {
@@ -1494,21 +1589,21 @@ class TimelinePanel extends React.Component<IProps, IState> {
14941589
// This is a hot-path optimization by skipping a promise tick
14951590
// by repeating a no-op sync branch in
14961591
// TimelineSet.getTimelineForEvent & MatrixClient.getEventTimeline
1497-
if (this.props.timelineSet.getTimelineForEvent(eventId)) {
1592+
if (this.props.timelineSet.getTimelineForEvent(eventId) && !this.overlayTimelineWindow) {
14981593
// if we've got an eventId, and the timeline exists, we can skip
14991594
// the promise tick.
15001595
this.timelineWindow.load(eventId, INITIAL_SIZE);
1501-
this.overlayTimelineWindow?.load(undefined, INITIAL_SIZE);
15021596
// in this branch this method will happen in sync time
15031597
onLoaded();
15041598
return;
15051599
}
15061600

15071601
const prom = this.timelineWindow.load(eventId, INITIAL_SIZE).then(async (): Promise<void> => {
15081602
if (this.overlayTimelineWindow) {
1509-
// @TODO(kerrya) use timestampToEvent to load the overlay timeline
1603+
// TODO: use timestampToEvent to load the overlay timeline
15101604
// with more correct position when main TL eventId is truthy
15111605
await this.overlayTimelineWindow.load(undefined, INITIAL_SIZE);
1606+
await this.extendOverlayWindowToCoverMainWindow();
15121607
}
15131608
});
15141609
this.buildLegacyCallEventGroupers();
@@ -1541,23 +1636,31 @@ class TimelinePanel extends React.Component<IProps, IState> {
15411636
this.reloadEvents();
15421637
}
15431638

1544-
// get the list of events from the timeline window and the pending event list
1639+
// get the list of events from the timeline windows and the pending event list
15451640
private getEvents(): Pick<IState, "events" | "liveEvents" | "firstVisibleEventIndex"> {
1546-
const mainEvents: MatrixEvent[] = this.timelineWindow?.getEvents() || [];
1547-
const eventFilter = this.props.overlayTimelineSetFilter || Boolean;
1548-
const overlayEvents = this.overlayTimelineWindow?.getEvents().filter(eventFilter) || [];
1641+
const mainEvents = this.timelineWindow?.getEvents() ?? [];
1642+
let overlayEvents = this.overlayTimelineWindow?.getEvents() ?? [];
1643+
if (this.props.overlayTimelineSetFilter !== undefined) {
1644+
overlayEvents = overlayEvents.filter(this.props.overlayTimelineSetFilter);
1645+
}
15491646

15501647
// maintain the main timeline event order as returned from the HS
15511648
// merge overlay events at approximately the right position based on local timestamp
15521649
const events = overlayEvents.reduce(
15531650
(acc: MatrixEvent[], overlayEvent: MatrixEvent) => {
15541651
// find the first main tl event with a later timestamp
1555-
const index = acc.findIndex((event) => event.localTimestamp > overlayEvent.localTimestamp);
1652+
const index = acc.findIndex((event) => overlaysBefore(overlayEvent, event));
15561653
// insert overlay event into timeline at approximately the right place
1557-
if (index > -1) {
1558-
acc.splice(index, 0, overlayEvent);
1654+
if (index === -1) {
1655+
if (!this.timelineWindow?.canPaginate(EventTimeline.FORWARDS)) {
1656+
acc.push(overlayEvent);
1657+
}
1658+
} else if (index === 0) {
1659+
if (!this.timelineWindow?.canPaginate(EventTimeline.BACKWARDS)) {
1660+
acc.unshift(overlayEvent);
1661+
}
15591662
} else {
1560-
acc.push(overlayEvent);
1663+
acc.splice(index, 0, overlayEvent);
15611664
}
15621665
return acc;
15631666
},
@@ -1574,7 +1677,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
15741677
client.decryptEventIfNeeded(event);
15751678
});
15761679

1577-
const firstVisibleEventIndex = this.checkForPreJoinUISI(mainEvents);
1680+
const firstVisibleEventIndex = this.checkForPreJoinUISI(events);
15781681

15791682
// Hold onto the live events separately. The read receipt and read marker
15801683
// should use this list, so that they don't advance into pending events.

0 commit comments

Comments
 (0)