Skip to content

Commit 068fbb7

Browse files
justjanneGermaingermain-gg
authored
Loading threads with server-side assistance (#2735)
* Fix bug where undefined vs null in pagination tokens wasn't correctly handled * Fix bug where thread list results were sorted incorrectly * Allow removing the relationship of an event to a thread * Implement feature detection for new threads MSCs and specs * Prefix dir parameter for threads pagination if necessary * Make threads conform to the same timeline APIs as any other timeline * Extract thread timeline loading out of thread class * fix thread roots not being updated correctly * fix jumping to events by link * implement new thread timeline loading * Fix fetchRoomEvent incorrect return type Co-authored-by: Germain <[email protected]> Co-authored-by: Germain <[email protected]>
1 parent b447871 commit 068fbb7

11 files changed

+872
-471
lines changed

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

+144-78
Original file line numberDiff line numberDiff line change
@@ -342,8 +342,14 @@ describe("MatrixClient event timelines", function() {
342342
httpBackend.verifyNoOutstandingExpectation();
343343
client.stopClient();
344344
Thread.setServerSideSupport(FeatureSupport.None);
345+
Thread.setServerSideListSupport(FeatureSupport.None);
346+
Thread.setServerSideFwdPaginationSupport(FeatureSupport.None);
345347
});
346348

349+
async function flushHttp<T>(promise: Promise<T>): Promise<T> {
350+
return Promise.all([promise, httpBackend.flushAllExpected()]).then(([result]) => result);
351+
}
352+
347353
describe("getEventTimeline", function() {
348354
it("should create a new timeline for new events", function() {
349355
const room = client.getRoom(roomId)!;
@@ -595,31 +601,51 @@ describe("MatrixClient event timelines", function() {
595601
// @ts-ignore
596602
client.clientOpts.experimentalThreadSupport = true;
597603
Thread.setServerSideSupport(FeatureSupport.Experimental);
598-
client.stopClient(); // we don't need the client to be syncing at this time
604+
await client.stopClient(); // we don't need the client to be syncing at this time
599605
const room = client.getRoom(roomId)!;
600-
const thread = room.createThread(THREAD_ROOT.event_id!, undefined, [], false);
601-
const timelineSet = thread.timelineSet;
602606

603-
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" + encodeURIComponent(THREAD_REPLY.event_id!))
607+
httpBackend.when("GET", "/rooms/!foo%3Abar/event/" + encodeURIComponent(THREAD_ROOT.event_id!))
608+
.respond(200, function() {
609+
return THREAD_ROOT;
610+
});
611+
612+
httpBackend.when("GET", "/rooms/!foo%3Abar/relations/" +
613+
encodeURIComponent(THREAD_ROOT.event_id!) + "/" +
614+
encodeURIComponent(THREAD_RELATION_TYPE.name) + "?dir=b&limit=1")
604615
.respond(200, function() {
605616
return {
606-
start: "start_token0",
607-
events_before: [],
608-
event: THREAD_REPLY,
609-
events_after: [],
610-
end: "end_token0",
611-
state: [],
617+
original_event: THREAD_ROOT,
618+
chunk: [THREAD_REPLY],
619+
// no next batch as this is the oldest end of the timeline
612620
};
613621
});
614622

623+
const thread = room.createThread(THREAD_ROOT.event_id!, undefined, [], false);
624+
await httpBackend.flushAllExpected();
625+
const timelineSet = thread.timelineSet;
626+
627+
const timelinePromise = client.getEventTimeline(timelineSet, THREAD_REPLY.event_id!);
628+
const timeline = await timelinePromise;
629+
630+
expect(timeline!.getEvents().find(e => e.getId() === THREAD_ROOT.event_id!)).toBeTruthy();
631+
expect(timeline!.getEvents().find(e => e.getId() === THREAD_REPLY.event_id!)).toBeTruthy();
632+
});
633+
634+
it("should handle thread replies with server support by fetching a contiguous thread timeline", async () => {
635+
// @ts-ignore
636+
client.clientOpts.experimentalThreadSupport = true;
637+
Thread.setServerSideSupport(FeatureSupport.Experimental);
638+
await client.stopClient(); // we don't need the client to be syncing at this time
639+
const room = client.getRoom(roomId)!;
640+
615641
httpBackend.when("GET", "/rooms/!foo%3Abar/event/" + encodeURIComponent(THREAD_ROOT.event_id!))
616642
.respond(200, function() {
617643
return THREAD_ROOT;
618644
});
619645

620646
httpBackend.when("GET", "/rooms/!foo%3Abar/relations/" +
621647
encodeURIComponent(THREAD_ROOT.event_id!) + "/" +
622-
encodeURIComponent(THREAD_RELATION_TYPE.name) + "?limit=20")
648+
encodeURIComponent(THREAD_RELATION_TYPE.name) + "?dir=b&limit=1")
623649
.respond(200, function() {
624650
return {
625651
original_event: THREAD_ROOT,
@@ -628,9 +654,11 @@ describe("MatrixClient event timelines", function() {
628654
};
629655
});
630656

631-
const timelinePromise = client.getEventTimeline(timelineSet, THREAD_REPLY.event_id!);
657+
const thread = room.createThread(THREAD_ROOT.event_id!, undefined, [], false);
632658
await httpBackend.flushAllExpected();
659+
const timelineSet = thread.timelineSet;
633660

661+
const timelinePromise = client.getEventTimeline(timelineSet, THREAD_REPLY.event_id!);
634662
const timeline = await timelinePromise;
635663

636664
expect(timeline!.getEvents().find(e => e.getId() === THREAD_ROOT.event_id!)).toBeTruthy();
@@ -1025,10 +1053,6 @@ describe("MatrixClient event timelines", function() {
10251053
});
10261054

10271055
describe("paginateEventTimeline for thread list timeline", function() {
1028-
async function flushHttp<T>(promise: Promise<T>): Promise<T> {
1029-
return Promise.all([promise, httpBackend.flushAllExpected()]).then(([result]) => result);
1030-
}
1031-
10321056
const RANDOM_TOKEN = "7280349c7bee430f91defe2a38a0a08c";
10331057

10341058
function respondToFilter(): ExpectedHttpRequest {
@@ -1050,7 +1074,7 @@ describe("MatrixClient event timelines", function() {
10501074
next_batch: RANDOM_TOKEN as string | null,
10511075
},
10521076
): ExpectedHttpRequest {
1053-
const request = httpBackend.when("GET", encodeUri("/_matrix/client/r0/rooms/$roomId/threads", {
1077+
const request = httpBackend.when("GET", encodeUri("/_matrix/client/v1/rooms/$roomId/threads", {
10541078
$roomId: roomId,
10551079
}));
10561080
request.respond(200, response);
@@ -1089,8 +1113,9 @@ describe("MatrixClient event timelines", function() {
10891113
beforeEach(() => {
10901114
// @ts-ignore
10911115
client.clientOpts.experimentalThreadSupport = true;
1092-
Thread.setServerSideSupport(FeatureSupport.Experimental);
1116+
Thread.setServerSideSupport(FeatureSupport.Stable);
10931117
Thread.setServerSideListSupport(FeatureSupport.Stable);
1118+
Thread.setServerSideFwdPaginationSupport(FeatureSupport.Stable);
10941119
});
10951120

10961121
async function testPagination(timelineSet: EventTimelineSet, direction: Direction) {
@@ -1111,7 +1136,7 @@ describe("MatrixClient event timelines", function() {
11111136

11121137
it("should allow you to paginate all threads backwards", async function() {
11131138
const room = client.getRoom(roomId);
1114-
const timelineSets = await (room?.createThreadsTimelineSets());
1139+
const timelineSets = await room!.createThreadsTimelineSets();
11151140
expect(timelineSets).not.toBeNull();
11161141
const [allThreads, myThreads] = timelineSets!;
11171142
await testPagination(allThreads, Direction.Backward);
@@ -1120,7 +1145,7 @@ describe("MatrixClient event timelines", function() {
11201145

11211146
it("should allow you to paginate all threads forwards", async function() {
11221147
const room = client.getRoom(roomId);
1123-
const timelineSets = await (room?.createThreadsTimelineSets());
1148+
const timelineSets = await room!.createThreadsTimelineSets();
11241149
expect(timelineSets).not.toBeNull();
11251150
const [allThreads, myThreads] = timelineSets!;
11261151

@@ -1130,7 +1155,7 @@ describe("MatrixClient event timelines", function() {
11301155

11311156
it("should allow fetching all threads", async function() {
11321157
const room = client.getRoom(roomId)!;
1133-
const timelineSets = await room.createThreadsTimelineSets();
1158+
const timelineSets = await room!.createThreadsTimelineSets();
11341159
expect(timelineSets).not.toBeNull();
11351160
respondToThreads();
11361161
respondToThreads();
@@ -1418,74 +1443,115 @@ describe("MatrixClient event timelines", function() {
14181443
});
14191444
});
14201445

1421-
it("should re-insert room IDs for bundled thread relation events", async () => {
1422-
// @ts-ignore
1423-
client.clientOpts.experimentalThreadSupport = true;
1424-
Thread.setServerSideSupport(FeatureSupport.Experimental);
1425-
1426-
httpBackend.when("GET", "/sync").respond(200, {
1427-
next_batch: "s_5_4",
1428-
rooms: {
1429-
join: {
1430-
[roomId]: {
1431-
timeline: {
1432-
events: [
1433-
SYNC_THREAD_ROOT,
1434-
],
1435-
prev_batch: "f_1_1",
1446+
describe("should re-insert room IDs for bundled thread relation events", () => {
1447+
async function doTest() {
1448+
httpBackend.when("GET", "/sync").respond(200, {
1449+
next_batch: "s_5_4",
1450+
rooms: {
1451+
join: {
1452+
[roomId]: {
1453+
timeline: {
1454+
events: [
1455+
SYNC_THREAD_ROOT,
1456+
],
1457+
prev_batch: "f_1_1",
1458+
},
14361459
},
14371460
},
14381461
},
1439-
},
1440-
});
1441-
await Promise.all([httpBackend.flushAllExpected(), utils.syncPromise(client)]);
1462+
});
1463+
await Promise.all([httpBackend.flushAllExpected(), utils.syncPromise(client)]);
14421464

1443-
const room = client.getRoom(roomId)!;
1444-
const thread = room.getThread(THREAD_ROOT.event_id!)!;
1445-
const timelineSet = thread.timelineSet;
1465+
const room = client.getRoom(roomId)!;
1466+
const thread = room.getThread(THREAD_ROOT.event_id!)!;
1467+
const timelineSet = thread.timelineSet;
14461468

1447-
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" + encodeURIComponent(THREAD_ROOT.event_id!))
1448-
.respond(200, {
1449-
start: "start_token",
1450-
events_before: [],
1451-
event: THREAD_ROOT,
1452-
events_after: [],
1453-
state: [],
1454-
end: "end_token",
1455-
});
1456-
httpBackend.when("GET", "/rooms/!foo%3Abar/relations/" +
1457-
encodeURIComponent(THREAD_ROOT.event_id!) + "/" +
1458-
encodeURIComponent(THREAD_RELATION_TYPE.name) + "?limit=20")
1459-
.respond(200, function() {
1460-
return {
1461-
original_event: THREAD_ROOT,
1462-
chunk: [THREAD_REPLY],
1463-
// no next batch as this is the oldest end of the timeline
1464-
};
1465-
});
1466-
await Promise.all([
1467-
client.getEventTimeline(timelineSet, THREAD_ROOT.event_id!),
1468-
httpBackend.flushAllExpected(),
1469-
]);
1469+
const buildParams = (direction: Direction, token: string): string => {
1470+
if (Thread.hasServerSideFwdPaginationSupport === FeatureSupport.Experimental) {
1471+
return `?from=${token}&org.matrix.msc3715.dir=${direction}`;
1472+
} else {
1473+
return `?dir=${direction}&from=${token}`;
1474+
}
1475+
};
14701476

1471-
httpBackend.when("GET", "/sync").respond(200, {
1472-
next_batch: "s_5_5",
1473-
rooms: {
1474-
join: {
1475-
[roomId]: {
1476-
timeline: {
1477-
events: [
1478-
SYNC_THREAD_REPLY,
1479-
],
1480-
prev_batch: "f_1_2",
1477+
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" + encodeURIComponent(THREAD_ROOT.event_id!))
1478+
.respond(200, {
1479+
start: "start_token",
1480+
events_before: [],
1481+
event: THREAD_ROOT,
1482+
events_after: [],
1483+
state: [],
1484+
end: "end_token",
1485+
});
1486+
httpBackend.when("GET", "/rooms/!foo%3Abar/relations/" +
1487+
encodeURIComponent(THREAD_ROOT.event_id!) + "/" +
1488+
encodeURIComponent(THREAD_RELATION_TYPE.name) + buildParams(Direction.Backward, "start_token"))
1489+
.respond(200, function() {
1490+
return {
1491+
original_event: THREAD_ROOT,
1492+
chunk: [],
1493+
};
1494+
});
1495+
httpBackend.when("GET", "/rooms/!foo%3Abar/relations/" +
1496+
encodeURIComponent(THREAD_ROOT.event_id!) + "/" +
1497+
encodeURIComponent(THREAD_RELATION_TYPE.name) + buildParams(Direction.Forward, "end_token"))
1498+
.respond(200, function() {
1499+
return {
1500+
original_event: THREAD_ROOT,
1501+
chunk: [THREAD_REPLY],
1502+
};
1503+
});
1504+
const timeline = await flushHttp(client.getEventTimeline(timelineSet, THREAD_ROOT.event_id!));
1505+
1506+
httpBackend.when("GET", "/sync").respond(200, {
1507+
next_batch: "s_5_5",
1508+
rooms: {
1509+
join: {
1510+
[roomId]: {
1511+
timeline: {
1512+
events: [
1513+
SYNC_THREAD_REPLY,
1514+
],
1515+
prev_batch: "f_1_2",
1516+
},
14811517
},
14821518
},
14831519
},
1484-
},
1520+
});
1521+
1522+
await Promise.all([httpBackend.flushAllExpected(), utils.syncPromise(client)]);
1523+
1524+
expect(timeline!.getEvents()[1]!.event).toEqual(THREAD_REPLY);
1525+
}
1526+
1527+
it("in stable mode", async () => {
1528+
// @ts-ignore
1529+
client.clientOpts.experimentalThreadSupport = true;
1530+
Thread.setServerSideSupport(FeatureSupport.Stable);
1531+
Thread.setServerSideListSupport(FeatureSupport.Stable);
1532+
Thread.setServerSideFwdPaginationSupport(FeatureSupport.Stable);
1533+
1534+
return doTest();
14851535
});
14861536

1487-
await Promise.all([httpBackend.flushAllExpected(), utils.syncPromise(client)]);
1537+
it("in backwards compatible unstable mode", async () => {
1538+
// @ts-ignore
1539+
client.clientOpts.experimentalThreadSupport = true;
1540+
Thread.setServerSideSupport(FeatureSupport.Experimental);
1541+
Thread.setServerSideListSupport(FeatureSupport.Experimental);
1542+
Thread.setServerSideFwdPaginationSupport(FeatureSupport.Experimental);
14881543

1489-
expect(thread.liveTimeline.getEvents()[1].event).toEqual(THREAD_REPLY);
1544+
return doTest();
1545+
});
1546+
1547+
it("in backwards compatible mode", async () => {
1548+
// @ts-ignore
1549+
client.clientOpts.experimentalThreadSupport = true;
1550+
Thread.setServerSideSupport(FeatureSupport.Experimental);
1551+
Thread.setServerSideListSupport(FeatureSupport.None);
1552+
Thread.setServerSideFwdPaginationSupport(FeatureSupport.None);
1553+
1554+
return doTest();
1555+
});
14901556
});
14911557
});

spec/integ/matrix-client-relations.spec.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ describe("MatrixClient relations", () => {
6060

6161
await httpBackend!.flushAllExpected();
6262

63-
expect(await response).toEqual({ "events": [], "nextBatch": "NEXT" });
63+
expect(await response).toEqual({ "events": [], "nextBatch": "NEXT", "originalEvent": null, "prevBatch": null });
6464
});
6565

6666
it("should read related events with relation type", async () => {
@@ -72,7 +72,7 @@ describe("MatrixClient relations", () => {
7272

7373
await httpBackend!.flushAllExpected();
7474

75-
expect(await response).toEqual({ "events": [], "nextBatch": "NEXT" });
75+
expect(await response).toEqual({ "events": [], "nextBatch": "NEXT", "originalEvent": null, "prevBatch": null });
7676
});
7777

7878
it("should read related events with relation type and event type", async () => {
@@ -87,7 +87,7 @@ describe("MatrixClient relations", () => {
8787

8888
await httpBackend!.flushAllExpected();
8989

90-
expect(await response).toEqual({ "events": [], "nextBatch": "NEXT" });
90+
expect(await response).toEqual({ "events": [], "nextBatch": "NEXT", "originalEvent": null, "prevBatch": null });
9191
});
9292

9393
it("should read related events with custom options", async () => {
@@ -107,7 +107,7 @@ describe("MatrixClient relations", () => {
107107

108108
await httpBackend!.flushAllExpected();
109109

110-
expect(await response).toEqual({ "events": [], "nextBatch": "NEXT" });
110+
expect(await response).toEqual({ "events": [], "nextBatch": "NEXT", "originalEvent": null, "prevBatch": null });
111111
});
112112

113113
it('should use default direction in the fetchRelations endpoint', async () => {

0 commit comments

Comments
 (0)