From 5f4d10d478f418e0f120d83a050ff9d1b9417c6f Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Tue, 12 Jul 2022 19:03:26 -0500 Subject: [PATCH 01/10] Fix getLatestTimeline not working when the latest even in the room was a threaded message See https://github.com/matrix-org/matrix-react-sdk/pull/8354#discussion_r892481473 Also have to keep in mind that we don't want to mix messages in the wrong timelines (main vs threaded timeline) - https://github.com/matrix-org/matrix-js-sdk/pull/2444 - https://github.com/matrix-org/matrix-js-sdk/pull/2454 --- .gitignore | 1 + spec/integ/matrix-client-room-timeline.spec.js | 2 +- src/client.ts | 4 ---- src/models/event-timeline-set.ts | 12 ++++++++++++ src/models/room.ts | 4 ++++ 5 files changed, 18 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 4fc51ab8595..2e74154ce4f 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ node_modules /*.log package-lock.json .lock-wscript +.DS_Store build/Release coverage lib-cov diff --git a/spec/integ/matrix-client-room-timeline.spec.js b/spec/integ/matrix-client-room-timeline.spec.js index acf751a8c09..63faeb536f6 100644 --- a/spec/integ/matrix-client-room-timeline.spec.js +++ b/spec/integ/matrix-client-room-timeline.spec.js @@ -688,7 +688,7 @@ describe("MatrixClient room timelines", function() { }); // Wait for the timeline to reset(when it goes blank) which means - // it's in the middle of the refrsh logic right before the + // it's in the middle of the refresh logic right before the // `getEventTimeline()` -> `/context`. Then simulate a racey `/sync` // to happen in the middle of all of this refresh timeline logic. We // want to make sure the sync pagination still works as expected diff --git a/src/client.ts b/src/client.ts index 73ca5144ef0..c102871acf8 100644 --- a/src/client.ts +++ b/src/client.ts @@ -5288,10 +5288,6 @@ export class MatrixClient extends TypedEventEmitter newTimeline = await this.client.getEventTimeline(timelineSet, mostRecentEventInTimeline.getId()); } + if (!newTimeline) { + throw new Error(`[refreshLiveTimeline for ${this.roomId}] No new timeline created`); + } + // If a racing `/sync` beat us to creating a new timeline, use that // instead because it's the latest in the room and any new messages in // the scrollback will include the history. From e8dc5907ddca98cddc591535e479831e43cb6668 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Tue, 12 Jul 2022 19:50:10 -0500 Subject: [PATCH 02/10] Adjust tests to not include problem events instead of undefined timeline --- .../matrix-client-event-timeline.spec.ts | 50 +++++++++++++++---- 1 file changed, 40 insertions(+), 10 deletions(-) diff --git a/spec/integ/matrix-client-event-timeline.spec.ts b/spec/integ/matrix-client-event-timeline.spec.ts index 3bde9dd6da3..4819feccf18 100644 --- a/spec/integ/matrix-client-event-timeline.spec.ts +++ b/spec/integ/matrix-client-event-timeline.spec.ts @@ -627,7 +627,7 @@ describe("MatrixClient event timelines", function() { expect(timeline.getEvents().find(e => e.getId() === THREAD_ROOT.event_id)).toBeTruthy(); }); - it("should return undefined when event is not in the thread that the given timelineSet is representing", () => { + it("should not include main timeline event when timelineSet is representing a thread", async () => { // @ts-ignore client.clientOpts.experimentalThreadSupport = true; Thread.setServerSideSupport(true, false); @@ -649,13 +649,40 @@ describe("MatrixClient event timelines", function() { }; }); - return Promise.all([ - expect(client.getEventTimeline(timelineSet, EVENTS[0].event_id)).resolves.toBeUndefined(), - httpBackend.flushAllExpected(), - ]); + // getEventTimeline -> thread.fetchInitialEvents + httpBackend.when("GET", "/rooms/!foo%3Abar/relations/" + + encodeURIComponent(THREAD_ROOT.event_id) + "/" + + encodeURIComponent(THREAD_RELATION_TYPE.name) + "?limit=20&direction=b") + .respond(200, function() { + return { + original_event: THREAD_ROOT, + chunk: [THREAD_REPLY], + // no next batch as this is the oldest end of the timeline + }; + }); + + // getEventTimeline -> thread.fetchEvents + httpBackend.when("GET", "/rooms/!foo%3Abar/relations/" + + encodeURIComponent(THREAD_ROOT.event_id) + "/" + + encodeURIComponent(THREAD_RELATION_TYPE.name) + "?direction=b&limit=50") + .respond(200, function() { + return { + original_event: THREAD_ROOT, + chunk: [THREAD_REPLY], + // no next batch as this is the oldest end of the timeline + }; + }); + + const timelinePromise = client.getEventTimeline(timelineSet, EVENTS[0].event_id); + await httpBackend.flushAllExpected(); + + const timeline = await timelinePromise; + + // The main timeline event should not be in the timelineSet representing a thread + expect(timeline.getEvents().find(e => e.getId() === EVENTS[0].event_id)).toBeFalsy(); }); - it("should return undefined when event is within a thread but timelineSet is not", () => { + it("should not include threaded reply when timelineSet is representing the main room", async () => { // @ts-ignore client.clientOpts.experimentalThreadSupport = true; Thread.setServerSideSupport(true, false); @@ -675,10 +702,13 @@ describe("MatrixClient event timelines", function() { }; }); - return Promise.all([ - expect(client.getEventTimeline(timelineSet, THREAD_REPLY.event_id)).resolves.toBeUndefined(), - httpBackend.flushAllExpected(), - ]); + const timelinePromise = client.getEventTimeline(timelineSet, THREAD_REPLY.event_id); + await httpBackend.flushAllExpected(); + + const timeline = await timelinePromise; + + // The threaded reply should not be in a main room timeline + expect(timeline.getEvents().find(e => e.getId() === THREAD_REPLY.event_id)).toBeFalsy(); }); it("should should add lazy loading filter when requested", async () => { From 9882eb62718e55335dc32f890ed5658ab0e57328 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Tue, 12 Jul 2022 20:14:25 -0500 Subject: [PATCH 03/10] Add tests for not being able to add mixed events --- spec/unit/event-timeline-set.spec.ts | 76 +++++++++++++++++++++------- 1 file changed, 59 insertions(+), 17 deletions(-) diff --git a/spec/unit/event-timeline-set.spec.ts b/spec/unit/event-timeline-set.spec.ts index 42f4bca4de2..6e89b1d4a3c 100644 --- a/spec/unit/event-timeline-set.spec.ts +++ b/spec/unit/event-timeline-set.spec.ts @@ -54,6 +54,23 @@ describe('EventTimelineSet', () => { }); }; + const mkThreadResponse = (root: MatrixEvent) => utils.mkEvent({ + event: true, + type: EventType.RoomMessage, + user: userA, + room: roomId, + content: { + "body": "Thread response :: " + Math.random(), + "m.relates_to": { + "event_id": root.getId(), + "m.in_reply_to": { + "event_id": root.getId(), + }, + "rel_type": "m.thread", + }, + }, + }, room.client); + beforeEach(() => { client = utils.mock(MatrixClient, 'MatrixClient'); client.reEmitter = utils.mock(ReEmitter, 'ReEmitter'); @@ -116,6 +133,13 @@ describe('EventTimelineSet', () => { }); describe('addEventToTimeline', () => { + let thread: Thread; + + beforeEach(() => { + (client.supportsExperimentalThreads as jest.Mock).mockReturnValue(true); + thread = new Thread("!thread_id:server", messageEvent, { room, client }); + }); + it("Adds event to timeline", () => { const liveTimeline = eventTimelineSet.getLiveTimeline(); expect(liveTimeline.getEvents().length).toStrictEqual(0); @@ -143,6 +167,41 @@ describe('EventTimelineSet', () => { ); }).not.toThrow(); }); + + it("should not add an event to a timeline that does not belong to the timelineSet", () => { + const eventTimelineSet2 = new EventTimelineSet(room); + const liveTimeline2 = eventTimelineSet2.getLiveTimeline(); + expect(liveTimeline2.getEvents().length).toStrictEqual(0); + + expect(() => { + eventTimelineSet.addEventToTimeline(messageEvent, liveTimeline2, { + toStartOfTimeline: true, + }); + }).toThrowError(); + }); + + it("should not add a threaded reply to the main room timeline", () => { + const liveTimeline = eventTimelineSet.getLiveTimeline(); + expect(liveTimeline.getEvents().length).toStrictEqual(0); + + const threadedReplyEvent = mkThreadResponse(messageEvent); + + eventTimelineSet.addEventToTimeline(threadedReplyEvent, liveTimeline, { + toStartOfTimeline: true, + }); + expect(liveTimeline.getEvents().length).toStrictEqual(0); + }); + + it("should not add a normal message to the timelineSet representing a thread", () => { + const eventTimelineSetForThread = new EventTimelineSet(room, {}, client, thread); + const liveTimeline = eventTimelineSetForThread.getLiveTimeline(); + expect(liveTimeline.getEvents().length).toStrictEqual(0); + + eventTimelineSetForThread.addEventToTimeline(messageEvent, liveTimeline, { + toStartOfTimeline: true, + }); + expect(liveTimeline.getEvents().length).toStrictEqual(0); + }); }); describe('aggregateRelations', () => { @@ -225,23 +284,6 @@ describe('EventTimelineSet', () => { }); describe("canContain", () => { - const mkThreadResponse = (root: MatrixEvent) => utils.mkEvent({ - event: true, - type: EventType.RoomMessage, - user: userA, - room: roomId, - content: { - "body": "Thread response :: " + Math.random(), - "m.relates_to": { - "event_id": root.getId(), - "m.in_reply_to": { - "event_id": root.getId(), - }, - "rel_type": "m.thread", - }, - }, - }, room.client); - let thread: Thread; beforeEach(() => { From fdeb9e0df751efda307e92990e0364a75c720e7c Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Tue, 12 Jul 2022 20:19:51 -0500 Subject: [PATCH 04/10] Correct JSDoc and types for getEventTimeline --- src/client.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client.ts b/src/client.ts index c102871acf8..ab6dc12d324 100644 --- a/src/client.ts +++ b/src/client.ts @@ -5234,7 +5234,7 @@ export class MatrixClient extends TypedEventEmitterIf the EventTimelineSet object already has the given event in its store, the * corresponding timeline will be returned. Otherwise, a /context request is * made, and used to construct an EventTimeline. - * If the event does not belong to this EventTimelineSet then undefined will be returned. + * If the event does not belong to this EventTimelineSet then it will ignored. * * @param {EventTimelineSet} timelineSet The timelineSet to look for the event in, must be bound to a room * @param {string} eventId The ID of the event to look for @@ -5242,7 +5242,7 @@ 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'" + From 78f80194f69c9eba66b3238e8fd8330c3e4f66f2 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Tue, 12 Jul 2022 20:21:30 -0500 Subject: [PATCH 05/10] No longer need check since it should always return a timeline --- src/models/room.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/models/room.ts b/src/models/room.ts index 8b076939ea6..eaaa8f9efdb 100644 --- a/src/models/room.ts +++ b/src/models/room.ts @@ -981,10 +981,6 @@ export class Room extends TypedEventEmitter newTimeline = await this.client.getEventTimeline(timelineSet, mostRecentEventInTimeline.getId()); } - if (!newTimeline) { - throw new Error(`[refreshLiveTimeline for ${this.roomId}] No new timeline created`); - } - // If a racing `/sync` beat us to creating a new timeline, use that // instead because it's the latest in the room and any new messages in // the scrollback will include the history. From 50b5bb1d031ceb2df26d23291bfdd5b644f4e752 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Tue, 12 Jul 2022 21:09:36 -0500 Subject: [PATCH 06/10] Add getLatestTimeline test for latest event being thread --- .../matrix-client-event-timeline.spec.ts | 63 +++++++++++++++++++ src/client.ts | 4 +- src/models/event-timeline-set.ts | 3 +- 3 files changed, 66 insertions(+), 4 deletions(-) diff --git a/spec/integ/matrix-client-event-timeline.spec.ts b/spec/integ/matrix-client-event-timeline.spec.ts index 4819feccf18..9f97b0fd16f 100644 --- a/spec/integ/matrix-client-event-timeline.spec.ts +++ b/spec/integ/matrix-client-event-timeline.spec.ts @@ -741,6 +741,11 @@ describe("MatrixClient event timelines", function() { }); describe("getLatestTimeline", function() { + beforeEach(() => { + // @ts-ignore + client.clientOpts.experimentalThreadSupport = true; + }); + it("should create a new timeline for new events", function() { const room = client.getRoom(roomId); const timelineSet = room.getTimelineSets()[0]; @@ -791,6 +796,64 @@ describe("MatrixClient event timelines", function() { ]); }); + it("should successfully create a new timeline even when the latest event is a threaded reply", function() { + const room = client.getRoom(roomId); + const timelineSet = room.getTimelineSets()[0]; + expect(timelineSet.thread).toBeUndefined(); + + const latestMessageId = 'threadedEvent1:bar'; + + httpBackend.when("GET", "/rooms/!foo%3Abar/messages") + .respond(200, function() { + return { + chunk: [{ + event_id: latestMessageId, + }], + }; + }); + + httpBackend.when("GET", `/rooms/!foo%3Abar/context/${encodeURIComponent(latestMessageId)}`) + .respond(200, function() { + return { + start: "start_token", + events_before: [THREAD_ROOT, EVENTS[0]], + event: THREAD_REPLY, + events_after: [], + state: [ + ROOM_NAME_EVENT, + USER_MEMBERSHIP_EVENT, + ], + end: "end_token", + }; + }); + + // Make it easy to debug when there is a mismatch of events. We care + // about the event ID for direct comparison and the content for a + // human readable description. + const eventPropertiesToCompare = (event) => { + return { + eventId: event.event_id || event.getId(), + contentBody: event.content?.body || event.getContent()?.body, + }; + }; + return Promise.all([ + client.getLatestTimeline(timelineSet).then(function(tl) { + const events = tl.getEvents(); + const expectedEvents = [EVENTS[0], THREAD_ROOT]; + expect(events.map(event => eventPropertiesToCompare(event))) + .toEqual(expectedEvents.map(event => eventPropertiesToCompare(event))); + // Sanity check: The threaded reply should not be in the timeline + expect(events.find(e => e.getId() === THREAD_REPLY.event_id)).toBeFalsy(); + + expect(tl.getPaginationToken(EventTimeline.BACKWARDS)) + .toEqual("start_token"); + expect(tl.getPaginationToken(EventTimeline.FORWARDS)) + .toEqual("end_token"); + }), + httpBackend.flushAllExpected(), + ]); + }); + it("should throw error when /messages does not return a message", () => { const room = client.getRoom(roomId); const timelineSet = room.getTimelineSets()[0]; diff --git a/src/client.ts b/src/client.ts index ab6dc12d324..39e44a60dfa 100644 --- a/src/client.ts +++ b/src/client.ts @@ -5334,9 +5334,7 @@ export class MatrixClient extends TypedEventEmitter Date: Tue, 12 Jul 2022 21:23:53 -0500 Subject: [PATCH 07/10] Add non-optional client to Room instances --- spec/unit/relations.spec.ts | 19 ++++++++++++++----- spec/unit/room.spec.ts | 5 ++++- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/spec/unit/relations.spec.ts b/spec/unit/relations.spec.ts index 091d95ea914..57a004eeb80 100644 --- a/spec/unit/relations.spec.ts +++ b/spec/unit/relations.spec.ts @@ -18,10 +18,19 @@ import { EventTimelineSet } from "../../src/models/event-timeline-set"; import { MatrixEvent, MatrixEventEvent } from "../../src/models/event"; import { Room } from "../../src/models/room"; import { Relations } from "../../src/models/relations"; +import { MatrixClient } from "../../src"; +import { TestClient } from "../TestClient"; describe("Relations", function() { + let client: MatrixClient; + beforeEach(() => { + client = (new TestClient( + "@alice:example.com", "alicedevice", + )).client; + }) + it("should deduplicate annotations", function() { - const room = new Room("room123", null, null); + const room = new Room("room123", client, null); const relations = new Relations("m.annotation", "m.reaction", room); // Create an instance of an annotation @@ -98,7 +107,7 @@ describe("Relations", function() { // Add the target event first, then the relation event { - const room = new Room("room123", null, null); + const room = new Room("room123", client, null); const relationsCreated = new Promise(resolve => { targetEvent.once(MatrixEventEvent.RelationsCreated, resolve); }); @@ -112,7 +121,7 @@ describe("Relations", function() { // Add the relation event first, then the target event { - const room = new Room("room123", null, null); + const room = new Room("room123", client, null); const relationsCreated = new Promise(resolve => { targetEvent.once(MatrixEventEvent.RelationsCreated, resolve); }); @@ -126,7 +135,7 @@ describe("Relations", function() { }); it("should re-use Relations between all timeline sets in a room", async () => { - const room = new Room("room123", null, null); + const room = new Room("room123", client, null); const timelineSet1 = new EventTimelineSet(room); const timelineSet2 = new EventTimelineSet(room); expect(room.relations).toBe(timelineSet1.relations); @@ -135,7 +144,7 @@ describe("Relations", function() { it("should ignore m.replace for state events", async () => { const userId = "@bob:example.com"; - const room = new Room("room123", null, userId); + const room = new Room("room123", client, userId); const relations = new Relations("m.replace", "m.room.topic", room); // Create an instance of a state event with rel_type m.replace diff --git a/spec/unit/room.spec.ts b/spec/unit/room.spec.ts index 4ed31380dfc..7b2a67573ae 100644 --- a/spec/unit/room.spec.ts +++ b/spec/unit/room.spec.ts @@ -1552,7 +1552,10 @@ describe("Room", function() { }); it("should remove cancelled events from the timeline", function() { - const room = new Room(roomId, null, userA); + const client = (new TestClient( + "@alice:example.com", "alicedevice", + )).client; + const room = new Room(roomId, client, userA); const eventA = utils.mkMessage({ room: roomId, user: userA, event: true, }); From cdef5cba482ea5d88a270737d2adfcf68df11f1e Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Tue, 12 Jul 2022 21:25:34 -0500 Subject: [PATCH 08/10] Fix lint --- spec/unit/relations.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/unit/relations.spec.ts b/spec/unit/relations.spec.ts index 57a004eeb80..1b67e6d9f4f 100644 --- a/spec/unit/relations.spec.ts +++ b/spec/unit/relations.spec.ts @@ -27,7 +27,7 @@ describe("Relations", function() { client = (new TestClient( "@alice:example.com", "alicedevice", )).client; - }) + }); it("should deduplicate annotations", function() { const room = new Room("room123", client, null); From cd41d7eb13a871d27a636a40e8ed76e42b9e1244 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Tue, 19 Jul 2022 18:06:39 -0500 Subject: [PATCH 09/10] We expect void so return void --- src/timeline-window.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/timeline-window.ts b/src/timeline-window.ts index 24c95fbcf07..9a8065d4820 100644 --- a/src/timeline-window.ts +++ b/src/timeline-window.ts @@ -99,7 +99,7 @@ export class TimelineWindow { * * @return {Promise} */ - public load(initialEventId?: string, initialWindowSize = 20): Promise { + public async load(initialEventId?: string, initialWindowSize = 20): Promise { // given an EventTimeline, find the event we were looking for, and initialise our // fields so that the event in question is in the middle of the window. const initFields = (timeline: EventTimeline) => { @@ -135,11 +135,12 @@ export class TimelineWindow { return Promise.resolve(); } - return this.client.getEventTimeline(this.timelineSet, initialEventId).then(initFields); + await this.client.getEventTimeline(this.timelineSet, initialEventId).then(initFields); + return; } else { const tl = this.timelineSet.getLiveTimeline(); initFields(tl); - return Promise.resolve(); + return; } } From f82ea0cb5990f8435a772b508e131775e59da0ae Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Thu, 13 Oct 2022 22:17:49 -0500 Subject: [PATCH 10/10] Fix some lints --- spec/integ/matrix-client-event-timeline.spec.ts | 12 ++++++------ spec/unit/event-timeline-set.spec.ts | 4 ++-- src/client.ts | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/spec/integ/matrix-client-event-timeline.spec.ts b/spec/integ/matrix-client-event-timeline.spec.ts index aa8937d6a89..4b8f3e0745c 100644 --- a/spec/integ/matrix-client-event-timeline.spec.ts +++ b/spec/integ/matrix-client-event-timeline.spec.ts @@ -744,7 +744,7 @@ describe("MatrixClient event timelines", function() { }; }); - const timelinePromise = client.getEventTimeline(timelineSet, THREAD_REPLY.event_id); + const timelinePromise = client.getEventTimeline(timelineSet, THREAD_REPLY.event_id!); await httpBackend.flushAllExpected(); const timeline = await timelinePromise; @@ -803,7 +803,7 @@ describe("MatrixClient event timelines", function() { await startClient(httpBackend, client); const room = client.getRoom(roomId); - const timelineSet = room.getTimelineSets()[0]; + const timelineSet = room!.getTimelineSets()[0]; await expect(client.getLatestTimeline(timelineSet)).rejects.toBeTruthy(); }); @@ -897,7 +897,7 @@ describe("MatrixClient event timelines", function() { it("should successfully create a new timeline even when the latest event is a threaded reply", function() { const room = client.getRoom(roomId); - const timelineSet = room.getTimelineSets()[0]; + const timelineSet = room!.getTimelineSets()[0]; expect(timelineSet.thread).toBeUndefined(); const latestMessageId = 'threadedEvent1:bar'; @@ -937,16 +937,16 @@ describe("MatrixClient event timelines", function() { }; return Promise.all([ client.getLatestTimeline(timelineSet).then(function(tl) { - const events = tl.getEvents(); + const events = tl!.getEvents(); const expectedEvents = [EVENTS[0], THREAD_ROOT]; expect(events.map(event => eventPropertiesToCompare(event))) .toEqual(expectedEvents.map(event => eventPropertiesToCompare(event))); // Sanity check: The threaded reply should not be in the timeline expect(events.find(e => e.getId() === THREAD_REPLY.event_id)).toBeFalsy(); - expect(tl.getPaginationToken(EventTimeline.BACKWARDS)) + expect(tl!.getPaginationToken(EventTimeline.BACKWARDS)) .toEqual("start_token"); - expect(tl.getPaginationToken(EventTimeline.FORWARDS)) + expect(tl!.getPaginationToken(EventTimeline.FORWARDS)) .toEqual("end_token"); }), httpBackend.flushAllExpected(), diff --git a/spec/unit/event-timeline-set.spec.ts b/spec/unit/event-timeline-set.spec.ts index d5214006f3b..bac38db5aa8 100644 --- a/spec/unit/event-timeline-set.spec.ts +++ b/spec/unit/event-timeline-set.spec.ts @@ -250,8 +250,8 @@ describe('EventTimelineSet', () => { }); it('should not return the related events', () => { - eventTimelineSet.relations.aggregateChildEvent(messageEvent); - const relations = eventTimelineSet.relations.getChildEventsForEvent( + eventTimelineSet!.relations.aggregateChildEvent(messageEvent); + const relations = eventTimelineSet!.relations.getChildEventsForEvent( messageEvent.getId(), "m.in_reply_to", EventType.RoomMessage, diff --git a/src/client.ts b/src/client.ts index c86c4b55f90..b9dc32f615b 100644 --- a/src/client.ts +++ b/src/client.ts @@ -5208,7 +5208,7 @@ export class MatrixClient extends TypedEventEmitter