@@ -5354,6 +5354,12 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
5354
5354
return timelineSet . getTimelineForEvent ( eventId ) ;
5355
5355
}
5356
5356
5357
+ if ( this . supportsExperimentalThreads ( )
5358
+ && Thread . hasServerSideSupport === FeatureSupport . Stable
5359
+ && timelineSet . thread ) {
5360
+ return this . getThreadTimeline ( timelineSet , eventId ) ;
5361
+ }
5362
+
5357
5363
const path = utils . encodeUri (
5358
5364
"/rooms/$roomId/context/$eventId" , {
5359
5365
$roomId : timelineSet . room . roomId ,
@@ -5388,38 +5394,6 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
5388
5394
...res . events_before . map ( mapper ) ,
5389
5395
] ;
5390
5396
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
-
5423
5397
// Here we handle non-thread timelines only, but still process any thread events to populate thread summaries.
5424
5398
let timeline = timelineSet . getTimelineForEvent ( events [ 0 ] . getId ( ) ) ;
5425
5399
if ( timeline ) {
@@ -5444,6 +5418,118 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
5444
5418
?? timeline ;
5445
5419
}
5446
5420
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
+
5447
5533
/**
5448
5534
* Get an EventTimeline for the latest events in the room. This will just
5449
5535
* call `/messages` to get the latest message in the room, then use
@@ -5465,28 +5551,44 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
5465
5551
throw new Error ( "getLatestTimeline only supports room timelines" ) ;
5466
5552
}
5467
5553
5468
- let res : IMessagesResponse ;
5469
- const roomId = timelineSet . room . roomId ;
5554
+ let event ;
5470
5555
if ( timelineSet . isThreadTimeline ) {
5471
- res = await this . createThreadListMessagesRequest (
5472
- roomId ,
5556
+ const res = await this . createThreadListMessagesRequest (
5557
+ timelineSet . room . roomId ,
5473
5558
null ,
5474
5559
1 ,
5475
5560
Direction . Backward ,
5476
5561
timelineSet . getFilter ( ) ,
5477
5562
) ;
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 ,
5481
5569
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
+ } ,
5485
5578
) ;
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 ] ;
5486
5589
}
5487
- const event = res . chunk ?. [ 0 ] ;
5488
5590
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" ) ;
5490
5592
}
5491
5593
5492
5594
return this . getEventTimeline ( timelineSet , event . event_id ) ;
@@ -5624,7 +5726,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
5624
5726
public paginateEventTimeline ( eventTimeline : EventTimeline , opts : IPaginateOpts ) : Promise < boolean > {
5625
5727
const isNotifTimeline = ( eventTimeline . getTimelineSet ( ) === this . notifTimelineSet ) ;
5626
5728
const room = this . getRoom ( eventTimeline . getRoomId ( ) ) ;
5627
- const isThreadTimeline = eventTimeline . getTimelineSet ( ) . isThreadTimeline ;
5729
+ const isThreadListTimeline = eventTimeline . getTimelineSet ( ) . isThreadTimeline ;
5730
+ const isThreadTimeline = ( eventTimeline . getTimelineSet ( ) . thread ) ;
5628
5731
5629
5732
// TODO: we should implement a backoff (as per scrollback()) to deal more
5630
5733
// nicely with HTTP errors.
@@ -5695,7 +5798,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
5695
5798
eventTimeline . paginationRequests [ dir ] = null ;
5696
5799
} ) ;
5697
5800
eventTimeline . paginationRequests [ dir ] = promise ;
5698
- } else if ( isThreadTimeline ) {
5801
+ } else if ( isThreadListTimeline ) {
5699
5802
if ( ! room ) {
5700
5803
throw new Error ( "Unknown room " + eventTimeline . getRoomId ( ) ) ;
5701
5804
}
@@ -5731,6 +5834,43 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
5731
5834
eventTimeline . paginationRequests [ dir ] = null ;
5732
5835
} ) ;
5733
5836
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 ;
5734
5874
} else {
5735
5875
if ( ! room ) {
5736
5876
throw new Error ( "Unknown room " + eventTimeline . getRoomId ( ) ) ;
@@ -5752,10 +5892,12 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
5752
5892
const matrixEvents = res . chunk . map ( this . getEventMapper ( ) ) ;
5753
5893
5754
5894
const timelineSet = eventTimeline . getTimelineSet ( ) ;
5755
- const [ timelineEvents , threadedEvents ] = room . partitionThreadedEvents ( matrixEvents ) ;
5895
+ const [ timelineEvents ] = room . partitionThreadedEvents ( matrixEvents ) ;
5756
5896
timelineSet . addEventsToTimeline ( timelineEvents , backwards , eventTimeline , token ) ;
5757
5897
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 ) ;
5759
5901
5760
5902
const atEnd = res . end === undefined || res . end === res . start ;
5761
5903
0 commit comments