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

Commit 87e2274

Browse files
authored
Enable pagination for overlay timelines (#10757)
* Update @types/jest to 29.2.6 This adds the correct types for the contexts field on mock objects, which I'll need shortly * Enable pagination for overlay timelines
1 parent a597da2 commit 87e2274

5 files changed

Lines changed: 518 additions & 79 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@
156156
"@types/fs-extra": "^11.0.0",
157157
"@types/geojson": "^7946.0.8",
158158
"@types/glob-to-regexp": "^0.4.1",
159-
"@types/jest": "29.2.5",
159+
"@types/jest": "29.2.6",
160160
"@types/katex": "^0.16.0",
161161
"@types/lodash": "^4.14.168",
162162
"@types/modernizr": "^3.5.3",

src/components/structures/TimelinePanel.tsx

Lines changed: 152 additions & 50 deletions
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,30 +511,64 @@ 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);
517-
this.timelineWindow?.unpaginate(count, backwards);
551+
this.timelineWindow!.unpaginate(count, backwards);
552+
}
518553

519-
const { events, liveEvents, firstVisibleEventIndex } = this.getEvents();
520-
this.buildLegacyCallEventGroupers(events);
521-
this.setState({
522-
events,
523-
liveEvents,
524-
firstVisibleEventIndex,
525-
});
554+
if (overlayCount > 0) {
555+
debuglog("Unpaginating", count, "from overlay timeline in direction", dir);
556+
this.overlayTimelineWindow!.unpaginate(overlayCount, backwards);
557+
}
526558

527-
// We can now paginate in the unpaginated direction
528-
if (backwards) {
529-
this.setState({ canBackPaginate: true });
530-
} else {
531-
this.setState({ canForwardPaginate: true });
532-
}
559+
const { events, liveEvents, firstVisibleEventIndex } = this.getEvents();
560+
this.buildLegacyCallEventGroupers(events);
561+
this.setState({
562+
events,
563+
liveEvents,
564+
firstVisibleEventIndex,
565+
});
566+
567+
// We can now paginate in the unpaginated direction
568+
if (backwards) {
569+
this.setState({ canBackPaginate: true });
570+
} else {
571+
this.setState({ canForwardPaginate: true });
533572
}
534573
};
535574

@@ -572,11 +611,15 @@ class TimelinePanel extends React.Component<IProps, IState> {
572611
debuglog("Initiating paginate; backwards:" + backwards);
573612
this.setState<null>({ [paginatingKey]: true });
574613

575-
return this.onPaginationRequest(this.timelineWindow, dir, PAGINATE_SIZE).then((r) => {
614+
return this.onPaginationRequest(this.timelineWindow, dir, PAGINATE_SIZE).then(async (r) => {
576615
if (this.unmounted) {
577616
return false;
578617
}
579618

619+
if (this.overlayTimelineWindow) {
620+
await this.extendOverlayWindowToCoverMainWindow();
621+
}
622+
580623
debuglog("paginate complete backwards:" + backwards + "; success:" + r);
581624

582625
const { events, liveEvents, firstVisibleEventIndex } = this.getEvents();
@@ -769,8 +812,15 @@ class TimelinePanel extends React.Component<IProps, IState> {
769812
});
770813
};
771814

815+
private hasTimelineSetFor(roomId: string | undefined): boolean {
816+
return (
817+
(roomId !== undefined && roomId === this.props.timelineSet.room?.roomId) ||
818+
roomId === this.props.overlayTimelineSet?.room?.roomId
819+
);
820+
}
821+
772822
private onRoomTimelineReset = (room: Room | undefined, 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.
@@ -817,10 +864,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
817864
}
818865

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

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

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

839883
// ignore events for other users
840884
if (member.userId != MatrixClientPeg.get().credentials?.userId) return;
@@ -857,7 +901,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
857901
if (this.unmounted) return;
858902

859903
// ignore events for other rooms
860-
if (replacedEvent.getRoomId() !== this.props.timelineSet.room?.roomId) return;
904+
if (!this.hasTimelineSetFor(replacedEvent.getRoomId())) return;
861905

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

879923
// ignore events for other rooms
880-
if (room !== this.props.timelineSet.room) return;
924+
if (!this.hasTimelineSetFor(room.roomId)) return;
881925

882926
this.reloadEvents();
883927
};
@@ -905,7 +949,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
905949
// Can be null for the notification timeline, etc.
906950
if (!this.props.timelineSet.room) return;
907951

908-
if (ev.getRoomId() !== this.props.timelineSet.room.roomId) return;
952+
if (!this.hasTimelineSetFor(ev.getRoomId())) return;
909953

910954
if (!this.state.events.includes(ev)) return;
911955

@@ -1380,6 +1424,48 @@ class TimelinePanel extends React.Component<IProps, IState> {
13801424
});
13811425
}
13821426

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

14181504
this.setState(
14191505
{
1420-
canBackPaginate: !!this.timelineWindow?.canPaginate(EventTimeline.BACKWARDS),
1421-
canForwardPaginate: !!this.timelineWindow?.canPaginate(EventTimeline.FORWARDS),
1506+
canBackPaginate:
1507+
(this.timelineWindow?.canPaginate(EventTimeline.BACKWARDS) ||
1508+
this.overlayTimelineWindow?.canPaginate(EventTimeline.BACKWARDS)) ??
1509+
false,
1510+
canForwardPaginate:
1511+
(this.timelineWindow?.canPaginate(EventTimeline.FORWARDS) ||
1512+
this.overlayTimelineWindow?.canPaginate(EventTimeline.FORWARDS)) ??
1513+
false,
14221514
timelineLoading: false,
14231515
},
14241516
() => {
@@ -1494,21 +1586,21 @@ class TimelinePanel extends React.Component<IProps, IState> {
14941586
// This is a hot-path optimization by skipping a promise tick
14951587
// by repeating a no-op sync branch in
14961588
// TimelineSet.getTimelineForEvent & MatrixClient.getEventTimeline
1497-
if (this.props.timelineSet.getTimelineForEvent(eventId)) {
1589+
if (this.props.timelineSet.getTimelineForEvent(eventId) && !this.overlayTimelineWindow) {
14981590
// if we've got an eventId, and the timeline exists, we can skip
14991591
// the promise tick.
15001592
this.timelineWindow.load(eventId, INITIAL_SIZE);
1501-
this.overlayTimelineWindow?.load(undefined, INITIAL_SIZE);
15021593
// in this branch this method will happen in sync time
15031594
onLoaded();
15041595
return;
15051596
}
15061597

15071598
const prom = this.timelineWindow.load(eventId, INITIAL_SIZE).then(async (): Promise<void> => {
15081599
if (this.overlayTimelineWindow) {
1509-
// @TODO(kerrya) use timestampToEvent to load the overlay timeline
1600+
// TODO: use timestampToEvent to load the overlay timeline
15101601
// with more correct position when main TL eventId is truthy
15111602
await this.overlayTimelineWindow.load(undefined, INITIAL_SIZE);
1603+
await this.extendOverlayWindowToCoverMainWindow();
15121604
}
15131605
});
15141606
this.buildLegacyCallEventGroupers();
@@ -1541,23 +1633,33 @@ class TimelinePanel extends React.Component<IProps, IState> {
15411633
this.reloadEvents();
15421634
}
15431635

1544-
// get the list of events from the timeline window and the pending event list
1636+
// get the list of events from the timeline windows and the pending event list
15451637
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) || [];
1638+
const mainEvents = this.timelineWindow!.getEvents();
1639+
let overlayEvents = this.overlayTimelineWindow?.getEvents() ?? [];
1640+
if (this.props.overlayTimelineSetFilter !== undefined) {
1641+
overlayEvents = overlayEvents.filter(this.props.overlayTimelineSetFilter);
1642+
}
15491643

15501644
// maintain the main timeline event order as returned from the HS
15511645
// merge overlay events at approximately the right position based on local timestamp
15521646
const events = overlayEvents.reduce(
15531647
(acc: MatrixEvent[], overlayEvent: MatrixEvent) => {
15541648
// find the first main tl event with a later timestamp
1555-
const index = acc.findIndex((event) => event.localTimestamp > overlayEvent.localTimestamp);
1649+
const index = acc.findIndex((event) => overlaysBefore(overlayEvent, event));
15561650
// insert overlay event into timeline at approximately the right place
1557-
if (index > -1) {
1558-
acc.splice(index, 0, overlayEvent);
1651+
// if it's beyond the edge of the main window, hide it so that expanding
1652+
// the main window doesn't cause new events to pop in and change its position
1653+
if (index === -1) {
1654+
if (!this.timelineWindow!.canPaginate(EventTimeline.FORWARDS)) {
1655+
acc.push(overlayEvent);
1656+
}
1657+
} else if (index === 0) {
1658+
if (!this.timelineWindow!.canPaginate(EventTimeline.BACKWARDS)) {
1659+
acc.unshift(overlayEvent);
1660+
}
15591661
} else {
1560-
acc.push(overlayEvent);
1662+
acc.splice(index, 0, overlayEvent);
15611663
}
15621664
return acc;
15631665
},
@@ -1574,14 +1676,14 @@ class TimelinePanel extends React.Component<IProps, IState> {
15741676
client.decryptEventIfNeeded(event);
15751677
});
15761678

1577-
const firstVisibleEventIndex = this.checkForPreJoinUISI(mainEvents);
1679+
const firstVisibleEventIndex = this.checkForPreJoinUISI(events);
15781680

15791681
// Hold onto the live events separately. The read receipt and read marker
15801682
// should use this list, so that they don't advance into pending events.
15811683
const liveEvents = [...events];
15821684

15831685
// if we're at the end of the live timeline, append the pending events
1584-
if (!this.timelineWindow?.canPaginate(EventTimeline.FORWARDS)) {
1686+
if (!this.timelineWindow!.canPaginate(EventTimeline.FORWARDS)) {
15851687
const pendingEvents = this.props.timelineSet.getPendingEvents();
15861688
events.push(
15871689
...pendingEvents.filter((event) => {

0 commit comments

Comments
 (0)