Skip to content

Commit

Permalink
Loading threads with server-side assistance (#2735)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
3 people authored Oct 28, 2022
1 parent b447871 commit 068fbb7
Show file tree
Hide file tree
Showing 11 changed files with 872 additions and 471 deletions.
222 changes: 144 additions & 78 deletions spec/integ/matrix-client-event-timeline.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -342,8 +342,14 @@ describe("MatrixClient event timelines", function() {
httpBackend.verifyNoOutstandingExpectation();
client.stopClient();
Thread.setServerSideSupport(FeatureSupport.None);
Thread.setServerSideListSupport(FeatureSupport.None);
Thread.setServerSideFwdPaginationSupport(FeatureSupport.None);
});

async function flushHttp<T>(promise: Promise<T>): Promise<T> {
return Promise.all([promise, httpBackend.flushAllExpected()]).then(([result]) => result);
}

describe("getEventTimeline", function() {
it("should create a new timeline for new events", function() {
const room = client.getRoom(roomId)!;
Expand Down Expand Up @@ -595,31 +601,51 @@ describe("MatrixClient event timelines", function() {
// @ts-ignore
client.clientOpts.experimentalThreadSupport = true;
Thread.setServerSideSupport(FeatureSupport.Experimental);
client.stopClient(); // we don't need the client to be syncing at this time
await client.stopClient(); // we don't need the client to be syncing at this time
const room = client.getRoom(roomId)!;
const thread = room.createThread(THREAD_ROOT.event_id!, undefined, [], false);
const timelineSet = thread.timelineSet;

httpBackend.when("GET", "/rooms/!foo%3Abar/context/" + encodeURIComponent(THREAD_REPLY.event_id!))
httpBackend.when("GET", "/rooms/!foo%3Abar/event/" + encodeURIComponent(THREAD_ROOT.event_id!))
.respond(200, function() {
return THREAD_ROOT;
});

httpBackend.when("GET", "/rooms/!foo%3Abar/relations/" +
encodeURIComponent(THREAD_ROOT.event_id!) + "/" +
encodeURIComponent(THREAD_RELATION_TYPE.name) + "?dir=b&limit=1")
.respond(200, function() {
return {
start: "start_token0",
events_before: [],
event: THREAD_REPLY,
events_after: [],
end: "end_token0",
state: [],
original_event: THREAD_ROOT,
chunk: [THREAD_REPLY],
// no next batch as this is the oldest end of the timeline
};
});

const thread = room.createThread(THREAD_ROOT.event_id!, undefined, [], false);
await httpBackend.flushAllExpected();
const timelineSet = thread.timelineSet;

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

expect(timeline!.getEvents().find(e => e.getId() === THREAD_ROOT.event_id!)).toBeTruthy();
expect(timeline!.getEvents().find(e => e.getId() === THREAD_REPLY.event_id!)).toBeTruthy();
});

it("should handle thread replies with server support by fetching a contiguous thread timeline", async () => {
// @ts-ignore
client.clientOpts.experimentalThreadSupport = true;
Thread.setServerSideSupport(FeatureSupport.Experimental);
await client.stopClient(); // we don't need the client to be syncing at this time
const room = client.getRoom(roomId)!;

httpBackend.when("GET", "/rooms/!foo%3Abar/event/" + encodeURIComponent(THREAD_ROOT.event_id!))
.respond(200, function() {
return THREAD_ROOT;
});

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

const timelinePromise = client.getEventTimeline(timelineSet, THREAD_REPLY.event_id!);
const thread = room.createThread(THREAD_ROOT.event_id!, undefined, [], false);
await httpBackend.flushAllExpected();
const timelineSet = thread.timelineSet;

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

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

describe("paginateEventTimeline for thread list timeline", function() {
async function flushHttp<T>(promise: Promise<T>): Promise<T> {
return Promise.all([promise, httpBackend.flushAllExpected()]).then(([result]) => result);
}

const RANDOM_TOKEN = "7280349c7bee430f91defe2a38a0a08c";

function respondToFilter(): ExpectedHttpRequest {
Expand All @@ -1050,7 +1074,7 @@ describe("MatrixClient event timelines", function() {
next_batch: RANDOM_TOKEN as string | null,
},
): ExpectedHttpRequest {
const request = httpBackend.when("GET", encodeUri("/_matrix/client/r0/rooms/$roomId/threads", {
const request = httpBackend.when("GET", encodeUri("/_matrix/client/v1/rooms/$roomId/threads", {
$roomId: roomId,
}));
request.respond(200, response);
Expand Down Expand Up @@ -1089,8 +1113,9 @@ describe("MatrixClient event timelines", function() {
beforeEach(() => {
// @ts-ignore
client.clientOpts.experimentalThreadSupport = true;
Thread.setServerSideSupport(FeatureSupport.Experimental);
Thread.setServerSideSupport(FeatureSupport.Stable);
Thread.setServerSideListSupport(FeatureSupport.Stable);
Thread.setServerSideFwdPaginationSupport(FeatureSupport.Stable);
});

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

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

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

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

it("should allow fetching all threads", async function() {
const room = client.getRoom(roomId)!;
const timelineSets = await room.createThreadsTimelineSets();
const timelineSets = await room!.createThreadsTimelineSets();
expect(timelineSets).not.toBeNull();
respondToThreads();
respondToThreads();
Expand Down Expand Up @@ -1418,74 +1443,115 @@ describe("MatrixClient event timelines", function() {
});
});

it("should re-insert room IDs for bundled thread relation events", async () => {
// @ts-ignore
client.clientOpts.experimentalThreadSupport = true;
Thread.setServerSideSupport(FeatureSupport.Experimental);

httpBackend.when("GET", "/sync").respond(200, {
next_batch: "s_5_4",
rooms: {
join: {
[roomId]: {
timeline: {
events: [
SYNC_THREAD_ROOT,
],
prev_batch: "f_1_1",
describe("should re-insert room IDs for bundled thread relation events", () => {
async function doTest() {
httpBackend.when("GET", "/sync").respond(200, {
next_batch: "s_5_4",
rooms: {
join: {
[roomId]: {
timeline: {
events: [
SYNC_THREAD_ROOT,
],
prev_batch: "f_1_1",
},
},
},
},
},
});
await Promise.all([httpBackend.flushAllExpected(), utils.syncPromise(client)]);
});
await Promise.all([httpBackend.flushAllExpected(), utils.syncPromise(client)]);

const room = client.getRoom(roomId)!;
const thread = room.getThread(THREAD_ROOT.event_id!)!;
const timelineSet = thread.timelineSet;
const room = client.getRoom(roomId)!;
const thread = room.getThread(THREAD_ROOT.event_id!)!;
const timelineSet = thread.timelineSet;

httpBackend.when("GET", "/rooms/!foo%3Abar/context/" + encodeURIComponent(THREAD_ROOT.event_id!))
.respond(200, {
start: "start_token",
events_before: [],
event: THREAD_ROOT,
events_after: [],
state: [],
end: "end_token",
});
httpBackend.when("GET", "/rooms/!foo%3Abar/relations/" +
encodeURIComponent(THREAD_ROOT.event_id!) + "/" +
encodeURIComponent(THREAD_RELATION_TYPE.name) + "?limit=20")
.respond(200, function() {
return {
original_event: THREAD_ROOT,
chunk: [THREAD_REPLY],
// no next batch as this is the oldest end of the timeline
};
});
await Promise.all([
client.getEventTimeline(timelineSet, THREAD_ROOT.event_id!),
httpBackend.flushAllExpected(),
]);
const buildParams = (direction: Direction, token: string): string => {
if (Thread.hasServerSideFwdPaginationSupport === FeatureSupport.Experimental) {
return `?from=${token}&org.matrix.msc3715.dir=${direction}`;
} else {
return `?dir=${direction}&from=${token}`;
}
};

httpBackend.when("GET", "/sync").respond(200, {
next_batch: "s_5_5",
rooms: {
join: {
[roomId]: {
timeline: {
events: [
SYNC_THREAD_REPLY,
],
prev_batch: "f_1_2",
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" + encodeURIComponent(THREAD_ROOT.event_id!))
.respond(200, {
start: "start_token",
events_before: [],
event: THREAD_ROOT,
events_after: [],
state: [],
end: "end_token",
});
httpBackend.when("GET", "/rooms/!foo%3Abar/relations/" +
encodeURIComponent(THREAD_ROOT.event_id!) + "/" +
encodeURIComponent(THREAD_RELATION_TYPE.name) + buildParams(Direction.Backward, "start_token"))
.respond(200, function() {
return {
original_event: THREAD_ROOT,
chunk: [],
};
});
httpBackend.when("GET", "/rooms/!foo%3Abar/relations/" +
encodeURIComponent(THREAD_ROOT.event_id!) + "/" +
encodeURIComponent(THREAD_RELATION_TYPE.name) + buildParams(Direction.Forward, "end_token"))
.respond(200, function() {
return {
original_event: THREAD_ROOT,
chunk: [THREAD_REPLY],
};
});
const timeline = await flushHttp(client.getEventTimeline(timelineSet, THREAD_ROOT.event_id!));

httpBackend.when("GET", "/sync").respond(200, {
next_batch: "s_5_5",
rooms: {
join: {
[roomId]: {
timeline: {
events: [
SYNC_THREAD_REPLY,
],
prev_batch: "f_1_2",
},
},
},
},
},
});

await Promise.all([httpBackend.flushAllExpected(), utils.syncPromise(client)]);

expect(timeline!.getEvents()[1]!.event).toEqual(THREAD_REPLY);
}

it("in stable mode", async () => {
// @ts-ignore
client.clientOpts.experimentalThreadSupport = true;
Thread.setServerSideSupport(FeatureSupport.Stable);
Thread.setServerSideListSupport(FeatureSupport.Stable);
Thread.setServerSideFwdPaginationSupport(FeatureSupport.Stable);

return doTest();
});

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

expect(thread.liveTimeline.getEvents()[1].event).toEqual(THREAD_REPLY);
return doTest();
});

it("in backwards compatible mode", async () => {
// @ts-ignore
client.clientOpts.experimentalThreadSupport = true;
Thread.setServerSideSupport(FeatureSupport.Experimental);
Thread.setServerSideListSupport(FeatureSupport.None);
Thread.setServerSideFwdPaginationSupport(FeatureSupport.None);

return doTest();
});
});
});
8 changes: 4 additions & 4 deletions spec/integ/matrix-client-relations.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ describe("MatrixClient relations", () => {

await httpBackend!.flushAllExpected();

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

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

await httpBackend!.flushAllExpected();

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

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

await httpBackend!.flushAllExpected();

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

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

await httpBackend!.flushAllExpected();

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

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

0 comments on commit 068fbb7

Please sign in to comment.