From 3d99e700ea815b2b155c430ac7ef119637a3892e Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski Date: Wed, 14 Sep 2022 16:26:53 +0200 Subject: [PATCH 01/17] feature detection code for thread list api --- .../matrix-client-event-timeline.spec.ts | 14 ++++---- spec/unit/room.spec.ts | 4 +-- src/client.ts | 36 ++++++++++--------- src/models/thread.ts | 33 ++++++++++++++--- 4 files changed, 57 insertions(+), 30 deletions(-) diff --git a/spec/integ/matrix-client-event-timeline.spec.ts b/spec/integ/matrix-client-event-timeline.spec.ts index c656f6447e2..1c262052d3a 100644 --- a/spec/integ/matrix-client-event-timeline.spec.ts +++ b/spec/integ/matrix-client-event-timeline.spec.ts @@ -18,7 +18,7 @@ import * as utils from "../test-utils/test-utils"; import { ClientEvent, EventTimeline, Filter, IEvent, MatrixClient, MatrixEvent, Room } from "../../src/matrix"; import { logger } from "../../src/logger"; import { TestClient } from "../TestClient"; -import { Thread, THREAD_RELATION_TYPE } from "../../src/models/thread"; +import { FeatureSupport, Thread, THREAD_RELATION_TYPE } from "../../src/models/thread"; const userId = "@alice:localhost"; const userName = "Alice"; @@ -299,7 +299,7 @@ describe("MatrixClient event timelines", function() { afterEach(function() { httpBackend.verifyNoOutstandingExpectation(); client.stopClient(); - Thread.setServerSideSupport(false, false); + Thread.setServerSideSupport(FeatureSupport.None); }); describe("getEventTimeline", function() { @@ -552,7 +552,7 @@ describe("MatrixClient event timelines", function() { it("should handle thread replies with server support by fetching a contiguous thread timeline", async () => { // @ts-ignore client.clientOpts.experimentalThreadSupport = true; - Thread.setServerSideSupport(true, false); + Thread.setServerSideSupport(FeatureSupport.Experimental); 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); @@ -598,7 +598,7 @@ describe("MatrixClient event timelines", function() { it("should return relevant timeline from non-thread timelineSet when asking for the thread root", async () => { // @ts-ignore client.clientOpts.experimentalThreadSupport = true; - Thread.setServerSideSupport(true, false); + Thread.setServerSideSupport(FeatureSupport.Experimental); client.stopClient(); // we don't need the client to be syncing at this time const room = client.getRoom(roomId); const threadRoot = new MatrixEvent(THREAD_ROOT); @@ -630,7 +630,7 @@ describe("MatrixClient event timelines", function() { it("should return undefined when event is not in the thread that the given timelineSet is representing", () => { // @ts-ignore client.clientOpts.experimentalThreadSupport = true; - Thread.setServerSideSupport(true, false); + Thread.setServerSideSupport(FeatureSupport.Experimental); client.stopClient(); // we don't need the client to be syncing at this time const room = client.getRoom(roomId); const threadRoot = new MatrixEvent(THREAD_ROOT); @@ -658,7 +658,7 @@ describe("MatrixClient event timelines", function() { it("should return undefined when event is within a thread but timelineSet is not", () => { // @ts-ignore client.clientOpts.experimentalThreadSupport = true; - Thread.setServerSideSupport(true, false); + Thread.setServerSideSupport(FeatureSupport.Experimental); client.stopClient(); // we don't need the client to be syncing at this time const room = client.getRoom(roomId); const timelineSet = room.getTimelineSets()[0]; @@ -1092,7 +1092,7 @@ 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(true, false); + Thread.setServerSideSupport(FeatureSupport.Experimental); httpBackend.when("GET", "/sync").respond(200, { next_batch: "s_5_4", diff --git a/spec/unit/room.spec.ts b/spec/unit/room.spec.ts index e79fb7110cc..f790a10655e 100644 --- a/spec/unit/room.spec.ts +++ b/spec/unit/room.spec.ts @@ -38,7 +38,7 @@ import { UNSTABLE_ELEMENT_FUNCTIONAL_USERS } from "../../src/@types/event"; import { TestClient } from "../TestClient"; import { emitPromise } from "../test-utils/test-utils"; import { ReceiptType } from "../../src/@types/read_receipts"; -import { Thread, ThreadEvent } from "../../src/models/thread"; +import { FeatureSupport, Thread, ThreadEvent } from "../../src/models/thread"; import { WrappedReceipt } from "../../src/models/read-receipt"; describe("Room", function() { @@ -2408,7 +2408,7 @@ describe("Room", function() { }); it("should aggregate relations in thread event timeline set", () => { - Thread.setServerSideSupport(true, true); + Thread.setServerSideSupport(FeatureSupport.Stable); const threadRoot = mkMessage(); const rootReaction = mkReaction(threadRoot); const threadResponse = mkThreadResponse(threadRoot); diff --git a/src/client.ts b/src/client.ts index ab86cf3b16c..24ef016d21c 100644 --- a/src/client.ts +++ b/src/client.ts @@ -195,7 +195,7 @@ import { TypedEventEmitter } from "./models/typed-event-emitter"; import { ReceiptType } from "./@types/read_receipts"; import { MSC3575SlidingSyncRequest, MSC3575SlidingSyncResponse, SlidingSync } from "./sliding-sync"; import { SlidingSyncSdk } from "./sliding-sync-sdk"; -import { Thread, THREAD_RELATION_TYPE } from "./models/thread"; +import { FeatureSupport, Thread, THREAD_RELATION_TYPE, determineFeatureSupport } from "./models/thread"; import { MBeaconInfoEventContent, M_BEACON_INFO } from "./@types/beacon"; import { UnstableValue } from "./NamespacedValue"; import { ToDeviceMessageQueue } from "./ToDeviceMessageQueue"; @@ -1211,14 +1211,9 @@ export class MatrixClient extends TypedEventEmitter { + threads: FeatureSupport; + list: FeatureSupport; + }> { try { - const hasUnstableSupport = await this.doesServerSupportUnstableFeature("org.matrix.msc3440"); - const hasStableSupport = await this.doesServerSupportUnstableFeature("org.matrix.msc3440.stable"); + const [threadUnstable, threadStable, listUnstable, listStable] = await Promise.all([ + this.doesServerSupportUnstableFeature("org.matrix.msc3440"), + this.doesServerSupportUnstableFeature("org.matrix.msc3440.stable"), + this.doesServerSupportUnstableFeature("org.matrix.msc3856"), + this.doesServerSupportUnstableFeature("org.matrix.msc3856.stable"), + ]); // TODO: Use `this.isVersionSupported("v1.3")` for whatever spec version includes MSC3440 formally. return { - serverSupport: hasUnstableSupport || hasStableSupport, - stable: hasStableSupport, + threads: determineFeatureSupport(threadStable, threadUnstable), + list: determineFeatureSupport(listStable, listUnstable), }; } catch (e) { // Assume server support and stability aren't available: null/no data return. // XXX: This should just return an object with `false` booleans instead. - return null; + return { + threads: FeatureSupport.None, + list: FeatureSupport.None, + }; } } diff --git a/src/models/thread.ts b/src/models/thread.ts index 3c7add2f4d4..60757f9c316 100644 --- a/src/models/thread.ts +++ b/src/models/thread.ts @@ -51,11 +51,28 @@ interface IThreadOpts { client: MatrixClient; } +export enum FeatureSupport { + None = 0, + Experimental = 1, + Stable = 2 +} + +export function determineFeatureSupport(stable: boolean, unstable: boolean): FeatureSupport { + if (stable) { + return FeatureSupport.Stable; + } else if (unstable) { + return FeatureSupport.Experimental; + } else { + return FeatureSupport.None; + } +} + /** * @experimental */ export class Thread extends ReadReceipt { - public static hasServerSideSupport: boolean; + public static hasServerSideSupport = FeatureSupport.None; + public static hasServerSideListSupport = FeatureSupport.None; /** * A reference to all the events ID at the bottom of the threads @@ -134,15 +151,23 @@ export class Thread extends ReadReceipt { this.emit(ThreadEvent.Update, this); } - public static setServerSideSupport(hasServerSideSupport: boolean, useStable: boolean): void { - Thread.hasServerSideSupport = hasServerSideSupport; - if (!useStable) { + public static setServerSideSupport( + status: FeatureSupport, + ): void { + Thread.hasServerSideSupport = status; + if (status !== FeatureSupport.Stable) { FILTER_RELATED_BY_SENDERS.setPreferUnstable(true); FILTER_RELATED_BY_REL_TYPES.setPreferUnstable(true); THREAD_RELATION_TYPE.setPreferUnstable(true); } } + public static setServerSideListSupport( + status: FeatureSupport, + ): void { + Thread.hasServerSideListSupport = status; + } + private onBeforeRedaction = (event: MatrixEvent, redaction: MatrixEvent) => { if (event?.isRelation(THREAD_RELATION_TYPE.name) && this.room.eventShouldLiveIn(event).threadId === this.id && From c0a4d512ceffc5e88db9950ac597918f9f74b7dd Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski Date: Wed, 14 Sep 2022 16:23:20 +0200 Subject: [PATCH 02/17] fix bug where createThreadsTimelineSets would sometimes return nothing --- src/models/room.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/models/room.ts b/src/models/room.ts index 75267fa2a29..2aaa7e1f2e1 100644 --- a/src/models/room.ts +++ b/src/models/room.ts @@ -356,7 +356,7 @@ export class Room extends ReadReceipt { } private threadTimelineSetsPromise: Promise<[EventTimelineSet, EventTimelineSet]> | null = null; - public async createThreadsTimelineSets(): Promise<[EventTimelineSet, EventTimelineSet]> { + public async createThreadsTimelineSets(): Promise<[EventTimelineSet, EventTimelineSet] | null> { if (this.threadTimelineSetsPromise) { return this.threadTimelineSetsPromise; } @@ -369,6 +369,7 @@ export class Room extends ReadReceipt { ]); const timelineSets = await this.threadTimelineSetsPromise; this.threadsTimelineSets.push(...timelineSets); + return timelineSets; } catch (e) { this.threadTimelineSetsPromise = null; } From b419f37d72ef12a7ca6824b65e0b5cda91deaadb Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski Date: Mon, 26 Sep 2022 12:21:43 +0200 Subject: [PATCH 03/17] initial implementation of thread listing msc --- src/client.ts | 173 ++++++++++++++++++++++++++----- src/models/event-timeline-set.ts | 3 + src/models/event-timeline.ts | 4 +- src/models/room-state.ts | 2 +- src/models/room.ts | 105 +++++++++++++++++-- 5 files changed, 250 insertions(+), 37 deletions(-) diff --git a/src/client.ts b/src/client.ts index 24ef016d21c..cf8244a590f 100644 --- a/src/client.ts +++ b/src/client.ts @@ -19,7 +19,7 @@ limitations under the License. * @module client */ -import { EmoteEvent, IPartialEvent, MessageEvent, NoticeEvent } from "matrix-events-sdk"; +import { EmoteEvent, IPartialEvent, MessageEvent, NoticeEvent, Optional } from "matrix-events-sdk"; import { ISyncStateData, SyncApi, SyncState } from "./sync"; import { @@ -33,7 +33,7 @@ import { } from "./models/event"; import { StubStore } from "./store/stub"; import { CallEvent, CallEventHandlerMap, createNewMatrixCall, MatrixCall, supportsMatrixCall } from "./webrtc/call"; -import { Filter, IFilterDefinition } from "./filter"; +import { Filter, IFilterDefinition, IRoomEventFilter } from "./filter"; import { CallEventHandlerEvent, CallEventHandler, CallEventHandlerEventHandlerMap } from './webrtc/callEventHandler'; import * as utils from './utils'; import { sleep } from './utils'; @@ -591,6 +591,13 @@ interface IMessagesResponse { state: IStateEvent[]; } +interface IThreadedMessagesResponse { + prev_batch: string; + next_batch: string; + chunk: IRoomEvent[]; + state: IStateEvent[]; +} + export interface IRequestTokenResponse { sid: string; submit_url?: string; @@ -5343,13 +5350,17 @@ export class MatrixClient extends TypedEventEmitter { + public async getEventTimeline(timelineSet: EventTimelineSet, eventId: string): Promise> { // don't allow any timeline support unless it's been enabled. if (!this.timelineSupport) { throw new Error("timeline support is disabled. Set the 'timelineSupport'" + " parameter to true when creating MatrixClient to enable it."); } + if (!timelineSet.room) { + throw new Error("getEventTimeline only supports room timelines"); + } + if (timelineSet.getTimelineForEvent(eventId)) { return timelineSet.getTimelineForEvent(eventId); } @@ -5361,7 +5372,7 @@ export class MatrixClient extends TypedEventEmitter = undefined; + let params: Record | undefined = undefined; if (this.clientOpts.lazyLoadMembers) { params = { filter: JSON.stringify(Filter.LAZY_LOADING_MESSAGES_FILTER) }; } @@ -5454,27 +5465,36 @@ export class MatrixClient extends TypedEventEmitter { + public async getLatestTimeline(timelineSet: EventTimelineSet): Promise> { // don't allow any timeline support unless it's been enabled. if (!this.timelineSupport) { throw new Error("timeline support is disabled. Set the 'timelineSupport'" + " parameter to true when creating MatrixClient to enable it."); } - const messagesPath = utils.encodeUri( - "/rooms/$roomId/messages", { - $roomId: timelineSet.room.roomId, - }, - ); - - const params: Record = { - dir: 'b', - }; - if (this.clientOpts.lazyLoadMembers) { - params.filter = JSON.stringify(Filter.LAZY_LOADING_MESSAGES_FILTER); + if (!timelineSet.room) { + throw new Error("getLatestTimeline only supports room timelines"); } - const res = await this.http.authedRequest(undefined, Method.Get, messagesPath, params); + let res: IMessagesResponse; + const roomId = timelineSet.room?.roomId; + if (timelineSet.isThreadTimeline) { + res = await this.createThreadListMessagesRequest( + roomId, + null, + 1, + Direction.Backward, + timelineSet.getFilter(), + ); + } else { + res = await this.createMessagesRequest( + roomId, + null, + 1, + Direction.Backward, + timelineSet.getFilter(), + ); + } const event = res.chunk?.[0]; if (!event) { throw new Error("No message returned from /messages when trying to construct getLatestTimeline"); @@ -5514,7 +5534,7 @@ export class MatrixClient extends TypedEventEmitter { + const path = utils.encodeUri("/rooms/$roomId/threads", { $roomId: roomId }); + + const params: Record = { + limit: limit.toString(), + dir: dir, + include: 'all', + }; + + if (fromToken) { + params.from = fromToken; + } + + let filter: IRoomEventFilter | null = null; + if (this.clientOpts.lazyLoadMembers) { + // create a shallow copy of LAZY_LOADING_MESSAGES_FILTER, + // so the timelineFilter doesn't get written into it below + filter = Object.assign({}, Filter.LAZY_LOADING_MESSAGES_FILTER); + } + if (timelineFilter) { + // XXX: it's horrific that /messages' filter parameter doesn't match + // /sync's one - see https://matrix.org/jira/browse/SPEC-451 + filter = filter || {}; + Object.assign(filter, timelineFilter.getRoomTimelineFilterComponent()?.toJSON()); + } + if (filter) { + params.filter = JSON.stringify(filter); + } + + const opts: { prefix?: string } = {}; + if (Thread.hasServerSideListSupport === FeatureSupport.Experimental) { + opts.prefix = "/_matrix/client/unstable/org.matrix.msc3856"; + } + + return this.http.authedRequest(undefined, Method.Get, path, params, undefined, opts) + .then(res => ({ + ...res, + start: res.prev_batch, + end: res.next_batch, + })); + } + /** * Take an EventTimeline, and back/forward-fill results. * @@ -5547,6 +5628,8 @@ export class MatrixClient extends TypedEventEmitter { const isNotifTimeline = (eventTimeline.getTimelineSet() === this.notifTimelineSet); + const room = this.getRoom(eventTimeline.getRoomId()); + const isThreadTimeline = eventTimeline.getTimelineSet().isThreadTimeline; // TODO: we should implement a backoff (as per scrollback()) to deal more // nicely with HTTP errors. @@ -5580,7 +5663,7 @@ export class MatrixClient extends TypedEventEmitter { const token = res.next_token; - const matrixEvents = []; + const matrixEvents: MatrixEvent[] = []; for (let i = 0; i < res.notifications.length; i++) { const notification = res.notifications[i]; @@ -5612,13 +5695,48 @@ export class MatrixClient extends TypedEventEmitter { + eventTimeline.paginationRequests[dir] = null; + }); + eventTimeline.paginationRequests[dir] = promise; + } else if (isThreadTimeline) { + if (!room) { + throw new Error("Unknown room " + eventTimeline.getRoomId()); + } + + promise = this.createThreadListMessagesRequest( + eventTimeline.getRoomId(), + token, + opts.limit, + dir, + eventTimeline.getFilter(), + ).then((res) => { + if (res.state) { + const roomState = eventTimeline.getState(dir); + const stateEvents = res.state.map(this.getEventMapper()); + roomState.setUnknownStateEvents(stateEvents); + } + const token = res.end; + const matrixEvents = res.chunk.map(this.getEventMapper()); + + const timelineSet = eventTimeline.getTimelineSet(); + timelineSet.addEventsToTimeline(matrixEvents, backwards, eventTimeline, token); + this.processBeaconEvents(room, matrixEvents); + this.processThreadRoots(room, matrixEvents, backwards); + + // if we've hit the end of the timeline, we need to stop trying to + // paginate. We need to keep the 'forwards' token though, to make sure + // we can recover from gappy syncs. + if (backwards && res.end == res.start) { + eventTimeline.setPaginationToken(null, dir); + } + return res.end != res.start; }).finally(() => { eventTimeline.paginationRequests[dir] = null; }); eventTimeline.paginationRequests[dir] = promise; } else { - const room = this.getRoom(eventTimeline.getRoomId()); if (!room) { throw new Error("Unknown room " + eventTimeline.getRoomId()); } @@ -5639,9 +5757,9 @@ export class MatrixClient extends TypedEventEmitter> = { + public paginationRequests: Record | null> = { [Direction.Backward]: null, [Direction.Forward]: null, }; @@ -311,7 +311,7 @@ export class EventTimeline { * token for going backwards in time; EventTimeline.FORWARDS to set the * pagination token for going forwards in time. */ - public setPaginationToken(token: string, direction: Direction): void { + public setPaginationToken(token: string | null, direction: Direction): void { this.getState(direction).paginationToken = token; } diff --git a/src/models/room-state.ts b/src/models/room-state.ts index b0104cf707c..3cca4a7b86b 100644 --- a/src/models/room-state.ts +++ b/src/models/room-state.ts @@ -97,7 +97,7 @@ export class RoomState extends TypedEventEmitter // XXX: Should be read-only public members: Record = {}; // userId: RoomMember public events = new Map>(); // Map> - public paginationToken: string = null; + public paginationToken: string | null = null; public readonly beacons = new Map(); private _liveBeaconIds: BeaconIdentifier[] = []; diff --git a/src/models/room.ts b/src/models/room.ts index 2aaa7e1f2e1..14968bbe643 100644 --- a/src/models/room.ts +++ b/src/models/room.ts @@ -372,8 +372,10 @@ export class Room extends ReadReceipt { return timelineSets; } catch (e) { this.threadTimelineSetsPromise = null; + return null; } } + return null; } /** @@ -1577,9 +1579,37 @@ export class Room extends ReadReceipt { return filter; } + /** + * Add a timelineSet for this room with the given filter + * @param {ThreadFilterType?} filterType Thread list type (e.g., All threads or My threads) + * @param {Object=} opts Configuration options + * @return {EventTimelineSet} The timelineSet + */ + public getOrCreateThreadTimelineSet( + filterType?: ThreadFilterType, + { + pendingEvents = true, + }: ICreateFilterOpts = {}, + ): EventTimelineSet { + if (this.threadsTimelineSets[filterType]) { + return this.filteredTimelineSets[filterType]; + } + const opts = Object.assign({ pendingEvents }, this.opts); + const timelineSet = + new EventTimelineSet(this, opts, undefined, undefined, Boolean(Thread.hasServerSideListSupport)); + this.reEmitter.reEmit(timelineSet, [ + RoomEvent.Timeline, + RoomEvent.TimelineReset, + ]); + + return timelineSet; + } + private async createThreadTimelineSet(filterType?: ThreadFilterType): Promise { let timelineSet: EventTimelineSet; - if (Thread.hasServerSideSupport) { + if (Thread.hasServerSideListSupport) { + timelineSet = this.getOrCreateThreadTimelineSet(filterType); + } else if (Thread.hasServerSideSupport) { const filter = await this.getThreadListFilter(filterType); timelineSet = this.getOrCreateFilteredTimelineSet( @@ -1614,11 +1644,38 @@ export class Room extends ReadReceipt { public threadsReady = false; + public processThreadRoots(events: MatrixEvent[], toStartOfTimeline: boolean): void { + for (const rootEvent of events) { + EventTimeline.setEventMetadata( + rootEvent, + this.currentState, + toStartOfTimeline, + ); + if (!this.getThread(rootEvent.getId())) { + this.createThread(rootEvent.getId(), rootEvent, [], toStartOfTimeline); + } + } + } + public async fetchRoomThreads(): Promise { if (this.threadsReady || !this.client.supportsExperimentalThreads()) { return; } + if (Thread.hasServerSideListSupport) { + await Promise.all([ + this.fetchRoomThreadList(ThreadFilterType.All), + this.fetchRoomThreadList(ThreadFilterType.My), + ]); + } else { + await this.fetchOldThreadList(); + } + + this.on(ThreadEvent.NewReply, this.onThreadNewReply); + this.threadsReady = true; + } + + private async fetchOldThreadList(): Promise { const allThreadsFilter = await this.getThreadListFilter(); const { chunk: events } = await this.client.createMessagesRequest( @@ -1667,26 +1724,54 @@ export class Room extends ReadReceipt { }); latestMyThreadsRootEvent = rootEvent; } - - if (!this.getThread(rootEvent.getId())) { - this.createThread(rootEvent.getId(), rootEvent, [], true); - } } + this.processThreadRoots(threadRoots, true); + this.client.decryptEventIfNeeded(threadRoots[threadRoots.length -1]); if (latestMyThreadsRootEvent) { this.client.decryptEventIfNeeded(latestMyThreadsRootEvent); } + } - this.threadsReady = true; + private async fetchRoomThreadList(filter?: ThreadFilterType): Promise { + const timelineSet = filter === ThreadFilterType.My + ? this.threadsTimelineSets[1] + : this.threadsTimelineSets[0]; - this.on(ThreadEvent.NewReply, this.onThreadNewReply); + const { chunk: events, end } = await this.client.createThreadListMessagesRequest( + this.roomId, + null, + undefined, + Direction.Backward, + timelineSet.getFilter(), + ); + + timelineSet.getLiveTimeline().setPaginationToken(end, Direction.Backward); + + if (!events.length) return; + + const matrixEvents = events.map(this.client.getEventMapper()); + this.processThreadRoots(matrixEvents, true); + const roomState = this.getLiveTimeline().getState(EventTimeline.FORWARDS); + for (const rootEvent of matrixEvents) { + timelineSet.addLiveEvent(rootEvent, { + duplicateStrategy: DuplicateStrategy.Replace, + fromCache: false, + roomState, + }); + } } private onThreadNewReply(thread: Thread): void { + const roomState = this.getLiveTimeline().getState(EventTimeline.FORWARDS); for (const timelineSet of this.threadsTimelineSets) { timelineSet.removeEvent(thread.id); - timelineSet.addLiveEvent(thread.rootEvent); + timelineSet.addLiveEvent(thread.rootEvent, { + duplicateStrategy: DuplicateStrategy.Replace, + fromCache: false, + roomState, + }); } } @@ -1832,8 +1917,6 @@ export class Room extends ReadReceipt { this.lastThread = thread; } - this.emit(ThreadEvent.New, thread, toStartOfTimeline); - if (this.threadsReady) { this.threadsTimelineSets.forEach(timelineSet => { if (thread.rootEvent) { @@ -1850,6 +1933,8 @@ export class Room extends ReadReceipt { }); } + this.emit(ThreadEvent.New, thread, toStartOfTimeline); + return thread; } From 2a3fc5326a27295b6b10910376157b754be30472 Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski Date: Mon, 26 Sep 2022 12:21:59 +0200 Subject: [PATCH 04/17] tests for thread list pagination --- .../matrix-client-event-timeline.spec.ts | 276 +++++++++++++++++- 1 file changed, 275 insertions(+), 1 deletion(-) diff --git a/spec/integ/matrix-client-event-timeline.spec.ts b/spec/integ/matrix-client-event-timeline.spec.ts index 1c262052d3a..65679dce471 100644 --- a/spec/integ/matrix-client-event-timeline.spec.ts +++ b/spec/integ/matrix-client-event-timeline.spec.ts @@ -15,8 +15,19 @@ limitations under the License. */ import * as utils from "../test-utils/test-utils"; -import { ClientEvent, EventTimeline, Filter, IEvent, MatrixClient, MatrixEvent, Room } from "../../src/matrix"; +import { + ClientEvent, + Direction, + EventTimeline, + EventTimelineSet, + Filter, + IEvent, + MatrixClient, + MatrixEvent, + Room, +} from "../../src/matrix"; import { logger } from "../../src/logger"; +import { encodeUri } from "../../src/utils"; import { TestClient } from "../TestClient"; import { FeatureSupport, Thread, THREAD_RELATION_TYPE } from "../../src/models/thread"; @@ -133,6 +144,44 @@ const THREAD_REPLY = utils.mkEvent({ THREAD_ROOT.unsigned["m.relations"]["io.element.thread"].latest_event = THREAD_REPLY; +const STABLE_THREAD_ROOT = utils.mkEvent({ + room: roomId, + user: userId, + type: "m.room.message", + content: { + "body": "thread root", + "msgtype": "m.text", + }, + unsigned: { + "m.relations": { + "m.thread": { + //"latest_event": undefined, + "count": 1, + "current_user_participated": true, + }, + }, + }, + event: false, +}); + +const STABLE_THREAD_REPLY = utils.mkEvent({ + room: roomId, + user: userId, + type: "m.room.message", + content: { + "body": "thread reply", + "msgtype": "m.text", + "m.relates_to": { + // We can't use the const here because we change server support mode for test + rel_type: "m.thread", + event_id: THREAD_ROOT.event_id, + }, + }, + event: false, +}); + +STABLE_THREAD_ROOT.unsigned["m.relations"]["m.thread"].latest_event = STABLE_THREAD_REPLY; + const SYNC_THREAD_ROOT = withoutRoomId(THREAD_ROOT); const SYNC_THREAD_REPLY = withoutRoomId(THREAD_REPLY); SYNC_THREAD_ROOT.unsigned = { @@ -925,6 +974,231 @@ describe("MatrixClient event timelines", function() { }); }); + describe("paginateEventTimeline for thread list timeline", function() { + async function flushHttp(promise: Promise): Promise { + return Promise.all([promise, httpBackend.flushAllExpected()]).then(([result]) => result); + } + + describe("with server compatibility", function() { + async function testPagination(timelineSet: EventTimelineSet, direction: Direction) { + const RANDOM_TOKEN = "7280349c7bee430f91defe2a38a0a08c"; + function respondToThreads() { + httpBackend.when("GET", encodeUri("/_matrix/client/r0/rooms/$roomId/threads", { + $roomId: roomId, + })).respond(200, { + chunk: [STABLE_THREAD_ROOT], + state: [], + next_batch: RANDOM_TOKEN, + }); + } + function respondToContext() { + httpBackend.when("GET", encodeUri("/_matrix/client/r0/rooms/$roomId/context/$eventId", { + $roomId: roomId, + $eventId: STABLE_THREAD_ROOT.event_id!, + })).respond(200, { + end: "", + start: "", + state: [], + events_before: [], + events_after: [], + event: STABLE_THREAD_ROOT, + }); + } + + respondToContext(); + await flushHttp(client.getEventTimeline(timelineSet, STABLE_THREAD_ROOT.event_id!)); + respondToThreads(); + const timeline = await flushHttp(client.getLatestTimeline(timelineSet)); + expect(timeline).not.toBeNull(); + + respondToThreads(); + const success = await flushHttp(client.paginateEventTimeline(timeline!, { + backwards: direction === Direction.Backward, + })); + expect(success).toBeTruthy(); + expect(timeline!.getEvents().length).toEqual(1); + expect(timeline!.getEvents()[0].event).toEqual(STABLE_THREAD_ROOT); + expect(timeline!.getPaginationToken(direction)).toEqual(RANDOM_TOKEN); + } + + it("should allow you to paginate all threads backwards", async function() { + // @ts-ignore + client.clientOpts.experimentalThreadSupport = true; + Thread.setServerSideSupport(FeatureSupport.Experimental); + Thread.setServerSideListSupport(FeatureSupport.Stable); + + const room = client.getRoom(roomId); + const timelineSets = await (room?.createThreadsTimelineSets()); + expect(timelineSets).not.toBeNull(); + const [allThreads, myThreads] = timelineSets!; + await testPagination(allThreads, Direction.Backward); + await testPagination(myThreads, Direction.Backward); + }); + + it("should allow you to paginate all threads forwards", async function() { + // @ts-ignore + client.clientOpts.experimentalThreadSupport = true; + Thread.setServerSideSupport(FeatureSupport.Experimental); + Thread.setServerSideListSupport(FeatureSupport.Stable); + + const room = client.getRoom(roomId); + const timelineSets = await (room?.createThreadsTimelineSets()); + expect(timelineSets).not.toBeNull(); + const [allThreads, myThreads] = timelineSets!; + + await testPagination(allThreads, Direction.Forward); + await testPagination(myThreads, Direction.Forward); + }); + + it("should allow fetching all threads", async function() { + // @ts-ignore + client.clientOpts.experimentalThreadSupport = true; + Thread.setServerSideSupport(FeatureSupport.Experimental); + Thread.setServerSideListSupport(FeatureSupport.Stable); + + const RANDOM_TOKEN = "7280349c7bee430f91defe2a38a0a08c"; + function respondToThreads() { + httpBackend.when("GET", encodeUri("/_matrix/client/r0/rooms/$roomId/threads", { + $roomId: roomId, + })).respond(200, { + chunk: [STABLE_THREAD_ROOT], + state: [], + next_batch: RANDOM_TOKEN, + }); + } + const room = client.getRoom(roomId); + const timelineSets = await room?.createThreadsTimelineSets(); + expect(timelineSets).not.toBeNull(); + respondToThreads(); + respondToThreads(); + httpBackend.when("GET", "/sync").respond(200, INITIAL_SYNC_DATA); + await flushHttp(room.fetchRoomThreads()); + }); + }); + + describe("without server compatibility", function() { + async function testPagination(timelineSet: EventTimelineSet, direction: Direction) { + const RANDOM_TOKEN = "7280349c7bee430f91defe2a38a0a08c"; + function respondToMessagesRequest() { + httpBackend.when("GET", encodeUri("/_matrix/client/r0/rooms/$roomId/messages", { + $roomId: roomId, + })).respond(200, { + chunk: [THREAD_ROOT], + state: [], + start: `${Direction.Forward}${RANDOM_TOKEN}2`, + end: `${Direction.Backward}${RANDOM_TOKEN}2`, + }); + } + function respondToContext() { + httpBackend.when("GET", encodeUri("/_matrix/client/r0/rooms/$roomId/context/$eventId", { + $roomId: roomId, + $eventId: THREAD_ROOT.event_id!, + })).respond(200, { + end: `${Direction.Forward}${RANDOM_TOKEN}1`, + start: `${Direction.Backward}${RANDOM_TOKEN}1`, + state: [], + events_before: [], + events_after: [], + event: THREAD_ROOT, + }); + } + function respondToSync() { + httpBackend.when("GET", "/sync").respond(200, INITIAL_SYNC_DATA); + } + + respondToContext(); + respondToSync(); + await flushHttp(client.getEventTimeline(timelineSet, THREAD_ROOT.event_id!)); + + respondToMessagesRequest(); + const timeline = await flushHttp(client.getLatestTimeline(timelineSet)); + expect(timeline).not.toBeNull(); + + respondToMessagesRequest(); + const success = await flushHttp(client.paginateEventTimeline(timeline!, { + backwards: direction === Direction.Backward, + })); + + expect(success).toBeTruthy(); + expect(timeline!.getEvents().length).toEqual(1); + expect(timeline!.getEvents()[0].event).toEqual(THREAD_ROOT); + expect(timeline!.getPaginationToken(direction)).toEqual(`${direction}${RANDOM_TOKEN}2`); + } + + it("should allow you to paginate all threads", async function() { + // @ts-ignore + client.clientOpts.experimentalThreadSupport = true; + Thread.setServerSideSupport(FeatureSupport.Experimental); + Thread.setServerSideListSupport(FeatureSupport.None); + + function respondToFilter() { + httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" }); + } + function respondToSync() { + httpBackend.when("GET", "/sync").respond(200, INITIAL_SYNC_DATA); + } + + const room = client.getRoom(roomId); + + respondToFilter(); + respondToSync(); + respondToFilter(); + respondToSync(); + + const timelineSetsPromise = room?.createThreadsTimelineSets(); + expect(timelineSetsPromise).not.toBeNull(); + const timelineSets = await flushHttp(timelineSetsPromise!); + expect(timelineSets).not.toBeNull(); + const [allThreads, myThreads] = timelineSets!; + + await testPagination(allThreads, Direction.Backward); + await testPagination(myThreads, Direction.Backward); + }); + + it("should allow fetching all threads", async function() { + // @ts-ignore + client.clientOpts.experimentalThreadSupport = true; + Thread.setServerSideSupport(FeatureSupport.Experimental); + Thread.setServerSideListSupport(FeatureSupport.None); + + const room = client.getRoom(roomId); + + const RANDOM_TOKEN = "7280349c7bee430f91defe2a38a0a08c"; + function respondToMessagesRequest() { + httpBackend.when("GET", encodeUri("/_matrix/client/r0/rooms/$roomId/messages", { + $roomId: roomId, + })).respond(200, { + chunk: [STABLE_THREAD_ROOT], + state: [], + start: `${Direction.Forward}${RANDOM_TOKEN}2`, + end: `${Direction.Backward}${RANDOM_TOKEN}2`, + }); + } + function respondToFilter() { + httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" }); + } + function respondToSync() { + httpBackend.when("GET", "/sync").respond(200, INITIAL_SYNC_DATA); + } + + respondToFilter(); + respondToSync(); + respondToFilter(); + respondToSync(); + + const timelineSetsPromise = room?.createThreadsTimelineSets(); + expect(timelineSetsPromise).not.toBeNull(); + await flushHttp(timelineSetsPromise!); + respondToFilter(); + respondToSync(); + respondToSync(); + respondToSync(); + respondToMessagesRequest(); + await flushHttp(room.fetchRoomThreads()); + }); + }); + }); + describe("event timeline for sent events", function() { const TXN_ID = "txn1"; const event = utils.mkMessage({ From fbb39afa864c70617799d83a46a6746486b20574 Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski Date: Wed, 28 Sep 2022 15:42:10 +0200 Subject: [PATCH 05/17] Make changes as requested by reviewers, cleaning up the code a bit --- .../matrix-client-event-timeline.spec.ts | 95 +++-------- src/client.ts | 8 +- src/models/room.ts | 155 +++++++++--------- 3 files changed, 98 insertions(+), 160 deletions(-) diff --git a/spec/integ/matrix-client-event-timeline.spec.ts b/spec/integ/matrix-client-event-timeline.spec.ts index 65679dce471..00935c830bb 100644 --- a/spec/integ/matrix-client-event-timeline.spec.ts +++ b/spec/integ/matrix-client-event-timeline.spec.ts @@ -144,44 +144,6 @@ const THREAD_REPLY = utils.mkEvent({ THREAD_ROOT.unsigned["m.relations"]["io.element.thread"].latest_event = THREAD_REPLY; -const STABLE_THREAD_ROOT = utils.mkEvent({ - room: roomId, - user: userId, - type: "m.room.message", - content: { - "body": "thread root", - "msgtype": "m.text", - }, - unsigned: { - "m.relations": { - "m.thread": { - //"latest_event": undefined, - "count": 1, - "current_user_participated": true, - }, - }, - }, - event: false, -}); - -const STABLE_THREAD_REPLY = utils.mkEvent({ - room: roomId, - user: userId, - type: "m.room.message", - content: { - "body": "thread reply", - "msgtype": "m.text", - "m.relates_to": { - // We can't use the const here because we change server support mode for test - rel_type: "m.thread", - event_id: THREAD_ROOT.event_id, - }, - }, - event: false, -}); - -STABLE_THREAD_ROOT.unsigned["m.relations"]["m.thread"].latest_event = STABLE_THREAD_REPLY; - const SYNC_THREAD_ROOT = withoutRoomId(THREAD_ROOT); const SYNC_THREAD_REPLY = withoutRoomId(THREAD_REPLY); SYNC_THREAD_ROOT.unsigned = { @@ -980,13 +942,20 @@ describe("MatrixClient event timelines", function() { } describe("with server compatibility", function() { + beforeEach(() => { + // @ts-ignore + client.clientOpts.experimentalThreadSupport = true; + Thread.setServerSideSupport(FeatureSupport.Experimental); + Thread.setServerSideListSupport(FeatureSupport.Stable); + }); + async function testPagination(timelineSet: EventTimelineSet, direction: Direction) { const RANDOM_TOKEN = "7280349c7bee430f91defe2a38a0a08c"; function respondToThreads() { httpBackend.when("GET", encodeUri("/_matrix/client/r0/rooms/$roomId/threads", { $roomId: roomId, })).respond(200, { - chunk: [STABLE_THREAD_ROOT], + chunk: [THREAD_ROOT], state: [], next_batch: RANDOM_TOKEN, }); @@ -994,19 +963,19 @@ describe("MatrixClient event timelines", function() { function respondToContext() { httpBackend.when("GET", encodeUri("/_matrix/client/r0/rooms/$roomId/context/$eventId", { $roomId: roomId, - $eventId: STABLE_THREAD_ROOT.event_id!, + $eventId: THREAD_ROOT.event_id!, })).respond(200, { end: "", start: "", state: [], events_before: [], events_after: [], - event: STABLE_THREAD_ROOT, + event: THREAD_ROOT, }); } respondToContext(); - await flushHttp(client.getEventTimeline(timelineSet, STABLE_THREAD_ROOT.event_id!)); + await flushHttp(client.getEventTimeline(timelineSet, THREAD_ROOT.event_id!)); respondToThreads(); const timeline = await flushHttp(client.getLatestTimeline(timelineSet)); expect(timeline).not.toBeNull(); @@ -1016,17 +985,11 @@ describe("MatrixClient event timelines", function() { backwards: direction === Direction.Backward, })); expect(success).toBeTruthy(); - expect(timeline!.getEvents().length).toEqual(1); - expect(timeline!.getEvents()[0].event).toEqual(STABLE_THREAD_ROOT); + expect(timeline!.getEvents().map(it => it.event)).toEqual([THREAD_ROOT]); expect(timeline!.getPaginationToken(direction)).toEqual(RANDOM_TOKEN); } it("should allow you to paginate all threads backwards", async function() { - // @ts-ignore - client.clientOpts.experimentalThreadSupport = true; - Thread.setServerSideSupport(FeatureSupport.Experimental); - Thread.setServerSideListSupport(FeatureSupport.Stable); - const room = client.getRoom(roomId); const timelineSets = await (room?.createThreadsTimelineSets()); expect(timelineSets).not.toBeNull(); @@ -1036,11 +999,6 @@ describe("MatrixClient event timelines", function() { }); it("should allow you to paginate all threads forwards", async function() { - // @ts-ignore - client.clientOpts.experimentalThreadSupport = true; - Thread.setServerSideSupport(FeatureSupport.Experimental); - Thread.setServerSideListSupport(FeatureSupport.Stable); - const room = client.getRoom(roomId); const timelineSets = await (room?.createThreadsTimelineSets()); expect(timelineSets).not.toBeNull(); @@ -1051,17 +1009,12 @@ describe("MatrixClient event timelines", function() { }); it("should allow fetching all threads", async function() { - // @ts-ignore - client.clientOpts.experimentalThreadSupport = true; - Thread.setServerSideSupport(FeatureSupport.Experimental); - Thread.setServerSideListSupport(FeatureSupport.Stable); - const RANDOM_TOKEN = "7280349c7bee430f91defe2a38a0a08c"; function respondToThreads() { httpBackend.when("GET", encodeUri("/_matrix/client/r0/rooms/$roomId/threads", { $roomId: roomId, })).respond(200, { - chunk: [STABLE_THREAD_ROOT], + chunk: [THREAD_ROOT], state: [], next_batch: RANDOM_TOKEN, }); @@ -1077,6 +1030,13 @@ describe("MatrixClient event timelines", function() { }); describe("without server compatibility", function() { + beforeEach(() => { + // @ts-ignore + client.clientOpts.experimentalThreadSupport = true; + Thread.setServerSideSupport(FeatureSupport.Experimental); + Thread.setServerSideListSupport(FeatureSupport.None); + }); + async function testPagination(timelineSet: EventTimelineSet, direction: Direction) { const RANDOM_TOKEN = "7280349c7bee430f91defe2a38a0a08c"; function respondToMessagesRequest() { @@ -1120,17 +1080,11 @@ describe("MatrixClient event timelines", function() { })); expect(success).toBeTruthy(); - expect(timeline!.getEvents().length).toEqual(1); - expect(timeline!.getEvents()[0].event).toEqual(THREAD_ROOT); + expect(timeline!.getEvents().map(it => it.event)).toEqual([THREAD_ROOT]); expect(timeline!.getPaginationToken(direction)).toEqual(`${direction}${RANDOM_TOKEN}2`); } it("should allow you to paginate all threads", async function() { - // @ts-ignore - client.clientOpts.experimentalThreadSupport = true; - Thread.setServerSideSupport(FeatureSupport.Experimental); - Thread.setServerSideListSupport(FeatureSupport.None); - function respondToFilter() { httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" }); } @@ -1156,11 +1110,6 @@ describe("MatrixClient event timelines", function() { }); it("should allow fetching all threads", async function() { - // @ts-ignore - client.clientOpts.experimentalThreadSupport = true; - Thread.setServerSideSupport(FeatureSupport.Experimental); - Thread.setServerSideListSupport(FeatureSupport.None); - const room = client.getRoom(roomId); const RANDOM_TOKEN = "7280349c7bee430f91defe2a38a0a08c"; @@ -1168,7 +1117,7 @@ describe("MatrixClient event timelines", function() { httpBackend.when("GET", encodeUri("/_matrix/client/r0/rooms/$roomId/messages", { $roomId: roomId, })).respond(200, { - chunk: [STABLE_THREAD_ROOT], + chunk: [THREAD_ROOT], state: [], start: `${Direction.Forward}${RANDOM_TOKEN}2`, end: `${Direction.Backward}${RANDOM_TOKEN}2`, diff --git a/src/client.ts b/src/client.ts index cf8244a590f..9dd7e8f794f 100644 --- a/src/client.ts +++ b/src/client.ts @@ -5357,7 +5357,7 @@ export class MatrixClient extends TypedEventEmitter { eventTimeline.paginationRequests[dir] = null; }); @@ -6866,8 +6866,6 @@ export class MatrixClient extends TypedEventEmitter { return filter; } - /** - * Add a timelineSet for this room with the given filter - * @param {ThreadFilterType?} filterType Thread list type (e.g., All threads or My threads) - * @param {Object=} opts Configuration options - * @return {EventTimelineSet} The timelineSet - */ - public getOrCreateThreadTimelineSet( - filterType?: ThreadFilterType, - { - pendingEvents = true, - }: ICreateFilterOpts = {}, - ): EventTimelineSet { - if (this.threadsTimelineSets[filterType]) { - return this.filteredTimelineSets[filterType]; - } - const opts = Object.assign({ pendingEvents }, this.opts); - const timelineSet = - new EventTimelineSet(this, opts, undefined, undefined, Boolean(Thread.hasServerSideListSupport)); - this.reEmitter.reEmit(timelineSet, [ - RoomEvent.Timeline, - RoomEvent.TimelineReset, - ]); - - return timelineSet; - } - private async createThreadTimelineSet(filterType?: ThreadFilterType): Promise { let timelineSet: EventTimelineSet; if (Thread.hasServerSideListSupport) { - timelineSet = this.getOrCreateThreadTimelineSet(filterType); + timelineSet = + new EventTimelineSet(this, this.opts, undefined, undefined, Boolean(Thread.hasServerSideListSupport)); + this.reEmitter.reEmit(timelineSet, [ + RoomEvent.Timeline, + RoomEvent.TimelineReset, + ]); } else if (Thread.hasServerSideSupport) { const filter = await this.getThreadListFilter(filterType); @@ -1642,8 +1621,13 @@ export class Room extends ReadReceipt { return timelineSet; } - public threadsReady = false; + private threadsReady = false; + /** + * Takes the given thread root events and creates threads for them. + * @param events + * @param toStartOfTimeline + */ public processThreadRoots(events: MatrixEvent[], toStartOfTimeline: boolean): void { for (const rootEvent of events) { EventTimeline.setEventMetadata( @@ -1657,6 +1641,11 @@ export class Room extends ReadReceipt { } } + /** + * Fetch the bare minimum of room threads required for the thread list to work reliably. + * With server support that means fetching one page. + * Without server support that means fetching as much at once as the server allows us to. + */ public async fetchRoomThreads(): Promise { if (this.threadsReady || !this.client.supportsExperimentalThreads()) { return; @@ -1668,72 +1657,74 @@ export class Room extends ReadReceipt { this.fetchRoomThreadList(ThreadFilterType.My), ]); } else { - await this.fetchOldThreadList(); - } - - this.on(ThreadEvent.NewReply, this.onThreadNewReply); - this.threadsReady = true; - } - - private async fetchOldThreadList(): Promise { - const allThreadsFilter = await this.getThreadListFilter(); - - const { chunk: events } = await this.client.createMessagesRequest( - this.roomId, - "", - Number.MAX_SAFE_INTEGER, - Direction.Backward, - allThreadsFilter, - ); - - if (!events.length) return; - - // Sorted by last_reply origin_server_ts - const threadRoots = events - .map(this.client.getEventMapper()) - .sort((eventA, eventB) => { - /** - * `origin_server_ts` in a decentralised world is far from ideal - * but for lack of any better, we will have to use this - * Long term the sorting should be handled by homeservers and this - * is only meant as a short term patch - */ - const threadAMetadata = eventA - .getServerAggregatedRelation(RelationType.Thread); - const threadBMetadata = eventB - .getServerAggregatedRelation(RelationType.Thread); - return threadAMetadata.latest_event.origin_server_ts - threadBMetadata.latest_event.origin_server_ts; - }); + const allThreadsFilter = await this.getThreadListFilter(); + + const { chunk: events } = await this.client.createMessagesRequest( + this.roomId, + "", + Number.MAX_SAFE_INTEGER, + Direction.Backward, + allThreadsFilter, + ); - let latestMyThreadsRootEvent: MatrixEvent; - const roomState = this.getLiveTimeline().getState(EventTimeline.FORWARDS); - for (const rootEvent of threadRoots) { - this.threadsTimelineSets[0].addLiveEvent(rootEvent, { - duplicateStrategy: DuplicateStrategy.Ignore, - fromCache: false, - roomState, - }); + if (!events.length) return; + + // Sorted by last_reply origin_server_ts + const threadRoots = events + .map(this.client.getEventMapper()) + .sort((eventA, eventB) => { + /** + * `origin_server_ts` in a decentralised world is far from ideal + * but for lack of any better, we will have to use this + * Long term the sorting should be handled by homeservers and this + * is only meant as a short term patch + */ + const threadAMetadata = eventA + .getServerAggregatedRelation(THREAD_RELATION_TYPE.name); + const threadBMetadata = eventB + .getServerAggregatedRelation(THREAD_RELATION_TYPE.name); + return threadAMetadata.latest_event.origin_server_ts - + threadBMetadata.latest_event.origin_server_ts; + }); - const threadRelationship = rootEvent - .getServerAggregatedRelation(RelationType.Thread); - if (threadRelationship.current_user_participated) { - this.threadsTimelineSets[1].addLiveEvent(rootEvent, { + let latestMyThreadsRootEvent: MatrixEvent; + const roomState = this.getLiveTimeline().getState(EventTimeline.FORWARDS); + for (const rootEvent of threadRoots) { + this.threadsTimelineSets[0].addLiveEvent(rootEvent, { duplicateStrategy: DuplicateStrategy.Ignore, fromCache: false, roomState, }); - latestMyThreadsRootEvent = rootEvent; + + const threadRelationship = rootEvent + .getServerAggregatedRelation(THREAD_RELATION_TYPE.name); + if (threadRelationship.current_user_participated) { + this.threadsTimelineSets[1].addLiveEvent(rootEvent, { + duplicateStrategy: DuplicateStrategy.Ignore, + fromCache: false, + roomState, + }); + latestMyThreadsRootEvent = rootEvent; + } } - } - this.processThreadRoots(threadRoots, true); + this.processThreadRoots(threadRoots, true); - this.client.decryptEventIfNeeded(threadRoots[threadRoots.length -1]); - if (latestMyThreadsRootEvent) { - this.client.decryptEventIfNeeded(latestMyThreadsRootEvent); + this.client.decryptEventIfNeeded(threadRoots[threadRoots.length -1]); + if (latestMyThreadsRootEvent) { + this.client.decryptEventIfNeeded(latestMyThreadsRootEvent); + } } + + this.on(ThreadEvent.NewReply, this.onThreadNewReply); + this.threadsReady = true; } + /** + * Fetch a single page of threadlist messages for the specific thread filter + * @param filter + * @private + */ private async fetchRoomThreadList(filter?: ThreadFilterType): Promise { const timelineSet = filter === ThreadFilterType.My ? this.threadsTimelineSets[1] From 54fa3e7dd02ce68352d2a630e2935e4d59621b01 Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski Date: Wed, 5 Oct 2022 15:19:46 +0200 Subject: [PATCH 06/17] Cleanup readability of filter handling for createThreadListMessagesRequest --- src/client.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/client.ts b/src/client.ts index 9dd7e8f794f..5f0daa21e5e 100644 --- a/src/client.ts +++ b/src/client.ts @@ -5588,13 +5588,18 @@ export class MatrixClient extends TypedEventEmitter Date: Wed, 5 Oct 2022 15:20:22 +0200 Subject: [PATCH 07/17] Create specific types for httpbackend in tests --- spec/integ/matrix-client-event-timeline.spec.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/spec/integ/matrix-client-event-timeline.spec.ts b/spec/integ/matrix-client-event-timeline.spec.ts index 00935c830bb..afdb7ad3659 100644 --- a/spec/integ/matrix-client-event-timeline.spec.ts +++ b/spec/integ/matrix-client-event-timeline.spec.ts @@ -156,8 +156,11 @@ SYNC_THREAD_ROOT.unsigned = { }, }; +type HttpBackend = TestClient["httpBackend"]; +type ExpectedHttpRequest = ReturnType; + // start the client, and wait for it to initialise -function startClient(httpBackend: TestClient["httpBackend"], client: MatrixClient) { +function startClient(httpBackend: HttpBackend, client: MatrixClient) { httpBackend.when("GET", "/versions").respond(200, {}); httpBackend.when("GET", "/pushrules").respond(200, {}); httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" }); @@ -183,7 +186,7 @@ function startClient(httpBackend: TestClient["httpBackend"], client: MatrixClien } describe("getEventTimeline support", function() { - let httpBackend: TestClient["httpBackend"]; + let httpBackend: HttpBackend; let client: MatrixClient; beforeEach(function() { @@ -291,7 +294,7 @@ describe("getEventTimeline support", function() { describe("MatrixClient event timelines", function() { let client: MatrixClient; - let httpBackend: TestClient["httpBackend"]; + let httpBackend: HttpBackend; beforeEach(function() { const testClient = new TestClient( From ec46d053785f1944575651c5d8b8bfc7747c374a Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski Date: Wed, 5 Oct 2022 15:20:50 +0200 Subject: [PATCH 08/17] Reduce duplication in test methods --- .../matrix-client-event-timeline.spec.ts | 141 +++++++----------- 1 file changed, 54 insertions(+), 87 deletions(-) diff --git a/spec/integ/matrix-client-event-timeline.spec.ts b/spec/integ/matrix-client-event-timeline.spec.ts index afdb7ad3659..8380c6f821e 100644 --- a/spec/integ/matrix-client-event-timeline.spec.ts +++ b/spec/integ/matrix-client-event-timeline.spec.ts @@ -944,6 +944,60 @@ describe("MatrixClient event timelines", function() { return Promise.all([promise, httpBackend.flushAllExpected()]).then(([result]) => result); } + const RANDOM_TOKEN = "7280349c7bee430f91defe2a38a0a08c"; + + function respondToFilter(): ExpectedHttpRequest { + const request = httpBackend.when("POST", "/filter"); + request.respond(200, { filter_id: "fid" }); + return request; + } + + function respondToSync(): ExpectedHttpRequest { + const request = httpBackend.when("GET", "/sync"); + request.respond(200, INITIAL_SYNC_DATA); + return request; + } + + function respondToThreads(): ExpectedHttpRequest { + const request = httpBackend.when("GET", encodeUri("/_matrix/client/r0/rooms/$roomId/threads", { + $roomId: roomId, + })); + request.respond(200, { + chunk: [THREAD_ROOT], + state: [], + next_batch: RANDOM_TOKEN, + }); + return request; + } + + function respondToContext(): ExpectedHttpRequest { + const request = httpBackend.when("GET", encodeUri("/_matrix/client/r0/rooms/$roomId/context/$eventId", { + $roomId: roomId, + $eventId: THREAD_ROOT.event_id!, + })); + request.respond(200, { + end: `${Direction.Forward}${RANDOM_TOKEN}1`, + start: `${Direction.Backward}${RANDOM_TOKEN}1`, + state: [], + events_before: [], + events_after: [], + event: THREAD_ROOT, + }); + return request; + } + function respondToMessagesRequest(): ExpectedHttpRequest { + const request = httpBackend.when("GET", encodeUri("/_matrix/client/r0/rooms/$roomId/messages", { + $roomId: roomId, + })); + request.respond(200, { + chunk: [THREAD_ROOT], + state: [], + start: `${Direction.Forward}${RANDOM_TOKEN}2`, + end: `${Direction.Backward}${RANDOM_TOKEN}2`, + }); + return request; + } + describe("with server compatibility", function() { beforeEach(() => { // @ts-ignore @@ -953,30 +1007,6 @@ describe("MatrixClient event timelines", function() { }); async function testPagination(timelineSet: EventTimelineSet, direction: Direction) { - const RANDOM_TOKEN = "7280349c7bee430f91defe2a38a0a08c"; - function respondToThreads() { - httpBackend.when("GET", encodeUri("/_matrix/client/r0/rooms/$roomId/threads", { - $roomId: roomId, - })).respond(200, { - chunk: [THREAD_ROOT], - state: [], - next_batch: RANDOM_TOKEN, - }); - } - function respondToContext() { - httpBackend.when("GET", encodeUri("/_matrix/client/r0/rooms/$roomId/context/$eventId", { - $roomId: roomId, - $eventId: THREAD_ROOT.event_id!, - })).respond(200, { - end: "", - start: "", - state: [], - events_before: [], - events_after: [], - event: THREAD_ROOT, - }); - } - respondToContext(); await flushHttp(client.getEventTimeline(timelineSet, THREAD_ROOT.event_id!)); respondToThreads(); @@ -1012,16 +1042,6 @@ describe("MatrixClient event timelines", function() { }); it("should allow fetching all threads", async function() { - const RANDOM_TOKEN = "7280349c7bee430f91defe2a38a0a08c"; - function respondToThreads() { - httpBackend.when("GET", encodeUri("/_matrix/client/r0/rooms/$roomId/threads", { - $roomId: roomId, - })).respond(200, { - chunk: [THREAD_ROOT], - state: [], - next_batch: RANDOM_TOKEN, - }); - } const room = client.getRoom(roomId); const timelineSets = await room?.createThreadsTimelineSets(); expect(timelineSets).not.toBeNull(); @@ -1041,34 +1061,6 @@ describe("MatrixClient event timelines", function() { }); async function testPagination(timelineSet: EventTimelineSet, direction: Direction) { - const RANDOM_TOKEN = "7280349c7bee430f91defe2a38a0a08c"; - function respondToMessagesRequest() { - httpBackend.when("GET", encodeUri("/_matrix/client/r0/rooms/$roomId/messages", { - $roomId: roomId, - })).respond(200, { - chunk: [THREAD_ROOT], - state: [], - start: `${Direction.Forward}${RANDOM_TOKEN}2`, - end: `${Direction.Backward}${RANDOM_TOKEN}2`, - }); - } - function respondToContext() { - httpBackend.when("GET", encodeUri("/_matrix/client/r0/rooms/$roomId/context/$eventId", { - $roomId: roomId, - $eventId: THREAD_ROOT.event_id!, - })).respond(200, { - end: `${Direction.Forward}${RANDOM_TOKEN}1`, - start: `${Direction.Backward}${RANDOM_TOKEN}1`, - state: [], - events_before: [], - events_after: [], - event: THREAD_ROOT, - }); - } - function respondToSync() { - httpBackend.when("GET", "/sync").respond(200, INITIAL_SYNC_DATA); - } - respondToContext(); respondToSync(); await flushHttp(client.getEventTimeline(timelineSet, THREAD_ROOT.event_id!)); @@ -1088,13 +1080,6 @@ describe("MatrixClient event timelines", function() { } it("should allow you to paginate all threads", async function() { - function respondToFilter() { - httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" }); - } - function respondToSync() { - httpBackend.when("GET", "/sync").respond(200, INITIAL_SYNC_DATA); - } - const room = client.getRoom(roomId); respondToFilter(); @@ -1115,24 +1100,6 @@ describe("MatrixClient event timelines", function() { it("should allow fetching all threads", async function() { const room = client.getRoom(roomId); - const RANDOM_TOKEN = "7280349c7bee430f91defe2a38a0a08c"; - function respondToMessagesRequest() { - httpBackend.when("GET", encodeUri("/_matrix/client/r0/rooms/$roomId/messages", { - $roomId: roomId, - })).respond(200, { - chunk: [THREAD_ROOT], - state: [], - start: `${Direction.Forward}${RANDOM_TOKEN}2`, - end: `${Direction.Backward}${RANDOM_TOKEN}2`, - }); - } - function respondToFilter() { - httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" }); - } - function respondToSync() { - httpBackend.when("GET", "/sync").respond(200, INITIAL_SYNC_DATA); - } - respondToFilter(); respondToSync(); respondToFilter(); From b18aef851cfb7d86f1f0ce7df687a155bde1d14f Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski Date: Wed, 5 Oct 2022 15:21:05 +0200 Subject: [PATCH 09/17] Test lazy loading filters for paginateEventTimeline --- .../matrix-client-event-timeline.spec.ts | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/spec/integ/matrix-client-event-timeline.spec.ts b/spec/integ/matrix-client-event-timeline.spec.ts index 8380c6f821e..7ae7798af85 100644 --- a/spec/integ/matrix-client-event-timeline.spec.ts +++ b/spec/integ/matrix-client-event-timeline.spec.ts @@ -1116,6 +1116,30 @@ describe("MatrixClient event timelines", function() { await flushHttp(room.fetchRoomThreads()); }); }); + + it("should add lazy loading filter", async () => { + // @ts-ignore + client.clientOpts.experimentalThreadSupport = true; + Thread.setServerSideSupport(FeatureSupport.Experimental); + Thread.setServerSideListSupport(FeatureSupport.Stable); + // @ts-ignore + client.clientOpts.lazyLoadMembers = true; + + const room = client.getRoom(roomId); + const timelineSets = await room?.createThreadsTimelineSets(); + expect(timelineSets).not.toBeNull(); + const [allThreads,] = timelineSets!; + + respondToThreads().check((request) => { + expect(request.queryParams.filter).toEqual(JSON.stringify({ + "lazy_load_members": true, + })); + }); + + await flushHttp(client.paginateEventTimeline(allThreads.getLiveTimeline(), { + backwards: true, + })); + }); }); describe("event timeline for sent events", function() { From f62e3f3b45072dc5d6c5f69498439a519d5076a5 Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski Date: Wed, 5 Oct 2022 15:22:24 +0200 Subject: [PATCH 10/17] Test pagination token for paginateEventTimeline --- .../matrix-client-event-timeline.spec.ts | 29 ++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/spec/integ/matrix-client-event-timeline.spec.ts b/spec/integ/matrix-client-event-timeline.spec.ts index 7ae7798af85..9d9e6dcc358 100644 --- a/spec/integ/matrix-client-event-timeline.spec.ts +++ b/spec/integ/matrix-client-event-timeline.spec.ts @@ -26,10 +26,10 @@ import { MatrixEvent, Room, } from "../../src/matrix"; -import { logger } from "../../src/logger"; -import { encodeUri } from "../../src/utils"; -import { TestClient } from "../TestClient"; -import { FeatureSupport, Thread, THREAD_RELATION_TYPE } from "../../src/models/thread"; +import {logger} from "../../src/logger"; +import {encodeUri} from "../../src/utils"; +import {TestClient} from "../TestClient"; +import {FeatureSupport, Thread, THREAD_RELATION_TYPE} from "../../src/models/thread"; const userId = "@alice:localhost"; const userName = "Alice"; @@ -1140,6 +1140,27 @@ describe("MatrixClient event timelines", function() { backwards: true, })); }); + + it("should correctly pass pagination token", async () => { + // @ts-ignore + client.clientOpts.experimentalThreadSupport = true; + Thread.setServerSideSupport(FeatureSupport.Experimental); + Thread.setServerSideListSupport(FeatureSupport.Stable); + + const room = client.getRoom(roomId); + const timelineSets = await room?.createThreadsTimelineSets(); + expect(timelineSets).not.toBeNull(); + const [allThreads,] = timelineSets!; + + respondToThreads().check((request) => { + expect(request.queryParams.from).toEqual(RANDOM_TOKEN); + }); + + allThreads.getLiveTimeline().setPaginationToken(RANDOM_TOKEN, Direction.Backward); + await flushHttp(client.paginateEventTimeline(allThreads.getLiveTimeline(), { + backwards: true, + })); + }); }); describe("event timeline for sent events", function() { From dfeb00e7f4bcb5fcefadd52395308e16c4aa696b Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski Date: Wed, 5 Oct 2022 15:27:00 +0200 Subject: [PATCH 11/17] Make Lint happy --- spec/integ/matrix-client-event-timeline.spec.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/spec/integ/matrix-client-event-timeline.spec.ts b/spec/integ/matrix-client-event-timeline.spec.ts index 9d9e6dcc358..e41dff2a874 100644 --- a/spec/integ/matrix-client-event-timeline.spec.ts +++ b/spec/integ/matrix-client-event-timeline.spec.ts @@ -26,10 +26,10 @@ import { MatrixEvent, Room, } from "../../src/matrix"; -import {logger} from "../../src/logger"; -import {encodeUri} from "../../src/utils"; -import {TestClient} from "../TestClient"; -import {FeatureSupport, Thread, THREAD_RELATION_TYPE} from "../../src/models/thread"; +import { logger } from "../../src/logger"; +import { encodeUri } from "../../src/utils"; +import { TestClient } from "../TestClient"; +import { FeatureSupport, Thread, THREAD_RELATION_TYPE } from "../../src/models/thread"; const userId = "@alice:localhost"; const userName = "Alice"; @@ -1128,7 +1128,7 @@ describe("MatrixClient event timelines", function() { const room = client.getRoom(roomId); const timelineSets = await room?.createThreadsTimelineSets(); expect(timelineSets).not.toBeNull(); - const [allThreads,] = timelineSets!; + const [allThreads] = timelineSets!; respondToThreads().check((request) => { expect(request.queryParams.filter).toEqual(JSON.stringify({ @@ -1150,7 +1150,7 @@ describe("MatrixClient event timelines", function() { const room = client.getRoom(roomId); const timelineSets = await room?.createThreadsTimelineSets(); expect(timelineSets).not.toBeNull(); - const [allThreads,] = timelineSets!; + const [allThreads] = timelineSets!; respondToThreads().check((request) => { expect(request.queryParams.from).toEqual(RANDOM_TOKEN); From bf10520555f20d2010562070b0d741cbd01c6686 Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski Date: Wed, 5 Oct 2022 15:47:01 +0200 Subject: [PATCH 12/17] Add new tests to validate new preconditions --- .../matrix-client-event-timeline.spec.ts | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/spec/integ/matrix-client-event-timeline.spec.ts b/spec/integ/matrix-client-event-timeline.spec.ts index e41dff2a874..04146961042 100644 --- a/spec/integ/matrix-client-event-timeline.spec.ts +++ b/spec/integ/matrix-client-event-timeline.spec.ts @@ -228,6 +228,13 @@ describe("getEventTimeline support", function() { }); }); + it("only works with room timelines", function () { + return startClient(httpBackend, client).then(function() { + const timelineSet = new EventTimelineSet(undefined); + expect(client.getEventTimeline(timelineSet, "event")).rejects.toBeTruthy(); + }); + }); + it("scrollback should be able to scroll back to before a gappy /sync", function() { // need a client with timelineSupport disabled to make this work let room: Room; @@ -725,6 +732,59 @@ describe("MatrixClient event timelines", function() { }); describe("getLatestTimeline", function() { + it("timeline support must be enabled to work", function() { + const testClient = new TestClient( + userId, + "DEVICE", + accessToken, + undefined, + { timelineSupport: true }, + ); + client = testClient.client; + httpBackend = testClient.httpBackend; + + return startClient(httpBackend, client).then(() => { + const room = client.getRoom(roomId); + const timelineSet = room.getTimelineSets()[0]; + expect(client.getLatestTimeline(timelineSet)).rejects.toBeTruthy(); + }); + }); + + it("timeline support works when enabled", function() { + const testClient = new TestClient( + userId, + "DEVICE", + accessToken, + undefined, + { timelineSupport: true }, + ); + client = testClient.client; + httpBackend = testClient.httpBackend; + + return startClient(httpBackend, client).then(() => { + const room = client.getRoom(roomId); + const timelineSet = room.getTimelineSets()[0]; + expect(client.getLatestTimeline(timelineSet)).rejects.toBeFalsy(); + }); + }); + + it("only works with room timelines", function () { + const testClient = new TestClient( + userId, + "DEVICE", + accessToken, + undefined, + { timelineSupport: true }, + ); + client = testClient.client; + httpBackend = testClient.httpBackend; + + return startClient(httpBackend, client).then(() => { + const timelineSet = new EventTimelineSet(undefined); + expect(client.getLatestTimeline(timelineSet)).rejects.toBeTruthy(); + }); + }); + it("should create a new timeline for new events", function() { const room = client.getRoom(roomId); const timelineSet = room.getTimelineSets()[0]; From 0d8542b72322d6e969c4eb584cf1caa96446e14a Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski Date: Wed, 5 Oct 2022 15:49:59 +0200 Subject: [PATCH 13/17] Make lint happy :( --- spec/integ/matrix-client-event-timeline.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/integ/matrix-client-event-timeline.spec.ts b/spec/integ/matrix-client-event-timeline.spec.ts index 04146961042..b8c0e196d62 100644 --- a/spec/integ/matrix-client-event-timeline.spec.ts +++ b/spec/integ/matrix-client-event-timeline.spec.ts @@ -228,7 +228,7 @@ describe("getEventTimeline support", function() { }); }); - it("only works with room timelines", function () { + it("only works with room timelines", function() { return startClient(httpBackend, client).then(function() { const timelineSet = new EventTimelineSet(undefined); expect(client.getEventTimeline(timelineSet, "event")).rejects.toBeTruthy(); @@ -768,7 +768,7 @@ describe("MatrixClient event timelines", function() { }); }); - it("only works with room timelines", function () { + it("only works with room timelines", function() { const testClient = new TestClient( userId, "DEVICE", From 3fade9bd83dc3e762c8c59bc6106b4e07cb3ac7e Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski Date: Wed, 5 Oct 2022 16:33:55 +0200 Subject: [PATCH 14/17] Make test preconditions more explicit --- .../matrix-client-event-timeline.spec.ts | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/spec/integ/matrix-client-event-timeline.spec.ts b/spec/integ/matrix-client-event-timeline.spec.ts index b8c0e196d62..d54be87fd2a 100644 --- a/spec/integ/matrix-client-event-timeline.spec.ts +++ b/spec/integ/matrix-client-event-timeline.spec.ts @@ -203,6 +203,16 @@ describe("getEventTimeline support", function() { }); it("timeline support must be enabled to work", function() { + const testClient = new TestClient( + userId, + "DEVICE", + accessToken, + undefined, + { timelineSupport: false }, + ); + client = testClient.client; + httpBackend = testClient.httpBackend; + return startClient(httpBackend, client).then(function() { const room = client.getRoom(roomId); const timelineSet = room.getTimelineSets()[0]; @@ -229,6 +239,16 @@ describe("getEventTimeline support", function() { }); it("only works with room timelines", function() { + const testClient = new TestClient( + userId, + "DEVICE", + accessToken, + undefined, + { timelineSupport: true }, + ); + client = testClient.client; + httpBackend = testClient.httpBackend; + return startClient(httpBackend, client).then(function() { const timelineSet = new EventTimelineSet(undefined); expect(client.getEventTimeline(timelineSet, "event")).rejects.toBeTruthy(); @@ -738,7 +758,7 @@ describe("MatrixClient event timelines", function() { "DEVICE", accessToken, undefined, - { timelineSupport: true }, + { timelineSupport: false }, ); client = testClient.client; httpBackend = testClient.httpBackend; From 25d7a336c5479eb19d7404d409e0cfda56311727 Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski Date: Wed, 5 Oct 2022 16:46:02 +0200 Subject: [PATCH 15/17] Fix issue with infinite pagination in test --- .../matrix-client-event-timeline.spec.ts | 48 +++++++++++-------- 1 file changed, 29 insertions(+), 19 deletions(-) diff --git a/spec/integ/matrix-client-event-timeline.spec.ts b/spec/integ/matrix-client-event-timeline.spec.ts index d54be87fd2a..e8088a2d7f7 100644 --- a/spec/integ/matrix-client-event-timeline.spec.ts +++ b/spec/integ/matrix-client-event-timeline.spec.ts @@ -752,7 +752,9 @@ describe("MatrixClient event timelines", function() { }); describe("getLatestTimeline", function() { - it("timeline support must be enabled to work", function() { + it("timeline support must be enabled to work", async function() { + await client.stopClient(); + const testClient = new TestClient( userId, "DEVICE", @@ -762,15 +764,16 @@ describe("MatrixClient event timelines", function() { ); client = testClient.client; httpBackend = testClient.httpBackend; + await startClient(httpBackend, client); - return startClient(httpBackend, client).then(() => { - const room = client.getRoom(roomId); - const timelineSet = room.getTimelineSets()[0]; - expect(client.getLatestTimeline(timelineSet)).rejects.toBeTruthy(); - }); + const room = client.getRoom(roomId); + const timelineSet = room.getTimelineSets()[0]; + await expect(client.getLatestTimeline(timelineSet)).rejects.toBeTruthy(); }); - it("timeline support works when enabled", function() { + it("timeline support works when enabled", async function() { + await client.stopClient(); + const testClient = new TestClient( userId, "DEVICE", @@ -788,7 +791,9 @@ describe("MatrixClient event timelines", function() { }); }); - it("only works with room timelines", function() { + it("only works with room timelines", async function() { + await client.stopClient(); + const testClient = new TestClient( userId, "DEVICE", @@ -798,11 +803,10 @@ describe("MatrixClient event timelines", function() { ); client = testClient.client; httpBackend = testClient.httpBackend; + await startClient(httpBackend, client); - return startClient(httpBackend, client).then(() => { - const timelineSet = new EventTimelineSet(undefined); - expect(client.getLatestTimeline(timelineSet)).rejects.toBeTruthy(); - }); + const timelineSet = new EventTimelineSet(undefined); + await expect(client.getLatestTimeline(timelineSet)).rejects.toBeTruthy(); }); it("should create a new timeline for new events", function() { @@ -1038,15 +1042,17 @@ describe("MatrixClient event timelines", function() { return request; } - function respondToThreads(): ExpectedHttpRequest { - const request = httpBackend.when("GET", encodeUri("/_matrix/client/r0/rooms/$roomId/threads", { - $roomId: roomId, - })); - request.respond(200, { + function respondToThreads( + response = { chunk: [THREAD_ROOT], state: [], next_batch: RANDOM_TOKEN, - }); + }, + ): ExpectedHttpRequest { + const request = httpBackend.when("GET", encodeUri("/_matrix/client/r0/rooms/$roomId/threads", { + $roomId: roomId, + })); + request.respond(200, response); return request; } @@ -1232,7 +1238,11 @@ describe("MatrixClient event timelines", function() { expect(timelineSets).not.toBeNull(); const [allThreads] = timelineSets!; - respondToThreads().check((request) => { + respondToThreads({ + chunk: [THREAD_ROOT], + state: [], + next_batch: null, + },).check((request) => { expect(request.queryParams.from).toEqual(RANDOM_TOKEN); }); From d5ee99adedd78471fa964d186d62ac170f60ffcb Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski Date: Wed, 5 Oct 2022 16:58:29 +0200 Subject: [PATCH 16/17] Make eslint happy: fix trailing comma --- spec/integ/matrix-client-event-timeline.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/integ/matrix-client-event-timeline.spec.ts b/spec/integ/matrix-client-event-timeline.spec.ts index e8088a2d7f7..8640e82be6c 100644 --- a/spec/integ/matrix-client-event-timeline.spec.ts +++ b/spec/integ/matrix-client-event-timeline.spec.ts @@ -1242,7 +1242,7 @@ describe("MatrixClient event timelines", function() { chunk: [THREAD_ROOT], state: [], next_batch: null, - },).check((request) => { + }).check((request) => { expect(request.queryParams.from).toEqual(RANDOM_TOKEN); }); From fe069a05c6cc5665b337c4f98d15005557d0e7f0 Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski Date: Wed, 5 Oct 2022 23:00:13 +0200 Subject: [PATCH 17/17] ci: empty commit to force CI to run again