@@ -154,7 +154,10 @@ export class StopGapWidget extends EventEmitter {
154154 private kind : WidgetKind ;
155155 private readonly virtual : boolean ;
156156 private readUpToMap : { [ roomId : string ] : string } = { } ; // room ID to event ID
157- private stickyPromise ?: ( ) => Promise < void > ; // This promise will be called and needs to resolve before the widget will actually become sticky.
157+ // This promise will be called and needs to resolve before the widget will actually become sticky.
158+ private stickyPromise ?: ( ) => Promise < void > ;
159+ // Holds events that should be fed to the widget once they finish decrypting
160+ private readonly eventsToFeed = new WeakSet < MatrixEvent > ( ) ;
158161
159162 public constructor ( private appTileProps : IAppTileProps ) {
160163 super ( ) ;
@@ -465,12 +468,10 @@ export class StopGapWidget extends EventEmitter {
465468
466469 private onEvent = ( ev : MatrixEvent ) : void => {
467470 this . client . decryptEventIfNeeded ( ev ) ;
468- if ( ev . isBeingDecrypted ( ) || ev . isDecryptionFailure ( ) ) return ;
469471 this . feedEvent ( ev ) ;
470472 } ;
471473
472474 private onEventDecrypted = ( ev : MatrixEvent ) : void => {
473- if ( ev . isDecryptionFailure ( ) ) return ;
474475 this . feedEvent ( ev ) ;
475476 } ;
476477
@@ -480,72 +481,103 @@ export class StopGapWidget extends EventEmitter {
480481 await this . messaging ?. feedToDevice ( ev . getEffectiveEvent ( ) as IRoomEvent , ev . isEncrypted ( ) ) ;
481482 } ;
482483
483- private feedEvent ( ev : MatrixEvent ) : void {
484- if ( ! this . messaging ) return ;
485-
486- // Check to see if this event would be before or after our "read up to" marker. If it's
487- // before, or we can't decide, then we assume the widget will have already seen the event.
488- // If the event is after, or we don't have a marker for the room, then we'll send it through.
489- //
490- // This approach of "read up to" prevents widgets receiving decryption spam from startup or
491- // receiving out-of-order events from backfill and such.
492- //
493- // Skip marker timeline check for events with relations to unknown parent because these
494- // events are not added to the timeline here and will be ignored otherwise:
495- // https://github.com/matrix-org/matrix-js-sdk/blob/d3dfcd924201d71b434af3d77343b5229b6ed75e/src/models/room.ts#L2207-L2213
496- let isRelationToUnknown : boolean | undefined = undefined ;
497- const upToEventId = this . readUpToMap [ ev . getRoomId ( ) ! ] ;
498- if ( upToEventId ) {
499- // Small optimization for exact match (prevent search)
500- if ( upToEventId === ev . getId ( ) ) {
501- return ;
502- }
484+ /**
485+ * Determines whether the event has a relation to an unknown parent.
486+ */
487+ private relatesToUnknown ( ev : MatrixEvent ) : boolean {
488+ // Replies to unknown events don't count
489+ if ( ! ev . relationEventId || ev . replyEventId ) return false ;
490+ const room = this . client . getRoom ( ev . getRoomId ( ) ) ;
491+ return room === null || ! room . findEventById ( ev . relationEventId ) ;
492+ }
503493
504- // should be true to forward the event to the widget
505- let shouldForward = false ;
506-
507- const room = this . client . getRoom ( ev . getRoomId ( ) ! ) ;
508- if ( ! room ) return ;
509- // Timelines are most recent last, so reverse the order and limit ourselves to 100 events
510- // to avoid overusing the CPU.
511- const timeline = room . getLiveTimeline ( ) ;
512- const events = arrayFastClone ( timeline . getEvents ( ) ) . reverse ( ) . slice ( 0 , 100 ) ;
513-
514- for ( const timelineEvent of events ) {
515- if ( timelineEvent . getId ( ) === upToEventId ) {
516- break ;
517- } else if ( timelineEvent . getId ( ) === ev . getId ( ) ) {
518- shouldForward = true ;
519- break ;
520- }
521- }
494+ /**
495+ * Determines whether the event comes from a room that we've been invited to
496+ * (in which case we likely don't have the full timeline).
497+ */
498+ private isFromInvite ( ev : MatrixEvent ) : boolean {
499+ const room = this . client . getRoom ( ev . getRoomId ( ) ) ;
500+ return room ?. getMyMembership ( ) === KnownMembership . Invite ;
501+ }
522502
523- if ( ! shouldForward ) {
524- // checks that the event has a relation to unknown event
525- isRelationToUnknown =
526- ! ev . replyEventId && ! ! ev . relationEventId && ! room . findEventById ( ev . relationEventId ) ;
527- if ( ! isRelationToUnknown ) {
528- // Ignore the event: it is before our interest.
529- return ;
530- }
531- }
503+ /**
504+ * Advances the "read up to" marker for a room to a certain event. No-ops if
505+ * the event is before the marker.
506+ * @returns Whether the "read up to" marker was advanced.
507+ */
508+ private advanceReadUpToMarker ( ev : MatrixEvent ) : boolean {
509+ const evId = ev . getId ( ) ;
510+ if ( evId === undefined ) return false ;
511+ const roomId = ev . getRoomId ( ) ;
512+ if ( roomId === undefined ) return false ;
513+ const room = this . client . getRoom ( roomId ) ;
514+ if ( room === null ) return false ;
515+
516+ const upToEventId = this . readUpToMap [ ev . getRoomId ( ) ! ] ;
517+ if ( ! upToEventId ) {
518+ // There's no marker yet; start it at this event
519+ this . readUpToMap [ roomId ] = evId ;
520+ return true ;
532521 }
533522
534- // Skip marker assignment if membership is 'invite', otherwise 'm.room.member' from
535- // invitation room will assign it and new state events will be not forwarded to the widget
536- // because of empty timeline for invitation room and assigned marker.
537- const evRoomId = ev . getRoomId ( ) ;
538- const evId = ev . getId ( ) ;
539- if ( evRoomId && evId ) {
540- const room = this . client . getRoom ( evRoomId ) ;
541- if ( room && room . getMyMembership ( ) === KnownMembership . Join && ! isRelationToUnknown ) {
542- this . readUpToMap [ evRoomId ] = evId ;
523+ // Small optimization for exact match (skip the search)
524+ if ( upToEventId === evId ) return false ;
525+
526+ // Timelines are most recent last, so reverse the order and limit ourselves to 100 events
527+ // to avoid overusing the CPU.
528+ const timeline = room . getLiveTimeline ( ) ;
529+ const events = arrayFastClone ( timeline . getEvents ( ) ) . reverse ( ) . slice ( 0 , 100 ) ;
530+
531+ for ( const timelineEvent of events ) {
532+ if ( timelineEvent . getId ( ) === upToEventId ) {
533+ // The event must be somewhere before the "read up to" marker
534+ return false ;
535+ } else if ( timelineEvent . getId ( ) === ev . getId ( ) ) {
536+ // The event is after the marker; advance it
537+ this . readUpToMap [ roomId ] = evId ;
538+ return true ;
543539 }
544540 }
545541
546- const raw = ev . getEffectiveEvent ( ) ;
547- this . messaging . feedEvent ( raw as IRoomEvent , this . eventListenerRoomId ! ) . catch ( ( e ) => {
548- logger . error ( "Error sending event to widget: " , e ) ;
549- } ) ;
542+ // We can't say for sure whether the widget has seen the event; let's
543+ // just assume that it has
544+ return false ;
545+ }
546+
547+ private feedEvent ( ev : MatrixEvent ) : void {
548+ if ( this . messaging === null ) return ;
549+ if (
550+ // If we had decided earlier to feed this event to the widget, but
551+ // it just wasn't ready, give it another try
552+ this . eventsToFeed . delete ( ev ) ||
553+ // Skip marker timeline check for events with relations to unknown parent because these
554+ // events are not added to the timeline here and will be ignored otherwise:
555+ // https://github.com/matrix-org/matrix-js-sdk/blob/d3dfcd924201d71b434af3d77343b5229b6ed75e/src/models/room.ts#L2207-L2213
556+ this . relatesToUnknown ( ev ) ||
557+ // Skip marker timeline check for rooms where membership is
558+ // 'invite', otherwise the membership event from the invitation room
559+ // will advance the marker and new state events will not be
560+ // forwarded to the widget.
561+ this . isFromInvite ( ev ) ||
562+ // Check whether this event would be before or after our "read up to" marker. If it's
563+ // before, or we can't decide, then we assume the widget will have already seen the event.
564+ // If the event is after, or we don't have a marker for the room, then the marker will advance and we'll
565+ // send it through.
566+ // This approach of "read up to" prevents widgets receiving decryption spam from startup or
567+ // receiving ancient events from backfill and such.
568+ this . advanceReadUpToMarker ( ev )
569+ ) {
570+ // If the event is still being decrypted, remember that we want to
571+ // feed it to the widget (even if not strictly in the order given by
572+ // the timeline) and get back to it later
573+ if ( ev . isBeingDecrypted ( ) || ev . isDecryptionFailure ( ) ) {
574+ this . eventsToFeed . add ( ev ) ;
575+ } else {
576+ const raw = ev . getEffectiveEvent ( ) ;
577+ this . messaging . feedEvent ( raw as IRoomEvent , this . eventListenerRoomId ! ) . catch ( ( e ) => {
578+ logger . error ( "Error sending event to widget: " , e ) ;
579+ } ) ;
580+ }
581+ }
550582 }
551583}
0 commit comments