@@ -5354,6 +5354,12 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
53545354 return timelineSet . getTimelineForEvent ( eventId ) ;
53555355 }
53565356
5357+ if ( this . supportsExperimentalThreads ( )
5358+ && Thread . hasServerSideSupport === FeatureSupport . Stable
5359+ && timelineSet . thread ) {
5360+ return this . getThreadTimeline ( timelineSet , eventId ) ;
5361+ }
5362+
53575363 const path = utils . encodeUri (
53585364 "/rooms/$roomId/context/$eventId" , {
53595365 $roomId : timelineSet . room . roomId ,
@@ -5388,38 +5394,6 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
53885394 ...res . events_before . map ( mapper ) ,
53895395 ] ;
53905396
5391- if ( this . supportsExperimentalThreads ( ) ) {
5392- if ( ! timelineSet . canContain ( event ) ) {
5393- return undefined ;
5394- }
5395-
5396- // Where the event is a thread reply (not a root) and running in MSC-enabled mode the Thread timeline only
5397- // functions contiguously, so we have to jump through some hoops to get our target event in it.
5398- // XXX: workaround for https://github.com/vector-im/element-meta/issues/150
5399- if ( Thread . hasServerSideSupport && timelineSet . thread ) {
5400- const thread = timelineSet . thread ;
5401- const opts : IRelationsRequestOpts = {
5402- dir : Direction . Backward ,
5403- limit : 50 ,
5404- } ;
5405-
5406- await thread . fetchInitialEvents ( ) ;
5407- let nextBatch : string | null | undefined = thread . liveTimeline . getPaginationToken ( Direction . Backward ) ;
5408-
5409- // Fetch events until we find the one we were asked for, or we run out of pages
5410- while ( ! thread . findEventById ( eventId ) ) {
5411- if ( nextBatch ) {
5412- opts . from = nextBatch ;
5413- }
5414-
5415- ( { nextBatch } = await thread . fetchEvents ( opts ) ) ;
5416- if ( ! nextBatch ) break ;
5417- }
5418-
5419- return thread . liveTimeline ;
5420- }
5421- }
5422-
54235397 // Here we handle non-thread timelines only, but still process any thread events to populate thread summaries.
54245398 let timeline = timelineSet . getTimelineForEvent ( events [ 0 ] . getId ( ) ) ;
54255399 if ( timeline ) {
@@ -5444,6 +5418,118 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
54445418 ?? timeline ;
54455419 }
54465420
5421+ public async getThreadTimeline ( timelineSet : EventTimelineSet , eventId : string ) : Promise < EventTimeline | undefined > {
5422+ if ( ! Thread . hasServerSideSupport ) {
5423+ throw new Error ( "could not get thread timeline: no serverside support" ) ;
5424+ }
5425+
5426+ if ( ! this . supportsExperimentalThreads ( ) ) {
5427+ throw new Error ( "could not get thread timeline: no client support" ) ;
5428+ }
5429+
5430+ const path = utils . encodeUri (
5431+ "/rooms/$roomId/context/$eventId" , {
5432+ $roomId : timelineSet . room . roomId ,
5433+ $eventId : eventId ,
5434+ } ,
5435+ ) ;
5436+
5437+ const params : Record < string , string | string [ ] > = {
5438+ limit : "0" ,
5439+ } ;
5440+ if ( this . clientOpts . lazyLoadMembers ) {
5441+ params . filter = JSON . stringify ( Filter . LAZY_LOADING_MESSAGES_FILTER ) ;
5442+ }
5443+
5444+ // TODO: we should implement a backoff (as per scrollback()) to deal more nicely with HTTP errors.
5445+ const res = await this . http . authedRequest < IContextResponse > ( undefined , Method . Get , path , params ) ;
5446+ const mapper = this . getEventMapper ( ) ;
5447+ const event = mapper ( res . event ) ;
5448+
5449+ if ( ! timelineSet . canContain ( event ) ) {
5450+ return undefined ;
5451+ }
5452+
5453+ if ( Thread . hasServerSideSupport === FeatureSupport . Stable ) {
5454+ if ( ! timelineSet . thread ) {
5455+ throw new Error ( "could not get thread timeline: not a thread timeline" ) ;
5456+ }
5457+
5458+ const thread = timelineSet . thread ;
5459+ const resOlder = await this . fetchRelations (
5460+ timelineSet . room . roomId ,
5461+ thread . id ,
5462+ THREAD_RELATION_TYPE . name ,
5463+ null ,
5464+ { dir : Direction . Backward , from : res . start } ,
5465+ ) ;
5466+ const resNewer = await this . fetchRelations (
5467+ timelineSet . room . roomId ,
5468+ thread . id ,
5469+ THREAD_RELATION_TYPE . name ,
5470+ null ,
5471+ { dir : Direction . Forward , from : res . end } ,
5472+ ) ;
5473+ const events = [
5474+ // Order events from most recent to oldest (reverse-chronological).
5475+ // We start with the last event, since that's the point at which we have known state.
5476+ // events_after is already backwards; events_before is forwards.
5477+ ...resNewer . chunk . reverse ( ) . map ( mapper ) ,
5478+ event ,
5479+ ...resOlder . chunk . map ( mapper ) ,
5480+ ] ;
5481+ await timelineSet . thread ?. fetchEditsWhereNeeded ( ...events ) ;
5482+
5483+ // Here we handle non-thread timelines only, but still process any thread events to populate thread summaries.
5484+ let timeline = timelineSet . getTimelineForEvent ( event . getId ( ) ) ;
5485+ if ( timeline ) {
5486+ timeline . getState ( EventTimeline . BACKWARDS ) . setUnknownStateEvents ( res . state . map ( mapper ) ) ;
5487+ } else {
5488+ timeline = timelineSet . addTimeline ( ) ;
5489+ timeline . initialiseState ( res . state . map ( mapper ) ) ;
5490+ }
5491+
5492+ timelineSet . addEventsToTimeline ( events , true , timeline , resNewer . next_batch ) ;
5493+ if ( ! resOlder . next_batch ) {
5494+ timelineSet . addEventsToTimeline ( [ mapper ( resOlder . original_event ) ] , true , timeline , null ) ;
5495+ }
5496+ timeline . setPaginationToken ( resOlder . next_batch ?? null , Direction . Backward ) ;
5497+ timeline . setPaginationToken ( resNewer . next_batch ?? null , Direction . Forward ) ;
5498+ this . processBeaconEvents ( timelineSet . room , events ) ;
5499+
5500+ // There is no guarantee that the event ended up in "timeline" (we might have switched to a neighbouring
5501+ // timeline) - so check the room's index again. On the other hand, there's no guarantee the event ended up
5502+ // anywhere, if it was later redacted, so we just return the timeline we first thought of.
5503+ return timelineSet . getTimelineForEvent ( eventId )
5504+ ?? timeline ;
5505+ } else {
5506+ // Where the event is a thread reply (not a root) and running in MSC-enabled mode the Thread timeline only
5507+ // functions contiguously, so we have to jump through some hoops to get our target event in it.
5508+ // XXX: workaround for https://github.com/vector-im/element-meta/issues/150
5509+
5510+ const thread = timelineSet . thread ;
5511+ const opts : IRelationsRequestOpts = {
5512+ dir : Direction . Backward ,
5513+ limit : 50 ,
5514+ } ;
5515+
5516+ await thread . fetchInitialEvents ( ) ;
5517+ let nextBatch : string | null | undefined = thread . liveTimeline . getPaginationToken ( Direction . Backward ) ;
5518+
5519+ // Fetch events until we find the one we were asked for, or we run out of pages
5520+ while ( ! thread . findEventById ( eventId ) ) {
5521+ if ( nextBatch ) {
5522+ opts . from = nextBatch ;
5523+ }
5524+
5525+ ( { nextBatch } = await thread . fetchEvents ( opts ) ) ;
5526+ if ( ! nextBatch ) break ;
5527+ }
5528+
5529+ return thread . liveTimeline ;
5530+ }
5531+ }
5532+
54475533 /**
54485534 * Get an EventTimeline for the latest events in the room. This will just
54495535 * call `/messages` to get the latest message in the room, then use
@@ -5465,28 +5551,44 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
54655551 throw new Error ( "getLatestTimeline only supports room timelines" ) ;
54665552 }
54675553
5468- let res : IMessagesResponse ;
5469- const roomId = timelineSet . room . roomId ;
5554+ let event ;
54705555 if ( timelineSet . isThreadTimeline ) {
5471- res = await this . createThreadListMessagesRequest (
5472- roomId ,
5556+ const res = await this . createThreadListMessagesRequest (
5557+ timelineSet . room . roomId ,
54735558 null ,
54745559 1 ,
54755560 Direction . Backward ,
54765561 timelineSet . getFilter ( ) ,
54775562 ) ;
5478- } else {
5479- res = await this . createMessagesRequest (
5480- roomId ,
5563+ event = res . chunk ?. [ 0 ] ;
5564+ } else if ( timelineSet . thread && Thread . hasServerSideSupport ) {
5565+ const res = await this . fetchRelations (
5566+ timelineSet . room . roomId ,
5567+ timelineSet . thread . id ,
5568+ THREAD_RELATION_TYPE . name ,
54815569 null ,
5482- 1 ,
5483- Direction . Backward ,
5484- timelineSet . getFilter ( ) ,
5570+ { dir : Direction . Backward , limit : 1 } ,
5571+ ) ;
5572+ event = res . chunk ?. [ 0 ] ;
5573+ } else {
5574+ const messagesPath = utils . encodeUri (
5575+ "/rooms/$roomId/messages" , {
5576+ $roomId : timelineSet . room . roomId ,
5577+ } ,
54855578 ) ;
5579+
5580+ const params : Record < string , string | string [ ] > = {
5581+ dir : 'b' ,
5582+ } ;
5583+ if ( this . clientOpts . lazyLoadMembers ) {
5584+ params . filter = JSON . stringify ( Filter . LAZY_LOADING_MESSAGES_FILTER ) ;
5585+ }
5586+
5587+ const res = await this . http . authedRequest < IMessagesResponse > ( undefined , Method . Get , messagesPath , params ) ;
5588+ event = res . chunk ?. [ 0 ] ;
54865589 }
5487- const event = res . chunk ?. [ 0 ] ;
54885590 if ( ! event ) {
5489- throw new Error ( "No message returned from /messages when trying to construct getLatestTimeline" ) ;
5591+ throw new Error ( "No message returned when trying to construct getLatestTimeline" ) ;
54905592 }
54915593
54925594 return this . getEventTimeline ( timelineSet , event . event_id ) ;
@@ -5624,7 +5726,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
56245726 public paginateEventTimeline ( eventTimeline : EventTimeline , opts : IPaginateOpts ) : Promise < boolean > {
56255727 const isNotifTimeline = ( eventTimeline . getTimelineSet ( ) === this . notifTimelineSet ) ;
56265728 const room = this . getRoom ( eventTimeline . getRoomId ( ) ) ;
5627- const isThreadTimeline = eventTimeline . getTimelineSet ( ) . isThreadTimeline ;
5729+ const isThreadListTimeline = eventTimeline . getTimelineSet ( ) . isThreadTimeline ;
5730+ const isThreadTimeline = ( eventTimeline . getTimelineSet ( ) . thread ) ;
56285731
56295732 // TODO: we should implement a backoff (as per scrollback()) to deal more
56305733 // nicely with HTTP errors.
@@ -5695,7 +5798,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
56955798 eventTimeline . paginationRequests [ dir ] = null ;
56965799 } ) ;
56975800 eventTimeline . paginationRequests [ dir ] = promise ;
5698- } else if ( isThreadTimeline ) {
5801+ } else if ( isThreadListTimeline ) {
56995802 if ( ! room ) {
57005803 throw new Error ( "Unknown room " + eventTimeline . getRoomId ( ) ) ;
57015804 }
@@ -5731,6 +5834,43 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
57315834 eventTimeline . paginationRequests [ dir ] = null ;
57325835 } ) ;
57335836 eventTimeline . paginationRequests [ dir ] = promise ;
5837+ } else if ( isThreadTimeline ) {
5838+ const room = this . getRoom ( eventTimeline . getRoomId ( ) ) ;
5839+ if ( ! room ) {
5840+ throw new Error ( "Unknown room " + eventTimeline . getRoomId ( ) ) ;
5841+ }
5842+
5843+ promise = this . fetchRelations (
5844+ eventTimeline . getRoomId ( ) ,
5845+ eventTimeline . getTimelineSet ( ) . thread ?. id ,
5846+ THREAD_RELATION_TYPE . name ,
5847+ null ,
5848+ { dir, limit : opts . limit , from : token } ,
5849+ ) . then ( ( res ) => {
5850+ const mapper = this . getEventMapper ( ) ;
5851+ const matrixEvents = res . chunk . map ( mapper ) ;
5852+ eventTimeline . getTimelineSet ( ) . thread ?. fetchEditsWhereNeeded ( ...matrixEvents ) ;
5853+
5854+ const newToken = res . next_batch ;
5855+
5856+ const timelineSet = eventTimeline . getTimelineSet ( ) ;
5857+ timelineSet . addEventsToTimeline ( matrixEvents , backwards , eventTimeline , newToken ?? null ) ;
5858+ if ( ! newToken && backwards ) {
5859+ timelineSet . addEventsToTimeline ( [ mapper ( res . original_event ) ] , true , eventTimeline , null ) ;
5860+ }
5861+ this . processBeaconEvents ( timelineSet . room , matrixEvents ) ;
5862+
5863+ // if we've hit the end of the timeline, we need to stop trying to
5864+ // paginate. We need to keep the 'forwards' token though, to make sure
5865+ // we can recover from gappy syncs.
5866+ if ( backwards && ! newToken ) {
5867+ eventTimeline . setPaginationToken ( null , dir ) ;
5868+ }
5869+ return Boolean ( newToken ) ;
5870+ } ) . finally ( ( ) => {
5871+ eventTimeline . paginationRequests [ dir ] = null ;
5872+ } ) ;
5873+ eventTimeline . paginationRequests [ dir ] = promise ;
57345874 } else {
57355875 if ( ! room ) {
57365876 throw new Error ( "Unknown room " + eventTimeline . getRoomId ( ) ) ;
@@ -5752,10 +5892,12 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
57525892 const matrixEvents = res . chunk . map ( this . getEventMapper ( ) ) ;
57535893
57545894 const timelineSet = eventTimeline . getTimelineSet ( ) ;
5755- const [ timelineEvents , threadedEvents ] = room . partitionThreadedEvents ( matrixEvents ) ;
5895+ const [ timelineEvents ] = room . partitionThreadedEvents ( matrixEvents ) ;
57565896 timelineSet . addEventsToTimeline ( timelineEvents , backwards , eventTimeline , token ) ;
57575897 this . processBeaconEvents ( room , timelineEvents ) ;
5758- this . processThreadEvents ( room , threadedEvents , backwards ) ;
5898+ this . processThreadRoots ( room ,
5899+ timelineEvents . filter ( it => it . isRelation ( THREAD_RELATION_TYPE . name ) ) ,
5900+ false ) ;
57595901
57605902 const atEnd = res . end === undefined || res . end === res . start ;
57615903
0 commit comments