From 1d0ff60ded00ccf9286b18ac385d296dc1542b79 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 22 Mar 2022 13:29:51 +0000 Subject: [PATCH 01/16] De-duplicate (timeline) MatrixEvents via event-mapper --- src/event-mapper.ts | 14 ++++++++++++-- src/models/event.ts | 2 +- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/event-mapper.ts b/src/event-mapper.ts index 53873d11333..150597dd5d3 100644 --- a/src/event-mapper.ts +++ b/src/event-mapper.ts @@ -29,9 +29,19 @@ export function eventMapperFor(client: MatrixClient, options: MapperOpts): Event const decrypt = options.decrypt !== false; function mapper(plainOldJsObject: Partial) { - const event = new MatrixEvent(plainOldJsObject); + const room = client.getRoom(plainOldJsObject.room_id); + + let event: MatrixEvent; + // If the event is already known to the room, let's re-use the model rather than duplicating. + // We avoid doing this to state events as they may be forward or backwards looking which tweaks behaviour. + if (room && plainOldJsObject.state_key === undefined) { + event = room.findEventById(plainOldJsObject.event_id); + } + + if (!event || event.status) { + event = new MatrixEvent(plainOldJsObject); + } - const room = client.getRoom(event.getRoomId()); if (room?.threads.has(event.getId())) { event.setThread(room.threads.get(event.getId())); } diff --git a/src/models/event.ts b/src/models/event.ts index a4d0340a039..f8c3967d65f 100644 --- a/src/models/event.ts +++ b/src/models/event.ts @@ -279,7 +279,7 @@ export class MatrixEvent extends TypedEventEmitter Date: Tue, 22 Mar 2022 13:30:12 +0000 Subject: [PATCH 02/16] Tidy up comments & types --- src/client.ts | 5 ++--- src/models/room.ts | 15 +++++++-------- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/client.ts b/src/client.ts index 57c45778aec..0750b90c7cb 100644 --- a/src/client.ts +++ b/src/client.ts @@ -9258,12 +9258,11 @@ export class MatrixClient extends TypedEventEmitter ( + const parentEvent = room?.findEventById(parentEventId) ?? events.find((mxEv: MatrixEvent) => ( mxEv.getId() === parentEventId )); - // A reaction targetting the thread root needs to be routed to both the - // the main timeline and the associated thread + // A reaction targeting the thread root needs to be routed to both the main timeline and the associated thread const targetingThreadRoot = parentEvent?.isThreadRoot || roots.has(event.relationEventId); if (targetingThreadRoot) { return { diff --git a/src/models/room.ts b/src/models/room.ts index 5d7cba5d68e..36f1bdc06fc 100644 --- a/src/models/room.ts +++ b/src/models/room.ts @@ -47,6 +47,7 @@ import { } from "./thread"; import { Method } from "../http-api"; import { TypedEventEmitter } from "./typed-event-emitter"; +import { IMinimalEvent } from "../sync-accumulator"; // These constants are used as sane defaults when the homeserver doesn't support // the m.room_versions capability. In practice, KNOWN_SAFE_ROOM_VERSION should be @@ -1490,10 +1491,9 @@ export class Room extends TypedEventEmitter } else { const events = [event]; let rootEvent = this.findEventById(event.threadRootId); - // If the rootEvent does not exist in the current sync, then look for - // it over the network + // If the rootEvent does not exist in the current sync, then look for it over the network. try { - let eventData; + let eventData: IMinimalEvent; if (event.threadRootId) { eventData = await this.client.fetchRoomEvent(this.roomId, event.threadRootId); } @@ -1504,10 +1504,9 @@ export class Room extends TypedEventEmitter rootEvent.setUnsigned(eventData.unsigned); } } finally { - // The root event might be not be visible to the person requesting - // it. If it wasn't fetched successfully the thread will work - // in "limited" mode and won't benefit from all the APIs a homeserver - // can provide to enhance the thread experience + // The root event might be not be visible to the person requesting it. + // If it wasn't fetched successfully the thread will work in "limited" mode and won't + // benefit from all the APIs a homeserver can provide to enhance the thread experience thread = this.createThread(rootEvent, events, toStartOfTimeline); } } @@ -1567,7 +1566,7 @@ export class Room extends TypedEventEmitter } } - applyRedaction(event: MatrixEvent): void { + private applyRedaction(event: MatrixEvent): void { if (event.isRedaction()) { const redactId = event.event.redacts; From d88d4ea51351f84f504dae0e71008593a25cd7f6 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 22 Mar 2022 13:32:59 +0000 Subject: [PATCH 03/16] Call setThread appropriately on MatrixEvents constructed outside of the event-mapper --- src/models/room.ts | 1 + src/models/thread.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/src/models/room.ts b/src/models/room.ts index 36f1bdc06fc..8b3d3e6e992 100644 --- a/src/models/room.ts +++ b/src/models/room.ts @@ -1508,6 +1508,7 @@ export class Room extends TypedEventEmitter // If it wasn't fetched successfully the thread will work in "limited" mode and won't // benefit from all the APIs a homeserver can provide to enhance the thread experience thread = this.createThread(rootEvent, events, toStartOfTimeline); + rootEvent.setThread(thread); } } diff --git a/src/models/thread.ts b/src/models/thread.ts index 3f9266e69a6..f7109e6617a 100644 --- a/src/models/thread.ts +++ b/src/models/thread.ts @@ -221,6 +221,7 @@ export class Thread extends TypedEventEmitter { const event = new MatrixEvent(bundledRelationship.latest_event); this.setEventMetadata(event); + event.setThread(this); this.lastEvent = event; } } From 80569820335c3ebb8ff794d130fe34cc3e5f5273 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 22 Mar 2022 13:40:22 +0000 Subject: [PATCH 04/16] Outdent for readability --- src/client.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/client.ts b/src/client.ts index 0750b90c7cb..fec2bb18609 100644 --- a/src/client.ts +++ b/src/client.ts @@ -9278,14 +9278,14 @@ export class MatrixClient extends TypedEventEmitter Date: Tue, 22 Mar 2022 14:20:19 +0000 Subject: [PATCH 05/16] Consolidate and fix handling of upserting thread events into the main room timeline --- src/models/room.ts | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/models/room.ts b/src/models/room.ts index 8b3d3e6e992..ba40e58dae9 100644 --- a/src/models/room.ts +++ b/src/models/room.ts @@ -1191,10 +1191,7 @@ export class Room extends TypedEventEmitter timeline: EventTimeline, paginationToken?: string, ): void { - timeline.getTimelineSet().addEventsToTimeline( - events, toStartOfTimeline, - timeline, paginationToken, - ); + timeline.getTimelineSet().addEventsToTimeline(events, toStartOfTimeline, timeline, paginationToken); } /** @@ -1783,6 +1780,14 @@ export class Room extends TypedEventEmitter } } + private shouldAddEventToMainTimeline(thread: Thread, event: MatrixEvent): boolean { + if (!thread) { + return true; + } + + return !event.isThreadRelation && thread.id === event.getAssociatedId(); + } + /** * Used to aggregate the local echo for a relation, and also * for re-applying a relation after it's redaction has been cancelled, @@ -1795,11 +1800,9 @@ export class Room extends TypedEventEmitter */ private aggregateNonLiveRelation(event: MatrixEvent): void { const thread = this.findThreadForEvent(event); - if (thread) { - thread.timelineSet.aggregateRelations(event); - } + thread?.timelineSet.aggregateRelations(event); - if (thread?.id === event.getAssociatedId() || !thread) { + if (this.shouldAddEventToMainTimeline(thread, event)) { // TODO: We should consider whether this means it would be a better // design to lift the relations handling up to the room instead. for (let i = 0; i < this.timelineSets.length; i++) { @@ -1856,11 +1859,9 @@ export class Room extends TypedEventEmitter localEvent.handleRemoteEcho(remoteEvent.event); const thread = this.findThreadForEvent(remoteEvent); - if (thread) { - thread.timelineSet.handleRemoteEcho(localEvent, oldEventId, newEventId); - } + thread?.timelineSet.handleRemoteEcho(localEvent, oldEventId, newEventId); - if (thread?.id === remoteEvent.getAssociatedId() || !thread) { + if (this.shouldAddEventToMainTimeline(thread, remoteEvent)) { for (let i = 0; i < this.timelineSets.length; i++) { const timelineSet = this.timelineSets[i]; @@ -1927,10 +1928,9 @@ export class Room extends TypedEventEmitter event.replaceLocalEventId(newEventId); const thread = this.findThreadForEvent(event); - if (thread) { - thread.timelineSet.replaceEventId(oldEventId, newEventId); - } - if (thread?.id === event.getAssociatedId() || !thread) { + thread?.timelineSet.replaceEventId(oldEventId, newEventId); + + if (this.shouldAddEventToMainTimeline(thread, event)) { // if the event was already in the timeline (which will be the case if // opts.pendingEventOrdering==chronological), we need to update the // timeline map. From 2869bc9e81072d94829e1aee6f5a519055dc7821 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 22 Mar 2022 15:18:31 +0000 Subject: [PATCH 06/16] Tidy --- src/client.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/client.ts b/src/client.ts index fec2bb18609..85a5046e214 100644 --- a/src/client.ts +++ b/src/client.ts @@ -3981,7 +3981,6 @@ export class MatrixClient extends TypedEventEmitter this.decryptEventIfNeeded(e))); @@ -6663,6 +6663,7 @@ export class MatrixClient extends TypedEventEmitter e.getType() === eventType); } } + if (originalEvent && relationType === RelationType.Replace) { events = events.filter(e => e.getSender() === originalEvent.getSender()); } @@ -9288,8 +9289,11 @@ export class MatrixClient extends TypedEventEmitter Date: Wed, 23 Mar 2022 08:40:44 +0000 Subject: [PATCH 07/16] Add missing null-guard and tidy --- src/models/room.ts | 24 ++++++++++++------------ src/models/thread.ts | 9 ++------- 2 files changed, 14 insertions(+), 19 deletions(-) diff --git a/src/models/room.ts b/src/models/room.ts index ba40e58dae9..463921a5715 100644 --- a/src/models/room.ts +++ b/src/models/room.ts @@ -992,17 +992,15 @@ export class Room extends TypedEventEmitter } /** - * Get an event which is stored in our unfiltered timeline set or in a thread + * Get an event which is stored in our unfiltered timeline set, or in a thread * - * @param {string} eventId event ID to look for + * @param {string} eventId event ID to look for * @return {?module:models/event.MatrixEvent} the given event, or undefined if unknown */ public findEventById(eventId: string): MatrixEvent | undefined { let event = this.getUnfilteredTimelineSet().findEventById(eventId); - if (event) { - return event; - } else { + if (!event) { const threads = this.getThreads(); for (let i = 0; i < threads.length; i++) { const thread = threads[i]; @@ -1012,6 +1010,8 @@ export class Room extends TypedEventEmitter } } } + + return event; } /** @@ -1505,7 +1505,9 @@ export class Room extends TypedEventEmitter // If it wasn't fetched successfully the thread will work in "limited" mode and won't // benefit from all the APIs a homeserver can provide to enhance the thread experience thread = this.createThread(rootEvent, events, toStartOfTimeline); - rootEvent.setThread(thread); + if (thread) { + rootEvent.setThread(thread); + } } } @@ -1941,12 +1943,10 @@ export class Room extends TypedEventEmitter } else if (newStatus == EventStatus.CANCELLED) { // remove it from the pending event list, or the timeline. if (this.pendingEventList) { - const idx = this.pendingEventList.findIndex(ev => ev.getId() === oldEventId); - if (idx !== -1) { - const [removedEvent] = this.pendingEventList.splice(idx, 1); - if (removedEvent.isRedaction()) { - this.revertRedactionLocalEcho(removedEvent); - } + const removedEvent = this.getPendingEvent(oldEventId); + this.removePendingEvent(oldEventId); + if (removedEvent.isRedaction()) { + this.revertRedactionLocalEcho(removedEvent); } } this.removeEvent(oldEventId); diff --git a/src/models/thread.ts b/src/models/thread.ts index f7109e6617a..4f91f7143b6 100644 --- a/src/models/thread.ts +++ b/src/models/thread.ts @@ -93,14 +93,9 @@ export class Thread extends TypedEventEmitter { RoomEvent.TimelineReset, ]); - // If we weren't able to find the root event, it's probably missing + // If we weren't able to find the root event, it's probably missing, // and we define the thread ID from one of the thread relation - if (!rootEvent) { - this.id = opts?.initialEvents - ?.find(event => event.isThreadRelation)?.relationEventId; - } else { - this.id = rootEvent.getId(); - } + this.id = rootEvent?.getId() ?? opts?.initialEvents?.find(event => event.isThreadRelation)?.relationEventId; this.initialiseThread(this.rootEvent); opts?.initialEvents?.forEach(event => this.addEvent(event, false)); From dc77a606adc76007f14ed1de763245bb61ed2186 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 23 Mar 2022 09:35:37 +0000 Subject: [PATCH 08/16] Add workaround for Synapse missing 2nd order bundled relationships --- src/models/thread.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/models/thread.ts b/src/models/thread.ts index 4f91f7143b6..48dfecac206 100644 --- a/src/models/thread.ts +++ b/src/models/thread.ts @@ -206,7 +206,7 @@ export class Thread extends TypedEventEmitter { this.emit(ThreadEvent.Update, this); } - private initialiseThread(rootEvent: MatrixEvent | undefined): void { + private async initialiseThread(rootEvent: MatrixEvent | undefined): Promise { const bundledRelationship = rootEvent ?.getServerAggregatedRelation(THREAD_RELATION_TYPE.name); @@ -218,6 +218,10 @@ export class Thread extends TypedEventEmitter { this.setEventMetadata(event); event.setThread(this); this.lastEvent = event; + + // XXX: Workaround for Synapse incorrectly sending 2nd-order bundled relationships + const eventData = await this.client.fetchRoomEvent(this.roomId, event.getId()); + event.setUnsigned(eventData.unsigned); } } From 3c562ef09aa2a777152933410e50bfbb8b2b173e Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 23 Mar 2022 10:10:13 +0000 Subject: [PATCH 09/16] Replace workaround with merging unsigned fields during MatrixEvent de-duplication --- src/event-mapper.ts | 3 +++ src/models/thread.ts | 4 ---- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/event-mapper.ts b/src/event-mapper.ts index 150597dd5d3..0e1e00af214 100644 --- a/src/event-mapper.ts +++ b/src/event-mapper.ts @@ -40,6 +40,9 @@ export function eventMapperFor(client: MatrixClient, options: MapperOpts): Event if (!event || event.status) { event = new MatrixEvent(plainOldJsObject); + } else { + // merge the latest unsigned data from the server + event.setUnsigned({ ...event.getUnsigned(), ...plainOldJsObject.unsigned }); } if (room?.threads.has(event.getId())) { diff --git a/src/models/thread.ts b/src/models/thread.ts index 48dfecac206..742b7c2d034 100644 --- a/src/models/thread.ts +++ b/src/models/thread.ts @@ -218,10 +218,6 @@ export class Thread extends TypedEventEmitter { this.setEventMetadata(event); event.setThread(this); this.lastEvent = event; - - // XXX: Workaround for Synapse incorrectly sending 2nd-order bundled relationships - const eventData = await this.client.fetchRoomEvent(this.roomId, event.getId()); - event.setUnsigned(eventData.unsigned); } } From ea148de6d945febe2e5b49f03a7ed628b59187ad Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 23 Mar 2022 10:21:36 +0000 Subject: [PATCH 10/16] Fix issues with edited events not affecting the thread summary --- src/models/thread.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/models/thread.ts b/src/models/thread.ts index 742b7c2d034..dddbb0213d8 100644 --- a/src/models/thread.ts +++ b/src/models/thread.ts @@ -249,6 +249,11 @@ export class Thread extends TypedEventEmitter { * Finds an event by ID in the current thread */ public findEventById(eventId: string) { + // Check the lastEvent as it may have been created based on a bundled relationship and not in a timeline + if (this.lastEvent?.getId() === eventId) { + return this.lastEvent; + } + return this.timelineSet.findEventById(eventId); } From bad45a9f19707cfc0d33bb400e3e65e202c55b14 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 23 Mar 2022 11:06:09 +0000 Subject: [PATCH 11/16] undo unrelated change --- src/models/thread.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/models/thread.ts b/src/models/thread.ts index dddbb0213d8..897305f4d9f 100644 --- a/src/models/thread.ts +++ b/src/models/thread.ts @@ -206,7 +206,7 @@ export class Thread extends TypedEventEmitter { this.emit(ThreadEvent.Update, this); } - private async initialiseThread(rootEvent: MatrixEvent | undefined): Promise { + private initialiseThread(rootEvent: MatrixEvent | undefined): void { const bundledRelationship = rootEvent ?.getServerAggregatedRelation(THREAD_RELATION_TYPE.name); From 37cdb93a5afc5319c5cbf375c85b60fe2e82f636 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 23 Mar 2022 11:17:23 +0000 Subject: [PATCH 12/16] Fix test util --- spec/test-utils/test-utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/test-utils/test-utils.js b/spec/test-utils/test-utils.js index df137ba6f53..b2c180205be 100644 --- a/spec/test-utils/test-utils.js +++ b/spec/test-utils/test-utils.js @@ -85,7 +85,7 @@ export function mkEvent(opts) { room_id: opts.room, sender: opts.sender || opts.user, // opts.user for backwards-compat content: opts.content, - unsigned: opts.unsigned, + unsigned: opts.unsigned || {}, event_id: "$" + Math.random() + "-" + Math.random(), }; if (opts.skey !== undefined) { From fc73566f0b00ae3f6143296c8788720a5d2a42c9 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 23 Mar 2022 13:44:03 +0000 Subject: [PATCH 13/16] Threads need to ask server for edits when using relations to fetch events --- src/client.ts | 6 ++---- src/models/thread.ts | 29 +++++++++++++++++++++++++---- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/src/client.ts b/src/client.ts index 209ccae45ee..09d610b9449 100644 --- a/src/client.ts +++ b/src/client.ts @@ -6620,10 +6620,8 @@ export class MatrixClient extends TypedEventEmitter { this.addEventToTimeline(event, toStartOfTimeline); await this.client.decryptEventIfNeeded(event, {}); - } + } else { + await this.fetchEditsWhereNeeded(event); - if (Thread.hasServerSideSupport && this.initialEventsFetched) { - if (event.localTimestamp > this.lastReply().localTimestamp) { + if (this.initialEventsFetched && event.localTimestamp > this.lastReply().localTimestamp) { this.addEventToTimeline(event, false); } } @@ -218,9 +219,26 @@ export class Thread extends TypedEventEmitter { this.setEventMetadata(event); event.setThread(this); this.lastEvent = event; + + this.fetchEditsWhereNeeded(event); } } + // XXX: Workaround for https://github.com/matrix-org/matrix-spec-proposals/pull/2676/files#r827240084 + private async fetchEditsWhereNeeded(...events: MatrixEvent[]): Promise { + return Promise.all(events.filter(e => e.isEncrypted()).map((event: MatrixEvent) => { + return this.client.relations(this.roomId, event.getId(), RelationType.Replace, event.getType(), { + limit: 1, + }).then(relations => { + if (relations.events.length) { + event.makeReplaced(relations.events[0]); + } + }).catch(e => { + logger.error("Failed to load edits for encrypted thread event", e); + }); + })); + } + public async fetchInitialEvents(): Promise<{ originalEvent: MatrixEvent; events: MatrixEvent[]; @@ -231,6 +249,7 @@ export class Thread extends TypedEventEmitter { this.initialEventsFetched = true; return null; } + try { const response = await this.fetchEvents(); this.initialEventsFetched = true; @@ -330,6 +349,8 @@ export class Thread extends TypedEventEmitter { events = [...events, originalEvent]; } + await this.fetchEditsWhereNeeded(...events); + await Promise.all(events.map(event => { this.setEventMetadata(event); return this.client.decryptEventIfNeeded(event); From 05e0c17f28deab9eb4bb5475af5a02f5ab71538d Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 23 Mar 2022 15:59:18 +0000 Subject: [PATCH 14/16] Add tests --- spec/unit/event-mapper.spec.ts | 180 ++++++++++++++++++++++++++++++++ spec/unit/event.spec.js | 2 +- spec/unit/matrix-client.spec.ts | 16 +++ src/event-mapper.ts | 1 + 4 files changed, 198 insertions(+), 1 deletion(-) create mode 100644 spec/unit/event-mapper.spec.ts diff --git a/spec/unit/event-mapper.spec.ts b/spec/unit/event-mapper.spec.ts new file mode 100644 index 00000000000..a46c955b7bd --- /dev/null +++ b/spec/unit/event-mapper.spec.ts @@ -0,0 +1,180 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { MatrixClient, MatrixEvent, MatrixEventEvent, MatrixScheduler, Room } from "../../src"; +import { eventMapperFor } from "../../src/event-mapper"; +import { IStore } from "../../src/store"; + +describe("eventMapperFor", function() { + let rooms: Room[] = []; + + const userId = "@test:example.org"; + + let client: MatrixClient; + + beforeEach(() => { + client = new MatrixClient({ + baseUrl: "https://my.home.server", + accessToken: "my.access.token", + request: function() {} as any, // NOP + store: { + getRoom(roomId: string): Room | null { + return rooms.find(r => r.roomId === roomId); + }, + } as IStore, + scheduler: { + setProcessFunction: jest.fn(), + } as unknown as MatrixScheduler, + userId: userId, + }); + + rooms = []; + }); + + it("should de-duplicate MatrixEvent instances by means of findEventById on the room object", async () => { + const roomId = "!room:example.org"; + const room = new Room(roomId, client, userId); + rooms.push(room); + + const mapper = eventMapperFor(client, { + preventReEmit: true, + decrypt: false, + }); + + const eventId = "$event1:server"; + const eventDefinition = { + type: "m.room.message", + room_id: roomId, + sender: userId, + content: { + body: "body", + }, + unsigned: {}, + event_id: eventId, + }; + + const event = mapper(eventDefinition); + expect(event).toBeInstanceOf(MatrixEvent); + + room.addLiveEvents([event]); + expect(room.findEventById(eventId)).toBe(event); + + const event2 = mapper(eventDefinition); + expect(event).toBe(event2); + }); + + it("should not de-duplicate state events due to directionality of sentinel members", async () => { + const roomId = "!room:example.org"; + const room = new Room(roomId, client, userId); + rooms.push(room); + + const mapper = eventMapperFor(client, { + preventReEmit: true, + decrypt: false, + }); + + const eventId = "$event1:server"; + const eventDefinition = { + type: "m.room.name", + room_id: roomId, + sender: userId, + content: { + name: "Room name", + }, + unsigned: {}, + event_id: eventId, + state_key: "", + }; + + const event = mapper(eventDefinition); + expect(event).toBeInstanceOf(MatrixEvent); + + room.oldState.setStateEvents([event]); + room.currentState.setStateEvents([event]); + room.addLiveEvents([event]); + expect(room.findEventById(eventId)).toBe(event); + + const event2 = mapper(eventDefinition); + expect(event).not.toBe(event2); + }); + + it("should decrypt appropriately", async () => { + const roomId = "!room:example.org"; + const room = new Room(roomId, client, userId); + rooms.push(room); + + const eventId = "$event1:server"; + const eventDefinition = { + type: "m.room.encrypted", + room_id: roomId, + sender: userId, + content: { + ciphertext: "", + }, + unsigned: {}, + event_id: eventId, + }; + + const decryptEventIfNeededSpy = jest.spyOn(client, "decryptEventIfNeeded"); + decryptEventIfNeededSpy.mockResolvedValue(); // stub it out + + const mapper = eventMapperFor(client, { + decrypt: true, + }); + const event = mapper(eventDefinition); + expect(event).toBeInstanceOf(MatrixEvent); + expect(decryptEventIfNeededSpy).toHaveBeenCalledWith(event); + }); + + it("should configure re-emitter appropriately", async () => { + const roomId = "!room:example.org"; + const room = new Room(roomId, client, userId); + rooms.push(room); + + const eventId = "$event1:server"; + const eventDefinition = { + type: "m.room.message", + room_id: roomId, + sender: userId, + content: { + body: "body", + }, + unsigned: {}, + event_id: eventId, + }; + + const evListener = jest.fn(); + client.on(MatrixEventEvent.Replaced, evListener); + + const noReEmitMapper = eventMapperFor(client, { + preventReEmit: true, + }); + const event1 = noReEmitMapper(eventDefinition); + expect(event1).toBeInstanceOf(MatrixEvent); + event1.emit(MatrixEventEvent.Replaced, event1); + expect(evListener).not.toHaveBeenCalled(); + + const reEmitMapper = eventMapperFor(client, { + preventReEmit: false, + }); + const event2 = reEmitMapper(eventDefinition); + expect(event2).toBeInstanceOf(MatrixEvent); + event2.emit(MatrixEventEvent.Replaced, event2); + expect(evListener.mock.calls[0][0]).toEqual(event2); + + expect(event1).not.toBe(event2); // the event wasn't added to a room so de-duplication wouldn't occur + }); +}); diff --git a/spec/unit/event.spec.js b/spec/unit/event.spec.js index a28b9224fcd..897d469b61f 100644 --- a/spec/unit/event.spec.js +++ b/spec/unit/event.spec.js @@ -1,6 +1,6 @@ /* Copyright 2017 New Vector Ltd -Copyright 2019 The Matrix.org Foundaction C.I.C. +Copyright 2019 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/spec/unit/matrix-client.spec.ts b/spec/unit/matrix-client.spec.ts index 67b922991ea..251ade94c48 100644 --- a/spec/unit/matrix-client.spec.ts +++ b/spec/unit/matrix-client.spec.ts @@ -1,3 +1,19 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + import { logger } from "../../src/logger"; import { MatrixClient } from "../../src/client"; import { Filter } from "../../src/filter"; diff --git a/src/event-mapper.ts b/src/event-mapper.ts index 0e1e00af214..7b4f106f884 100644 --- a/src/event-mapper.ts +++ b/src/event-mapper.ts @@ -59,6 +59,7 @@ export function eventMapperFor(client: MatrixClient, options: MapperOpts): Event client.decryptEventIfNeeded(event); } } + if (!preventReEmit) { client.reEmitter.reEmit(event, [ MatrixEventEvent.Replaced, From 021a2feb7b4b327cf3c91d0222e6eae21ffe8bbb Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 24 Mar 2022 12:14:27 +0000 Subject: [PATCH 15/16] Remove stale test --- spec/unit/room.spec.ts | 48 ------------------------------------------ 1 file changed, 48 deletions(-) diff --git a/spec/unit/room.spec.ts b/spec/unit/room.spec.ts index dbb5f33d50d..f854e4e921f 100644 --- a/spec/unit/room.spec.ts +++ b/spec/unit/room.spec.ts @@ -1867,54 +1867,6 @@ describe("Room", function() { expect(() => room.createThread(rootEvent, [])).not.toThrow(); }); - - it("should not add events before server supports is known", function() { - Thread.hasServerSideSupport = undefined; - - const rootEvent = new MatrixEvent({ - event_id: "$666", - room_id: roomId, - content: {}, - unsigned: { - "age": 1, - "m.relations": { - "m.thread": { - latest_event: null, - count: 1, - current_user_participated: false, - }, - }, - }, - }); - - let age = 1; - function mkEvt(id): MatrixEvent { - return new MatrixEvent({ - event_id: id, - room_id: roomId, - content: { - "m.relates_to": { - "rel_type": "m.thread", - "event_id": "$666", - }, - }, - unsigned: { - "age": age++, - }, - }); - } - - const thread = room.createThread(rootEvent, []); - expect(thread.length).toBe(0); - - thread.addEvent(mkEvt("$1")); - expect(thread.length).toBe(0); - - Thread.hasServerSideSupport = true; - - thread.addEvent(mkEvt("$2")); - expect(thread.length).toBeGreaterThan(0); - }); }); }); }); From 37a0a35e15b8dd3f78b4589575e82778d2d28cdf Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 24 Mar 2022 12:18:19 +0000 Subject: [PATCH 16/16] delint --- spec/unit/room.spec.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/unit/room.spec.ts b/spec/unit/room.spec.ts index f854e4e921f..6977c862f89 100644 --- a/spec/unit/room.spec.ts +++ b/spec/unit/room.spec.ts @@ -26,7 +26,6 @@ import { Room } from "../../src/models/room"; import { RoomState } from "../../src/models/room-state"; import { UNSTABLE_ELEMENT_FUNCTIONAL_USERS } from "../../src/@types/event"; import { TestClient } from "../TestClient"; -import { Thread } from "../../src/models/thread"; describe("Room", function() { const roomId = "!foo:bar";