@@ -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,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,33 @@ 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 it's beyond the edge of the main window, hide it so that expanding
1655+ // the main window doesn't cause new events to pop in and change its position
1656+ if ( index === - 1 ) {
1657+ if ( ! this . timelineWindow ?. canPaginate ( EventTimeline . FORWARDS ) ) {
1658+ acc . push ( overlayEvent ) ;
1659+ }
1660+ } else if ( index === 0 ) {
1661+ if ( ! this . timelineWindow ?. canPaginate ( EventTimeline . BACKWARDS ) ) {
1662+ acc . unshift ( overlayEvent ) ;
1663+ }
15591664 } else {
1560- acc . push ( overlayEvent ) ;
1665+ acc . splice ( index , 0 , overlayEvent ) ;
15611666 }
15621667 return acc ;
15631668 } ,
@@ -1574,7 +1679,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
15741679 client . decryptEventIfNeeded ( event ) ;
15751680 } ) ;
15761681
1577- const firstVisibleEventIndex = this . checkForPreJoinUISI ( mainEvents ) ;
1682+ const firstVisibleEventIndex = this . checkForPreJoinUISI ( events ) ;
15781683
15791684 // Hold onto the live events separately. The read receipt and read marker
15801685 // should use this list, so that they don't advance into pending events.
0 commit comments