diff --git a/spec/integ/matrix-client-event-timeline.spec.ts b/spec/integ/matrix-client-event-timeline.spec.ts index c7083db1968..49e472e76aa 100644 --- a/spec/integ/matrix-client-event-timeline.spec.ts +++ b/spec/integ/matrix-client-event-timeline.spec.ts @@ -29,6 +29,7 @@ import { PendingEventOrdering, RelationType, Room, + UNSIGNED_THREAD_ID_FIELD, } from "../../src/matrix"; import { logger } from "../../src/logger"; import { encodeParams, encodeUri, QueryDict, replaceParam } from "../../src/utils"; @@ -52,12 +53,16 @@ const withoutRoomId = (e: Partial): Partial => { /** * Our httpBackend only allows matching calls if we have the exact same query, in the exact same order * This method allows building queries with the exact same parameter order as the fetchRelations method in client + * @param client Matrix client to mock the request for * @param params query parameters */ -const buildRelationPaginationQuery = (params: QueryDict): string => { +const buildRelationPaginationQuery = (client: MatrixClient, params: QueryDict): string => { if (Thread.hasServerSideFwdPaginationSupport === FeatureSupport.Experimental) { params = replaceParam("dir", "org.matrix.msc3715.dir", params); } + if (client.canSupport.get(Feature.RelationsRecursion) === ServerSupport.Unstable) { + params = replaceParam("recurse", "org.matrix.msc3981.recurse", params); + } return "?" + encodeParams(params).toString(); }; @@ -179,6 +184,9 @@ const THREAD_REPLY = utils.mkEvent({ event_id: THREAD_ROOT.event_id, }, }, + unsigned: { + [UNSIGNED_THREAD_ID_FIELD.name]: THREAD_ROOT.event_id, + }, event: false, }); @@ -622,7 +630,7 @@ describe("MatrixClient event timelines", function () { encodeURIComponent(THREAD_ROOT.event_id!) + "/" + encodeURIComponent(THREAD_RELATION_TYPE.name) + - buildRelationPaginationQuery({ dir: Direction.Backward, limit: 1 }), + buildRelationPaginationQuery(client, { dir: Direction.Backward, limit: 1 }), ) .respond(200, function () { return { @@ -1020,6 +1028,87 @@ describe("MatrixClient event timelines", function () { ]); }); + it("should use recursive relations to paginate thread timelines", async function () { + function respondToThreads( + response = { + chunk: [THREAD_ROOT], + state: [], + next_batch: null, + }, + ): ExpectedHttpRequest { + const request = httpBackend.when( + "GET", + encodeUri("/_matrix/client/v1/rooms/$roomId/threads", { + $roomId: roomId, + }), + ); + request.respond(200, response); + return request; + } + + function respondToThread( + root: Partial, + replies: Partial[], + limit?: number, + ): ExpectedHttpRequest { + const request = httpBackend.when( + "GET", + "/_matrix/client/v1/rooms/!foo%3Abar/relations/" + + encodeURIComponent(root.event_id!) + + buildRelationPaginationQuery(client, { + dir: Direction.Backward, + limit: limit, + recurse: true, + }), + ); + request.respond(200, function () { + return { + original_event: root, + chunk: replies, + // no next batch as this is the oldest end of the timeline + }; + }); + return request; + } + + function respondToEvent(event: Partial = THREAD_ROOT): ExpectedHttpRequest { + const request = httpBackend.when( + "GET", + encodeUri("/_matrix/client/r0/rooms/$roomId/event/$eventId", { + $roomId: roomId, + $eventId: event.event_id!, + }), + ); + request.respond(200, event); + return request; + } + + // Setup + // @ts-ignore + client.clientOpts.threadSupport = true; + Thread.setServerSideSupport(FeatureSupport.Stable); + Thread.setServerSideListSupport(FeatureSupport.Stable); + Thread.setServerSideFwdPaginationSupport(FeatureSupport.Stable); + client.canSupport.set(Feature.RelationsRecursion, ServerSupport.Unstable); + const room = client.getRoom(roomId)!; + await room!.createThreadsTimelineSets(); + respondToThreads(); + respondToThreads(); + respondToEvent(); + respondToEvent(); + respondToEvent(); + respondToEvent(); + respondToEvent(); + respondToThread(THREAD_ROOT, [THREAD_REPLY], 1); + await flushHttp(room.fetchRoomThreads()); + const thread = room.getThread(THREAD_ROOT.event_id!)!; + expect(thread).not.toBeNull(); + respondToThread(THREAD_ROOT, [THREAD_REPLY], 1); + expect(thread.timelineSet.thread).toBe(thread); + expect(Thread.hasServerSideSupport).toBe(FeatureSupport.Stable); + await flushHttp(client.getLatestTimeline(thread.timelineSet)); + }); + it("should create threads for thread roots discovered", function () { const room = client.getRoom(roomId)!; const timelineSet = room.getTimelineSets()[0]; @@ -1091,6 +1180,9 @@ describe("MatrixClient event timelines", function () { event_id: THREAD_ROOT.event_id, }, }, + unsigned: { + [UNSIGNED_THREAD_ID_FIELD.name]: THREAD_ROOT.event_id, + }, event: true, }); THREAD_REPLY2.localTimestamp += 1000; @@ -1109,6 +1201,9 @@ describe("MatrixClient event timelines", function () { event_id: THREAD_ROOT.event_id, }, }, + unsigned: { + [UNSIGNED_THREAD_ID_FIELD.name]: THREAD_ROOT.event_id, + }, event: true, }); THREAD_REPLY3.localTimestamp += 2000; @@ -1182,6 +1277,9 @@ describe("MatrixClient event timelines", function () { event_id: THREAD_ROOT.event_id, }, }, + unsigned: { + [UNSIGNED_THREAD_ID_FIELD.name]: THREAD_ROOT.event_id, + }, event: true, }); THREAD_REPLY2.localTimestamp += 1000; @@ -1214,6 +1312,9 @@ describe("MatrixClient event timelines", function () { event_id: THREAD_ROOT.event_id, }, }, + unsigned: { + [UNSIGNED_THREAD_ID_FIELD.name]: THREAD_ROOT.event_id, + }, event: true, }); THREAD_REPLY3.localTimestamp += 3000; @@ -1254,9 +1355,7 @@ describe("MatrixClient event timelines", function () { "GET", "/_matrix/client/v1/rooms/!foo%3Abar/relations/" + encodeURIComponent(THREAD_ROOT_UPDATED.event_id!) + - "/" + - encodeURIComponent(THREAD_RELATION_TYPE.name) + - buildRelationPaginationQuery({ + buildRelationPaginationQuery(client, { dir: Direction.Backward, limit: 3, recurse: true, @@ -1904,7 +2003,7 @@ describe("MatrixClient event timelines", function () { encodeURIComponent(THREAD_ROOT.event_id!) + "/" + encodeURIComponent(THREAD_RELATION_TYPE.name) + - buildRelationPaginationQuery({ dir: Direction.Backward, limit: 1 }), + buildRelationPaginationQuery(client, { dir: Direction.Backward, limit: 1 }), ) .respond(200, function () { return { @@ -1957,7 +2056,7 @@ describe("MatrixClient event timelines", function () { encodeURIComponent(THREAD_ROOT.event_id!) + "/" + encodeURIComponent(THREAD_RELATION_TYPE.name) + - buildRelationPaginationQuery({ + buildRelationPaginationQuery(client, { dir: Direction.Backward, from: "start_token", }), @@ -1974,7 +2073,7 @@ describe("MatrixClient event timelines", function () { encodeURIComponent(THREAD_ROOT.event_id!) + "/" + encodeURIComponent(THREAD_RELATION_TYPE.name) + - buildRelationPaginationQuery({ dir: Direction.Forward, from: "end_token" }), + buildRelationPaginationQuery(client, { dir: Direction.Forward, from: "end_token" }), ) .respond(200, function () { return { diff --git a/spec/unit/room.spec.ts b/spec/unit/room.spec.ts index 2ed44dffd7b..4e2292810eb 100644 --- a/spec/unit/room.spec.ts +++ b/spec/unit/room.spec.ts @@ -42,6 +42,7 @@ import { RelationType, RoomEvent, RoomMember, + UNSIGNED_THREAD_ID_FIELD, } from "../../src"; import { EventTimeline } from "../../src/models/event-timeline"; import { NotificationCountType, Room } from "../../src/models/room"; @@ -133,6 +134,9 @@ describe("Room", function () { "rel_type": "m.thread", }, }, + unsigned: { + [UNSIGNED_THREAD_ID_FIELD.name]: root.getId(), + }, }, room.client, ); diff --git a/src/client.ts b/src/client.ts index caca1a89a82..9bd8d7e7421 100644 --- a/src/client.ts +++ b/src/client.ts @@ -151,6 +151,7 @@ import { UNSTABLE_MSC3088_PURPOSE, UNSTABLE_MSC3089_TREE_SUBTYPE, MSC3912_RELATION_BASED_REDACTIONS_PROP, + UNSIGNED_THREAD_ID_FIELD, } from "./@types/event"; import { IdServerUnbindResult, IImageInfo, Preset, Visibility } from "./@types/partials"; import { EventMapper, eventMapperFor, MapperOpts } from "./event-mapper"; @@ -5750,18 +5751,19 @@ export class MatrixClient extends TypedEventEmitter = res.end; while (nextBatch) { const resNewer: IRelationsResponse = await this.fetchRelations( timelineSet.room.roomId, thread.id, - THREAD_RELATION_TYPE.name, + relType, null, { dir: Direction.Forward, from: nextBatch, recurse: recurse || undefined }, ); @@ -5835,6 +5840,12 @@ export class MatrixClient extends TypedEventEmitter