@@ -24,7 +24,7 @@ import { TimelineWindow } from "matrix-js-sdk/src/timeline-window";
2424import { EventType , RelationType } from "matrix-js-sdk/src/@types/event" ;
2525import { SyncState } from "matrix-js-sdk/src/sync" ;
2626import { RoomMember , RoomMemberEvent } from "matrix-js-sdk/src/models/room-member" ;
27- import { debounce , throttle } from "lodash" ;
27+ import { debounce , findLastIndex , throttle } from "lodash" ;
2828import { logger } from "matrix-js-sdk/src/logger" ;
2929import { ClientEvent } from "matrix-js-sdk/src/client" ;
3030import { 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+
7682interface 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