Skip to content

Commit 4dfe884

Browse files
committed
proof of concept for thread list api implementation
1 parent 9eb7290 commit 4dfe884

File tree

6 files changed

+225
-21
lines changed

6 files changed

+225
-21
lines changed

spec/integ/matrix-client-event-timeline.spec.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,7 @@ describe("MatrixClient event timelines", function() {
299299
afterEach(function() {
300300
httpBackend.verifyNoOutstandingExpectation();
301301
client.stopClient();
302-
Thread.setServerSideSupport(false, false);
302+
Thread.setServerSideSupport(false, false, false);
303303
});
304304

305305
describe("getEventTimeline", function() {
@@ -552,7 +552,7 @@ describe("MatrixClient event timelines", function() {
552552
it("should handle thread replies with server support by fetching a contiguous thread timeline", async () => {
553553
// @ts-ignore
554554
client.clientOpts.experimentalThreadSupport = true;
555-
Thread.setServerSideSupport(true, false);
555+
Thread.setServerSideSupport(true, false, false);
556556
client.stopClient(); // we don't need the client to be syncing at this time
557557
const room = client.getRoom(roomId);
558558
const thread = room.createThread(THREAD_ROOT.event_id, undefined, [], false);
@@ -598,7 +598,7 @@ describe("MatrixClient event timelines", function() {
598598
it("should return relevant timeline from non-thread timelineSet when asking for the thread root", async () => {
599599
// @ts-ignore
600600
client.clientOpts.experimentalThreadSupport = true;
601-
Thread.setServerSideSupport(true, false);
601+
Thread.setServerSideSupport(true, false, false);
602602
client.stopClient(); // we don't need the client to be syncing at this time
603603
const room = client.getRoom(roomId);
604604
const threadRoot = new MatrixEvent(THREAD_ROOT);
@@ -630,7 +630,7 @@ describe("MatrixClient event timelines", function() {
630630
it("should return undefined when event is not in the thread that the given timelineSet is representing", () => {
631631
// @ts-ignore
632632
client.clientOpts.experimentalThreadSupport = true;
633-
Thread.setServerSideSupport(true, false);
633+
Thread.setServerSideSupport(true, false, false);
634634
client.stopClient(); // we don't need the client to be syncing at this time
635635
const room = client.getRoom(roomId);
636636
const threadRoot = new MatrixEvent(THREAD_ROOT);
@@ -658,7 +658,7 @@ describe("MatrixClient event timelines", function() {
658658
it("should return undefined when event is within a thread but timelineSet is not", () => {
659659
// @ts-ignore
660660
client.clientOpts.experimentalThreadSupport = true;
661-
Thread.setServerSideSupport(true, false);
661+
Thread.setServerSideSupport(true, false, false);
662662
client.stopClient(); // we don't need the client to be syncing at this time
663663
const room = client.getRoom(roomId);
664664
const timelineSet = room.getTimelineSets()[0];
@@ -1053,7 +1053,7 @@ describe("MatrixClient event timelines", function() {
10531053
it("should re-insert room IDs for bundled thread relation events", async () => {
10541054
// @ts-ignore
10551055
client.clientOpts.experimentalThreadSupport = true;
1056-
Thread.setServerSideSupport(true, false);
1056+
Thread.setServerSideSupport(true, false, false);
10571057

10581058
httpBackend.when("GET", "/sync").respond(200, {
10591059
next_batch: "s_5_4",

spec/unit/room.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2383,7 +2383,7 @@ describe("Room", function() {
23832383
});
23842384

23852385
it("should aggregate relations in thread event timeline set", () => {
2386-
Thread.setServerSideSupport(true, true);
2386+
Thread.setServerSideSupport(true, true, false);
23872387
const threadRoot = mkMessage();
23882388
const rootReaction = mkReaction(threadRoot);
23892389
const threadResponse = mkThreadResponse(threadRoot);

src/client.ts

Lines changed: 123 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -566,6 +566,13 @@ interface IMessagesResponse {
566566
state: IStateEvent[];
567567
}
568568

569+
interface IThreadedMessagesResponse {
570+
prev_batch: string;
571+
next_batch: string;
572+
chunk: IRoomEvent[];
573+
state: IStateEvent[];
574+
}
575+
569576
export interface IRequestTokenResponse {
570577
sid: string;
571578
submit_url?: string;
@@ -1178,12 +1185,12 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
11781185
}
11791186

11801187
try {
1181-
const { serverSupport, stable } = await this.doesServerSupportThread();
1182-
Thread.setServerSideSupport(serverSupport, stable);
1188+
const { serverSupport, stable, listThreads } = await this.doesServerSupportThread();
1189+
Thread.setServerSideSupport(serverSupport, stable, listThreads);
11831190
} catch (e) {
11841191
// Most likely cause is that `doesServerSupportThread` returned `null` (as it
11851192
// is allowed to do) and thus we enter "degraded mode" on threads.
1186-
Thread.setServerSideSupport(false, true);
1193+
Thread.setServerSideSupport(false, true, false);
11871194
}
11881195

11891196
// shallow-copy the opts dict before modifying and storing it
@@ -5480,6 +5487,63 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
54805487
return this.http.authedRequest(undefined, Method.Get, path, params);
54815488
}
54825489

5490+
/**
5491+
* Makes a request to /messages with the appropriate lazy loading filter set.
5492+
* XXX: if we do get rid of scrollback (as it's not used at the moment),
5493+
* we could inline this method again in paginateEventTimeline as that would
5494+
* then be the only call-site
5495+
* @param {string} roomId
5496+
* @param {string} fromToken
5497+
* @param {number} limit the maximum amount of events the retrieve
5498+
* @param {string} dir 'f' or 'b'
5499+
* @param {Filter} timelineFilter the timeline filter to pass
5500+
* @return {Promise}
5501+
*/
5502+
// XXX: Intended private, used by room.fetchRoomThreads
5503+
public createThreadMessagesRequest(
5504+
roomId: string,
5505+
fromToken: string | null,
5506+
limit = 30,
5507+
dir: Direction,
5508+
timelineFilter?: Filter,
5509+
): Promise<IMessagesResponse> {
5510+
const path = utils.encodeUri("/rooms/$roomId/threads", { $roomId: roomId });
5511+
5512+
const params: Record<string, string> = {
5513+
limit: limit.toString(),
5514+
dir: dir,
5515+
include: 'all',
5516+
};
5517+
5518+
if (fromToken) {
5519+
params.from = fromToken;
5520+
}
5521+
5522+
let filter = null;
5523+
if (this.clientOpts.lazyLoadMembers) {
5524+
// create a shallow copy of LAZY_LOADING_MESSAGES_FILTER,
5525+
// so the timelineFilter doesn't get written into it below
5526+
filter = Object.assign({}, Filter.LAZY_LOADING_MESSAGES_FILTER);
5527+
}
5528+
if (timelineFilter) {
5529+
// XXX: it's horrific that /messages' filter parameter doesn't match
5530+
// /sync's one - see https://matrix.org/jira/browse/SPEC-451
5531+
filter = filter || {};
5532+
Object.assign(filter, timelineFilter.getRoomTimelineFilterComponent()?.toJSON());
5533+
}
5534+
if (filter) {
5535+
params.filter = JSON.stringify(filter);
5536+
}
5537+
5538+
return this.http.authedRequest<IThreadedMessagesResponse>(undefined, Method.Get, path, params, undefined, {
5539+
prefix: "/_matrix/client/unstable/org.matrix.msc3856",
5540+
}).then(res => ({
5541+
...res,
5542+
start: res.prev_batch,
5543+
end: res.next_batch,
5544+
}));
5545+
}
5546+
54835547
/**
54845548
* Take an EventTimeline, and back/forward-fill results.
54855549
*
@@ -5495,6 +5559,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
54955559
*/
54965560
public paginateEventTimeline(eventTimeline: EventTimeline, opts: IPaginateOpts): Promise<boolean> {
54975561
const isNotifTimeline = (eventTimeline.getTimelineSet() === this.notifTimelineSet);
5562+
const room = this.getRoom(eventTimeline.getRoomId());
5563+
const isThreadTimeline = eventTimeline.getTimelineSet().isThreadTimeline;
54985564

54995565
// TODO: we should implement a backoff (as per scrollback()) to deal more
55005566
// nicely with HTTP errors.
@@ -5565,8 +5631,43 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
55655631
eventTimeline.paginationRequests[dir] = null;
55665632
});
55675633
eventTimeline.paginationRequests[dir] = promise;
5634+
} else if (isThreadTimeline) {
5635+
if (!room) {
5636+
throw new Error("Unknown room " + eventTimeline.getRoomId());
5637+
}
5638+
5639+
promise = this.createThreadMessagesRequest(
5640+
eventTimeline.getRoomId(),
5641+
token,
5642+
opts.limit,
5643+
dir,
5644+
eventTimeline.getFilter(),
5645+
).then((res) => {
5646+
if (res.state) {
5647+
const roomState = eventTimeline.getState(dir);
5648+
const stateEvents = res.state.map(this.getEventMapper());
5649+
roomState.setUnknownStateEvents(stateEvents);
5650+
}
5651+
const token = res.end;
5652+
const matrixEvents = res.chunk.map(this.getEventMapper());
5653+
5654+
const timelineSet = eventTimeline.getTimelineSet();
5655+
timelineSet.addEventsToTimeline(matrixEvents, backwards, eventTimeline, token);
5656+
this.processBeaconEvents(timelineSet.room, matrixEvents);
5657+
this.processThreadRoots(timelineSet.room, matrixEvents, backwards);
5658+
5659+
// if we've hit the end of the timeline, we need to stop trying to
5660+
// paginate. We need to keep the 'forwards' token though, to make sure
5661+
// we can recover from gappy syncs.
5662+
if (backwards && res.end == res.start) {
5663+
eventTimeline.setPaginationToken(null, dir);
5664+
}
5665+
return res.end != res.start;
5666+
}).finally(() => {
5667+
eventTimeline.paginationRequests[dir] = null;
5668+
});
5669+
eventTimeline.paginationRequests[dir] = promise;
55685670
} else {
5569-
const room = this.getRoom(eventTimeline.getRoomId());
55705671
if (!room) {
55715672
throw new Error("Unknown room " + eventTimeline.getRoomId());
55725673
}
@@ -6649,6 +6750,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
66496750
* @return {Promise<boolean>} true if the feature is supported
66506751
*/
66516752
public async doesServerSupportUnstableFeature(feature: string): Promise<boolean> {
6753+
// FIXME: WORKAROUND FOR NOW
6754+
if (feature === "org.matrix.msc3856") {
6755+
return this.http.opts.baseUrl === "https://threads-dev.lab.element.dev";
6756+
}
66526757
const response = await this.getVersions();
66536758
if (!response) return false;
66546759
const unstableFeatures = response["unstable_features"];
@@ -6678,16 +6783,21 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
66786783
public async doesServerSupportThread(): Promise<{
66796784
serverSupport: boolean;
66806785
stable: boolean;
6786+
listThreads: boolean;
66816787
} | null> {
66826788
try {
6683-
const hasUnstableSupport = await this.doesServerSupportUnstableFeature("org.matrix.msc3440");
6684-
const hasStableSupport = await this.doesServerSupportUnstableFeature("org.matrix.msc3440.stable");
6789+
const [hasUnstableSupport, hasStableSupport, hasListThreadsSupport] = await Promise.all([
6790+
this.doesServerSupportUnstableFeature("org.matrix.msc3440"),
6791+
this.doesServerSupportUnstableFeature("org.matrix.msc3440.stable"),
6792+
this.doesServerSupportUnstableFeature("org.matrix.msc3856"),
6793+
]);
66856794

66866795
// TODO: Use `this.isVersionSupported("v1.3")` for whatever spec version includes MSC3440 formally.
66876796

66886797
return {
66896798
serverSupport: hasUnstableSupport || hasStableSupport,
66906799
stable: hasStableSupport,
6800+
listThreads: hasListThreadsSupport,
66916801
};
66926802
} catch (e) {
66936803
// Assume server support and stability aren't available: null/no data return.
@@ -9060,6 +9170,13 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
90609170
room.processThreadedEvents(threadedEvents, toStartOfTimeline);
90619171
}
90629172

9173+
/**
9174+
* @experimental
9175+
*/
9176+
public processThreadRoots(room: Room, threadedEvents: MatrixEvent[], toStartOfTimeline: boolean): void {
9177+
room.processThreadRoots(threadedEvents, toStartOfTimeline);
9178+
}
9179+
90639180
public processBeaconEvents(
90649181
room?: Room,
90659182
events?: MatrixEvent[],

src/models/event-timeline-set.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,12 +123,15 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
123123
* @param {MatrixClient=} client the Matrix client which owns this EventTimelineSet,
124124
* can be omitted if room is specified.
125125
* @param {Thread=} thread the thread to which this timeline set relates.
126+
* @param {boolean} isThreadTimeline Whether this timeline set relates to a thread list timeline
127+
* (e.g., All threads or My threads)
126128
*/
127129
constructor(
128130
public readonly room: Room | undefined,
129131
opts: IOpts = {},
130132
client?: MatrixClient,
131133
public readonly thread?: Thread,
134+
public readonly isThreadTimeline: boolean = false,
132135
) {
133136
super();
134137

0 commit comments

Comments
 (0)