Skip to content

Commit c1fd56a

Browse files
committed
proof of concept for thread list api implementation
1 parent fb565f3 commit c1fd56a

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
@@ -2394,7 +2394,7 @@ describe("Room", function() {
23942394
});
23952395

23962396
it("should aggregate relations in thread event timeline set", () => {
2397-
Thread.setServerSideSupport(true, true);
2397+
Thread.setServerSideSupport(true, true, false);
23982398
const threadRoot = mkMessage();
23992399
const rootReaction = mkReaction(threadRoot);
24002400
const threadResponse = mkThreadResponse(threadRoot);

src/client.ts

Lines changed: 123 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -573,6 +573,13 @@ interface IMessagesResponse {
573573
state: IStateEvent[];
574574
}
575575

576+
interface IThreadedMessagesResponse {
577+
prev_batch: string;
578+
next_batch: string;
579+
chunk: IRoomEvent[];
580+
state: IStateEvent[];
581+
}
582+
576583
export interface IRequestTokenResponse {
577584
sid: string;
578585
submit_url?: string;
@@ -1194,12 +1201,12 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
11941201
}
11951202

11961203
try {
1197-
const { serverSupport, stable } = await this.doesServerSupportThread();
1198-
Thread.setServerSideSupport(serverSupport, stable);
1204+
const { serverSupport, stable, listThreads } = await this.doesServerSupportThread();
1205+
Thread.setServerSideSupport(serverSupport, stable, listThreads);
11991206
} catch (e) {
12001207
// Most likely cause is that `doesServerSupportThread` returned `null` (as it
12011208
// is allowed to do) and thus we enter "degraded mode" on threads.
1202-
Thread.setServerSideSupport(false, true);
1209+
Thread.setServerSideSupport(false, true, false);
12031210
}
12041211

12051212
// shallow-copy the opts dict before modifying and storing it
@@ -5496,6 +5503,63 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
54965503
return this.http.authedRequest(undefined, Method.Get, path, params);
54975504
}
54985505

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

55155581
// TODO: we should implement a backoff (as per scrollback()) to deal more
55165582
// nicely with HTTP errors.
@@ -5581,8 +5647,43 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
55815647
eventTimeline.paginationRequests[dir] = null;
55825648
});
55835649
eventTimeline.paginationRequests[dir] = promise;
5650+
} else if (isThreadTimeline) {
5651+
if (!room) {
5652+
throw new Error("Unknown room " + eventTimeline.getRoomId());
5653+
}
5654+
5655+
promise = this.createThreadMessagesRequest(
5656+
eventTimeline.getRoomId(),
5657+
token,
5658+
opts.limit,
5659+
dir,
5660+
eventTimeline.getFilter(),
5661+
).then((res) => {
5662+
if (res.state) {
5663+
const roomState = eventTimeline.getState(dir);
5664+
const stateEvents = res.state.map(this.getEventMapper());
5665+
roomState.setUnknownStateEvents(stateEvents);
5666+
}
5667+
const token = res.end;
5668+
const matrixEvents = res.chunk.map(this.getEventMapper());
5669+
5670+
const timelineSet = eventTimeline.getTimelineSet();
5671+
timelineSet.addEventsToTimeline(matrixEvents, backwards, eventTimeline, token);
5672+
this.processBeaconEvents(timelineSet.room, matrixEvents);
5673+
this.processThreadRoots(timelineSet.room, matrixEvents, backwards);
5674+
5675+
// if we've hit the end of the timeline, we need to stop trying to
5676+
// paginate. We need to keep the 'forwards' token though, to make sure
5677+
// we can recover from gappy syncs.
5678+
if (backwards && res.end == res.start) {
5679+
eventTimeline.setPaginationToken(null, dir);
5680+
}
5681+
return res.end != res.start;
5682+
}).finally(() => {
5683+
eventTimeline.paginationRequests[dir] = null;
5684+
});
5685+
eventTimeline.paginationRequests[dir] = promise;
55845686
} else {
5585-
const room = this.getRoom(eventTimeline.getRoomId());
55865687
if (!room) {
55875688
throw new Error("Unknown room " + eventTimeline.getRoomId());
55885689
}
@@ -6665,6 +6766,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
66656766
* @return {Promise<boolean>} true if the feature is supported
66666767
*/
66676768
public async doesServerSupportUnstableFeature(feature: string): Promise<boolean> {
6769+
// FIXME: WORKAROUND FOR NOW
6770+
if (feature === "org.matrix.msc3856") {
6771+
return this.http.opts.baseUrl === "https://threads-dev.lab.element.dev";
6772+
}
66686773
const response = await this.getVersions();
66696774
if (!response) return false;
66706775
const unstableFeatures = response["unstable_features"];
@@ -6694,16 +6799,21 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
66946799
public async doesServerSupportThread(): Promise<{
66956800
serverSupport: boolean;
66966801
stable: boolean;
6802+
listThreads: boolean;
66976803
} | null> {
66986804
try {
6699-
const hasUnstableSupport = await this.doesServerSupportUnstableFeature("org.matrix.msc3440");
6700-
const hasStableSupport = await this.doesServerSupportUnstableFeature("org.matrix.msc3440.stable");
6805+
const [hasUnstableSupport, hasStableSupport, hasListThreadsSupport] = await Promise.all([
6806+
this.doesServerSupportUnstableFeature("org.matrix.msc3440"),
6807+
this.doesServerSupportUnstableFeature("org.matrix.msc3440.stable"),
6808+
this.doesServerSupportUnstableFeature("org.matrix.msc3856"),
6809+
]);
67016810

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

67046813
return {
67056814
serverSupport: hasUnstableSupport || hasStableSupport,
67066815
stable: hasStableSupport,
6816+
listThreads: hasListThreadsSupport,
67076817
};
67086818
} catch (e) {
67096819
// Assume server support and stability aren't available: null/no data return.
@@ -9086,6 +9196,13 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
90869196
room.processThreadedEvents(threadedEvents, toStartOfTimeline);
90879197
}
90889198

9199+
/**
9200+
* @experimental
9201+
*/
9202+
public processThreadRoots(room: Room, threadedEvents: MatrixEvent[], toStartOfTimeline: boolean): void {
9203+
room.processThreadRoots(threadedEvents, toStartOfTimeline);
9204+
}
9205+
90899206
public processBeaconEvents(
90909207
room?: Room,
90919208
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)