Skip to content

Commit 917adb1

Browse files
committed
poc
1 parent fb0c350 commit 917adb1

File tree

5 files changed

+338
-137
lines changed

5 files changed

+338
-137
lines changed

src/client.ts

+190-48
Original file line numberDiff line numberDiff line change
@@ -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

src/models/event-timeline-set.ts

+6
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,12 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
374374
}
375375
}
376376

377+
if (this.thread) {
378+
for (const event of events) {
379+
EventTimeline.setEventMetadata(event, this.room.currentState, false);
380+
}
381+
}
382+
377383
const direction = toStartOfTimeline ? EventTimeline.BACKWARDS :
378384
EventTimeline.FORWARDS;
379385
const inverseDirection = toStartOfTimeline ? EventTimeline.FORWARDS :

src/models/room.ts

+30-6
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,7 @@ export class Room extends ReadReceipt<EmittedEvents, RoomEventHandlerMap> {
252252
/**
253253
* @experimental
254254
*/
255-
private threads = new Map<string, Thread>();
255+
public threads = new Map<string, Thread>();
256256
public lastThread: Thread;
257257

258258
/**
@@ -1790,13 +1790,19 @@ export class Room extends ReadReceipt<EmittedEvents, RoomEventHandlerMap> {
17901790

17911791
private onThreadNewReply(thread: Thread): void {
17921792
const roomState = this.getLiveTimeline().getState(EventTimeline.FORWARDS);
1793-
for (const timelineSet of this.threadsTimelineSets) {
1794-
timelineSet.removeEvent(thread.id);
1795-
timelineSet.addLiveEvent(thread.rootEvent, {
1793+
if (thread.length) {
1794+
this.threadsTimelineSets?.[0]?.addLiveEvent(thread.rootEvent, {
17961795
duplicateStrategy: DuplicateStrategy.Replace,
17971796
fromCache: false,
17981797
roomState,
17991798
});
1799+
if (thread.hasCurrentUserParticipated) {
1800+
this.threadsTimelineSets?.[1]?.addLiveEvent(thread.rootEvent, {
1801+
duplicateStrategy: DuplicateStrategy.Replace,
1802+
fromCache: false,
1803+
roomState,
1804+
});
1805+
}
18001806
}
18011807
}
18021808

@@ -1924,7 +1930,6 @@ export class Room extends ReadReceipt<EmittedEvents, RoomEventHandlerMap> {
19241930
}
19251931

19261932
const thread = new Thread(threadId, rootEvent, {
1927-
initialEvents: events,
19281933
room: this,
19291934
client: this.client,
19301935
});
@@ -1946,7 +1951,11 @@ export class Room extends ReadReceipt<EmittedEvents, RoomEventHandlerMap> {
19461951
this.threadsTimelineSets.forEach(timelineSet => {
19471952
if (thread.rootEvent) {
19481953
if (Thread.hasServerSideSupport) {
1949-
timelineSet.addLiveEvent(thread.rootEvent);
1954+
timelineSet.addLiveEvent(thread.rootEvent, {
1955+
duplicateStrategy: DuplicateStrategy.Replace,
1956+
fromCache: false,
1957+
roomState: this.currentState,
1958+
});
19501959
} else {
19511960
timelineSet.addEventToTimeline(
19521961
thread.rootEvent,
@@ -1960,6 +1969,21 @@ export class Room extends ReadReceipt<EmittedEvents, RoomEventHandlerMap> {
19601969

19611970
this.emit(ThreadEvent.New, thread, toStartOfTimeline);
19621971

1972+
if (thread.length) {
1973+
this.threadsTimelineSets?.[0]?.addLiveEvent(thread.rootEvent, {
1974+
duplicateStrategy: DuplicateStrategy.Replace,
1975+
fromCache: false,
1976+
roomState: this.currentState,
1977+
});
1978+
if (thread.hasCurrentUserParticipated) {
1979+
this.threadsTimelineSets?.[1]?.addLiveEvent(thread.rootEvent, {
1980+
duplicateStrategy: DuplicateStrategy.Replace,
1981+
fromCache: false,
1982+
roomState: this.currentState,
1983+
});
1984+
}
1985+
}
1986+
19631987
return thread;
19641988
}
19651989

0 commit comments

Comments
 (0)