From 8b2a334ac4e4976edb2ea629a9280d8fe6ea18b1 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Tue, 7 Nov 2023 13:44:36 +0000 Subject: [PATCH 1/7] Revert "Revert "Move redacted messages out of any thread, into main timeline."" This reverts commit 46114a025c5ec5b2658803c8c86d5e855b55a4fb. --- spec/unit/models/event.spec.ts | 110 ++++++++++++++++++++++++++++++++- spec/unit/room-state.spec.ts | 9 ++- spec/unit/room.spec.ts | 2 +- src/models/event.ts | 25 +++++++- src/models/room.ts | 2 +- 5 files changed, 141 insertions(+), 7 deletions(-) diff --git a/spec/unit/models/event.spec.ts b/spec/unit/models/event.spec.ts index efb2404f6..bcc2b42a2 100644 --- a/spec/unit/models/event.spec.ts +++ b/spec/unit/models/event.spec.ts @@ -14,10 +14,19 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { MockedObject } from "jest-mock"; + import { MatrixEvent, MatrixEventEvent } from "../../../src/models/event"; import { emitPromise } from "../../test-utils/test-utils"; import { Crypto, IEventDecryptionResult } from "../../../src/crypto"; -import { IAnnotatedPushRule, PushRuleActionName, TweakName } from "../../../src"; +import { + IAnnotatedPushRule, + MatrixClient, + PushRuleActionName, + Room, + THREAD_RELATION_TYPE, + TweakName, +} from "../../../src"; describe("MatrixEvent", () => { it("should create copies of itself", () => { @@ -77,17 +86,98 @@ describe("MatrixEvent", () => { expect(ev.getWireContent().body).toBeUndefined(); expect(ev.getWireContent().ciphertext).toBe("xyz"); + const mockClient = {} as unknown as MockedObject; + const room = new Room("!roomid:e.xyz", mockClient, "myname"); const redaction = new MatrixEvent({ type: "m.room.redaction", redacts: ev.getId(), }); - ev.makeRedacted(redaction); + ev.makeRedacted(redaction, room); expect(ev.getContent().body).toBeUndefined(); expect(ev.getWireContent().body).toBeUndefined(); expect(ev.getWireContent().ciphertext).toBeUndefined(); }); + it("should remain in the main timeline when redacted", async () => { + // Given an event in the main timeline + const mockClient = { + supportsThreads: jest.fn().mockReturnValue(true), + decryptEventIfNeeded: jest.fn().mockReturnThis(), + getUserId: jest.fn().mockReturnValue("@user:server"), + } as unknown as MockedObject; + const room = new Room("!roomid:e.xyz", mockClient, "myname"); + const ev = new MatrixEvent({ + type: "m.room.message", + content: { + body: "Test", + }, + event_id: "$event1:server", + }); + + await room.addLiveEvents([ev]); + await room.createThreadsTimelineSets(); + expect(ev.threadRootId).toBeUndefined(); + expect(mainTimelineLiveEventIds(room)).toEqual(["$event1:server"]); + + // When I redact it + const redaction = new MatrixEvent({ + type: "m.room.redaction", + redacts: ev.getId(), + }); + ev.makeRedacted(redaction, room); + + // Then it remains in the main timeline + expect(ev.threadRootId).toBeUndefined(); + expect(mainTimelineLiveEventIds(room)).toEqual(["$event1:server"]); + }); + + it("should move into the main timeline when redacted", async () => { + // Given an event in a thread + const mockClient = { + supportsThreads: jest.fn().mockReturnValue(true), + decryptEventIfNeeded: jest.fn().mockReturnThis(), + getUserId: jest.fn().mockReturnValue("@user:server"), + } as unknown as MockedObject; + const room = new Room("!roomid:e.xyz", mockClient, "myname"); + const threadRoot = new MatrixEvent({ + type: "m.room.message", + content: { + body: "threadRoot", + }, + event_id: "$threadroot:server", + }); + const ev = new MatrixEvent({ + type: "m.room.message", + content: { + "body": "Test", + "m.relates_to": { + rel_type: THREAD_RELATION_TYPE.name, + event_id: "$threadroot:server", + }, + }, + event_id: "$event1:server", + }); + + await room.addLiveEvents([threadRoot, ev]); + await room.createThreadsTimelineSets(); + expect(ev.threadRootId).toEqual("$threadroot:server"); + expect(mainTimelineLiveEventIds(room)).toEqual(["$threadroot:server"]); + expect(threadLiveEventIds(room, 0)).toEqual(["$threadroot:server", "$event1:server"]); + + // When I redact it + const redaction = new MatrixEvent({ + type: "m.room.redaction", + redacts: ev.getId(), + }); + ev.makeRedacted(redaction, room); + + // Then it disappears from the thread and appears in the main timeline + expect(ev.threadRootId).toBeUndefined(); + expect(mainTimelineLiveEventIds(room)).toEqual(["$threadroot:server", "$event1:server"]); + expect(threadLiveEventIds(room, 0)).not.toContain("$event1:server"); + }); + describe("applyVisibilityEvent", () => { it("should emit VisibilityChange if a change was made", async () => { const ev = new MatrixEvent({ @@ -330,3 +420,19 @@ describe("MatrixEvent", () => { expect(stateEvent.threadRootId).toBeUndefined(); }); }); + +function mainTimelineLiveEventIds(room: Room): Array { + return room + .getLiveTimeline() + .getEvents() + .map((e) => e.getId()!); +} + +function threadLiveEventIds(room: Room, threadIndex: number): Array { + return room + .getThreads() + [threadIndex].getUnfilteredTimelineSet() + .getLiveTimeline() + .getEvents() + .map((e) => e.getId()!); +} diff --git a/spec/unit/room-state.spec.ts b/spec/unit/room-state.spec.ts index 435d2e33d..6c60d08f9 100644 --- a/spec/unit/room-state.spec.ts +++ b/spec/unit/room-state.spec.ts @@ -27,6 +27,7 @@ import { M_BEACON } from "../../src/@types/beacon"; import { MatrixClient } from "../../src/client"; import { DecryptionError } from "../../src/crypto/algorithms"; import { defer } from "../../src/utils"; +import { Room } from "../../src/models/room"; describe("RoomState", function () { const roomId = "!foo:bar"; @@ -362,9 +363,11 @@ describe("RoomState", function () { }); it("does not add redacted beacon info events to state", () => { + const mockClient = {} as unknown as MockedObject; const redactedBeaconEvent = makeBeaconInfoEvent(userA, roomId); const redactionEvent = new MatrixEvent({ type: "m.room.redaction" }); - redactedBeaconEvent.makeRedacted(redactionEvent); + const room = new Room(roomId, mockClient, userA); + redactedBeaconEvent.makeRedacted(redactionEvent, room); const emitSpy = jest.spyOn(state, "emit"); state.setStateEvents([redactedBeaconEvent]); @@ -394,11 +397,13 @@ describe("RoomState", function () { }); it("destroys and removes redacted beacon events", () => { + const mockClient = {} as unknown as MockedObject; const beaconId = "$beacon1"; const beaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, beaconId); const redactedBeaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, beaconId); const redactionEvent = new MatrixEvent({ type: "m.room.redaction", redacts: beaconEvent.getId() }); - redactedBeaconEvent.makeRedacted(redactionEvent); + const room = new Room(roomId, mockClient, userA); + redactedBeaconEvent.makeRedacted(redactionEvent, room); state.setStateEvents([beaconEvent]); const beaconInstance = state.beacons.get(getBeaconInfoIdentifier(beaconEvent)); diff --git a/spec/unit/room.spec.ts b/spec/unit/room.spec.ts index f32cf5fb6..481ccdcaf 100644 --- a/spec/unit/room.spec.ts +++ b/spec/unit/room.spec.ts @@ -3654,7 +3654,7 @@ describe("Room", function () { expect(room.polls.get(pollStartEvent.getId()!)).toBeTruthy(); const redactedEvent = new MatrixEvent({ type: "m.room.redaction" }); - pollStartEvent.makeRedacted(redactedEvent); + pollStartEvent.makeRedacted(redactedEvent, room); await flushPromises(); diff --git a/src/models/event.ts b/src/models/event.ts index 66c410ba2..d1e56f140 100644 --- a/src/models/event.ts +++ b/src/models/event.ts @@ -45,6 +45,8 @@ import { DecryptionError } from "../crypto/algorithms"; import { CryptoBackend } from "../common-crypto/CryptoBackend"; import { WITHHELD_MESSAGES } from "../crypto/OlmDevice"; import { IAnnotatedPushRule } from "../@types/PushRules"; +import { Room } from "./room"; +import { EventTimeline } from "./event-timeline"; export { EventStatus } from "./event-status"; @@ -1150,13 +1152,19 @@ export class MatrixEvent extends TypedEventEmitter { // if we know about this event, redact its contents now. const redactedEvent = redactId ? this.findEventById(redactId) : undefined; if (redactedEvent) { - redactedEvent.makeRedacted(event); + redactedEvent.makeRedacted(event, this); // If this is in the current state, replace it with the redacted version if (redactedEvent.isState()) { From a3762c8e2208cbc5e5a2f40539d25ee8799df2bd Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Tue, 7 Nov 2023 13:44:44 +0000 Subject: [PATCH 2/7] Revert "Revert "Move redaction event tests into their own describe block"" This reverts commit 2e24481df335411ee489ac7046c5514821afa4fa. --- spec/unit/models/event.spec.ts | 202 +++++++++++++++++---------------- 1 file changed, 102 insertions(+), 100 deletions(-) diff --git a/spec/unit/models/event.spec.ts b/spec/unit/models/event.spec.ts index bcc2b42a2..c3ac23ce4 100644 --- a/spec/unit/models/event.spec.ts +++ b/spec/unit/models/event.spec.ts @@ -70,112 +70,114 @@ describe("MatrixEvent", () => { expect(a.toSnapshot().isEquivalentTo(b)).toBe(false); }); - it("should prune clearEvent when being redacted", () => { - const ev = new MatrixEvent({ - type: "m.room.message", - content: { - body: "Test", - }, - event_id: "$event1:server", - }); - - expect(ev.getContent().body).toBe("Test"); - expect(ev.getWireContent().body).toBe("Test"); - ev.makeEncrypted("m.room.encrypted", { ciphertext: "xyz" }, "", ""); - expect(ev.getContent().body).toBe("Test"); - expect(ev.getWireContent().body).toBeUndefined(); - expect(ev.getWireContent().ciphertext).toBe("xyz"); - - const mockClient = {} as unknown as MockedObject; - const room = new Room("!roomid:e.xyz", mockClient, "myname"); - const redaction = new MatrixEvent({ - type: "m.room.redaction", - redacts: ev.getId(), - }); - - ev.makeRedacted(redaction, room); - expect(ev.getContent().body).toBeUndefined(); - expect(ev.getWireContent().body).toBeUndefined(); - expect(ev.getWireContent().ciphertext).toBeUndefined(); - }); - - it("should remain in the main timeline when redacted", async () => { - // Given an event in the main timeline - const mockClient = { - supportsThreads: jest.fn().mockReturnValue(true), - decryptEventIfNeeded: jest.fn().mockReturnThis(), - getUserId: jest.fn().mockReturnValue("@user:server"), - } as unknown as MockedObject; - const room = new Room("!roomid:e.xyz", mockClient, "myname"); - const ev = new MatrixEvent({ - type: "m.room.message", - content: { - body: "Test", - }, - event_id: "$event1:server", - }); - - await room.addLiveEvents([ev]); - await room.createThreadsTimelineSets(); - expect(ev.threadRootId).toBeUndefined(); - expect(mainTimelineLiveEventIds(room)).toEqual(["$event1:server"]); - - // When I redact it - const redaction = new MatrixEvent({ - type: "m.room.redaction", - redacts: ev.getId(), - }); - ev.makeRedacted(redaction, room); - - // Then it remains in the main timeline - expect(ev.threadRootId).toBeUndefined(); - expect(mainTimelineLiveEventIds(room)).toEqual(["$event1:server"]); - }); - - it("should move into the main timeline when redacted", async () => { - // Given an event in a thread - const mockClient = { - supportsThreads: jest.fn().mockReturnValue(true), - decryptEventIfNeeded: jest.fn().mockReturnThis(), - getUserId: jest.fn().mockReturnValue("@user:server"), - } as unknown as MockedObject; - const room = new Room("!roomid:e.xyz", mockClient, "myname"); - const threadRoot = new MatrixEvent({ - type: "m.room.message", - content: { - body: "threadRoot", - }, - event_id: "$threadroot:server", - }); - const ev = new MatrixEvent({ - type: "m.room.message", - content: { - "body": "Test", - "m.relates_to": { - rel_type: THREAD_RELATION_TYPE.name, - event_id: "$threadroot:server", + describe("redaction", () => { + it("should prune clearEvent when being redacted", () => { + const ev = new MatrixEvent({ + type: "m.room.message", + content: { + body: "Test", }, - }, - event_id: "$event1:server", + event_id: "$event1:server", + }); + + expect(ev.getContent().body).toBe("Test"); + expect(ev.getWireContent().body).toBe("Test"); + ev.makeEncrypted("m.room.encrypted", { ciphertext: "xyz" }, "", ""); + expect(ev.getContent().body).toBe("Test"); + expect(ev.getWireContent().body).toBeUndefined(); + expect(ev.getWireContent().ciphertext).toBe("xyz"); + + const mockClient = {} as unknown as MockedObject; + const room = new Room("!roomid:e.xyz", mockClient, "myname"); + const redaction = new MatrixEvent({ + type: "m.room.redaction", + redacts: ev.getId(), + }); + + ev.makeRedacted(redaction, room); + expect(ev.getContent().body).toBeUndefined(); + expect(ev.getWireContent().body).toBeUndefined(); + expect(ev.getWireContent().ciphertext).toBeUndefined(); }); - await room.addLiveEvents([threadRoot, ev]); - await room.createThreadsTimelineSets(); - expect(ev.threadRootId).toEqual("$threadroot:server"); - expect(mainTimelineLiveEventIds(room)).toEqual(["$threadroot:server"]); - expect(threadLiveEventIds(room, 0)).toEqual(["$threadroot:server", "$event1:server"]); + it("should remain in the main timeline when redacted", async () => { + // Given an event in the main timeline + const mockClient = { + supportsThreads: jest.fn().mockReturnValue(true), + decryptEventIfNeeded: jest.fn().mockReturnThis(), + getUserId: jest.fn().mockReturnValue("@user:server"), + } as unknown as MockedObject; + const room = new Room("!roomid:e.xyz", mockClient, "myname"); + const ev = new MatrixEvent({ + type: "m.room.message", + content: { + body: "Test", + }, + event_id: "$event1:server", + }); - // When I redact it - const redaction = new MatrixEvent({ - type: "m.room.redaction", - redacts: ev.getId(), + await room.addLiveEvents([ev]); + await room.createThreadsTimelineSets(); + expect(ev.threadRootId).toBeUndefined(); + expect(mainTimelineLiveEventIds(room)).toEqual(["$event1:server"]); + + // When I redact it + const redaction = new MatrixEvent({ + type: "m.room.redaction", + redacts: ev.getId(), + }); + ev.makeRedacted(redaction, room); + + // Then it remains in the main timeline + expect(ev.threadRootId).toBeUndefined(); + expect(mainTimelineLiveEventIds(room)).toEqual(["$event1:server"]); }); - ev.makeRedacted(redaction, room); - // Then it disappears from the thread and appears in the main timeline - expect(ev.threadRootId).toBeUndefined(); - expect(mainTimelineLiveEventIds(room)).toEqual(["$threadroot:server", "$event1:server"]); - expect(threadLiveEventIds(room, 0)).not.toContain("$event1:server"); + it("should move into the main timeline when redacted", async () => { + // Given an event in a thread + const mockClient = { + supportsThreads: jest.fn().mockReturnValue(true), + decryptEventIfNeeded: jest.fn().mockReturnThis(), + getUserId: jest.fn().mockReturnValue("@user:server"), + } as unknown as MockedObject; + const room = new Room("!roomid:e.xyz", mockClient, "myname"); + const threadRoot = new MatrixEvent({ + type: "m.room.message", + content: { + body: "threadRoot", + }, + event_id: "$threadroot:server", + }); + const ev = new MatrixEvent({ + type: "m.room.message", + content: { + "body": "Test", + "m.relates_to": { + rel_type: THREAD_RELATION_TYPE.name, + event_id: "$threadroot:server", + }, + }, + event_id: "$event1:server", + }); + + await room.addLiveEvents([threadRoot, ev]); + await room.createThreadsTimelineSets(); + expect(ev.threadRootId).toEqual("$threadroot:server"); + expect(mainTimelineLiveEventIds(room)).toEqual(["$threadroot:server"]); + expect(threadLiveEventIds(room, 0)).toEqual(["$threadroot:server", "$event1:server"]); + + // When I redact it + const redaction = new MatrixEvent({ + type: "m.room.redaction", + redacts: ev.getId(), + }); + ev.makeRedacted(redaction, room); + + // Then it disappears from the thread and appears in the main timeline + expect(ev.threadRootId).toBeUndefined(); + expect(mainTimelineLiveEventIds(room)).toEqual(["$threadroot:server", "$event1:server"]); + expect(threadLiveEventIds(room, 0)).not.toContain("$event1:server"); + }); }); describe("applyVisibilityEvent", () => { From 304c2b12bfe5a74e400f0bb4edfeffb08061fe9a Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Tue, 7 Nov 2023 13:44:50 +0000 Subject: [PATCH 3/7] Revert "Revert "Factor out utils in redaction tests"" This reverts commit 2525c82049dc1a958446b66cc85656c1b57a5271. --- spec/unit/models/event.spec.ts | 116 ++++++++++++++++----------------- 1 file changed, 55 insertions(+), 61 deletions(-) diff --git a/spec/unit/models/event.spec.ts b/spec/unit/models/event.spec.ts index c3ac23ce4..696681aff 100644 --- a/spec/unit/models/event.spec.ts +++ b/spec/unit/models/event.spec.ts @@ -72,13 +72,7 @@ describe("MatrixEvent", () => { describe("redaction", () => { it("should prune clearEvent when being redacted", () => { - const ev = new MatrixEvent({ - type: "m.room.message", - content: { - body: "Test", - }, - event_id: "$event1:server", - }); + const ev = createEvent("$event1:server", "Test"); expect(ev.getContent().body).toBe("Test"); expect(ev.getWireContent().body).toBe("Test"); @@ -89,10 +83,7 @@ describe("MatrixEvent", () => { const mockClient = {} as unknown as MockedObject; const room = new Room("!roomid:e.xyz", mockClient, "myname"); - const redaction = new MatrixEvent({ - type: "m.room.redaction", - redacts: ev.getId(), - }); + const redaction = createRedaction(ev.getId()!); ev.makeRedacted(redaction, room); expect(ev.getContent().body).toBeUndefined(); @@ -102,82 +93,85 @@ describe("MatrixEvent", () => { it("should remain in the main timeline when redacted", async () => { // Given an event in the main timeline - const mockClient = { - supportsThreads: jest.fn().mockReturnValue(true), - decryptEventIfNeeded: jest.fn().mockReturnThis(), - getUserId: jest.fn().mockReturnValue("@user:server"), - } as unknown as MockedObject; + const mockClient = createMockClient(); const room = new Room("!roomid:e.xyz", mockClient, "myname"); - const ev = new MatrixEvent({ - type: "m.room.message", - content: { - body: "Test", - }, - event_id: "$event1:server", - }); + const ev = createEvent("$event1:server"); await room.addLiveEvents([ev]); await room.createThreadsTimelineSets(); expect(ev.threadRootId).toBeUndefined(); - expect(mainTimelineLiveEventIds(room)).toEqual(["$event1:server"]); + expect(mainTimelineLiveEventIds(room)).toEqual([ev.getId()]); // When I redact it - const redaction = new MatrixEvent({ - type: "m.room.redaction", - redacts: ev.getId(), - }); + const redaction = createRedaction(ev.getId()!); ev.makeRedacted(redaction, room); // Then it remains in the main timeline expect(ev.threadRootId).toBeUndefined(); - expect(mainTimelineLiveEventIds(room)).toEqual(["$event1:server"]); + expect(mainTimelineLiveEventIds(room)).toEqual([ev.getId()]); }); it("should move into the main timeline when redacted", async () => { // Given an event in a thread - const mockClient = { - supportsThreads: jest.fn().mockReturnValue(true), - decryptEventIfNeeded: jest.fn().mockReturnThis(), - getUserId: jest.fn().mockReturnValue("@user:server"), - } as unknown as MockedObject; + const mockClient = createMockClient(); const room = new Room("!roomid:e.xyz", mockClient, "myname"); - const threadRoot = new MatrixEvent({ - type: "m.room.message", - content: { - body: "threadRoot", - }, - event_id: "$threadroot:server", - }); - const ev = new MatrixEvent({ - type: "m.room.message", - content: { - "body": "Test", - "m.relates_to": { - rel_type: THREAD_RELATION_TYPE.name, - event_id: "$threadroot:server", - }, - }, - event_id: "$event1:server", - }); + const threadRoot = createEvent("$threadroot:server"); + const ev = createThreadedEvent("$event1:server", threadRoot.getId()!); await room.addLiveEvents([threadRoot, ev]); await room.createThreadsTimelineSets(); - expect(ev.threadRootId).toEqual("$threadroot:server"); - expect(mainTimelineLiveEventIds(room)).toEqual(["$threadroot:server"]); - expect(threadLiveEventIds(room, 0)).toEqual(["$threadroot:server", "$event1:server"]); + expect(ev.threadRootId).toEqual(threadRoot.getId()); + expect(mainTimelineLiveEventIds(room)).toEqual([threadRoot.getId()]); + expect(threadLiveEventIds(room, 0)).toEqual([threadRoot.getId(), ev.getId()]); // When I redact it - const redaction = new MatrixEvent({ - type: "m.room.redaction", - redacts: ev.getId(), - }); + const redaction = createRedaction(ev.getId()!); ev.makeRedacted(redaction, room); // Then it disappears from the thread and appears in the main timeline expect(ev.threadRootId).toBeUndefined(); - expect(mainTimelineLiveEventIds(room)).toEqual(["$threadroot:server", "$event1:server"]); - expect(threadLiveEventIds(room, 0)).not.toContain("$event1:server"); + expect(mainTimelineLiveEventIds(room)).toEqual([threadRoot.getId(), ev.getId()]); + expect(threadLiveEventIds(room, 0)).not.toContain(ev.getId()); }); + + function createMockClient(): MatrixClient { + return { + supportsThreads: jest.fn().mockReturnValue(true), + decryptEventIfNeeded: jest.fn().mockReturnThis(), + getUserId: jest.fn().mockReturnValue("@user:server"), + } as unknown as MockedObject; + } + + function createEvent(eventId: string, body?: string): MatrixEvent { + return new MatrixEvent({ + type: "m.room.message", + content: { + body: body ?? eventId, + }, + event_id: eventId, + }); + } + + function createThreadedEvent(eventId: string, threadRootId: string): MatrixEvent { + return new MatrixEvent({ + type: "m.room.message", + content: { + "body": eventId, + "m.relates_to": { + rel_type: THREAD_RELATION_TYPE.name, + event_id: threadRootId, + }, + }, + event_id: eventId, + }); + } + + function createRedaction(redactedEventid: string): MatrixEvent { + return new MatrixEvent({ + type: "m.room.redaction", + redacts: redactedEventid, + }); + } }); describe("applyVisibilityEvent", () => { From 582ea68c31d9ca991386656c688baa865b0c448d Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Tue, 7 Nov 2023 13:44:57 +0000 Subject: [PATCH 4/7] Revert "Revert "Factor out the code for moving an event to the main timeline"" This reverts commit 272be48a54a45df89603a27fbbe6e26da88b95ba. --- src/models/event.ts | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/models/event.ts b/src/models/event.ts index d1e56f140..3da9d2de0 100644 --- a/src/models/event.ts +++ b/src/models/event.ts @@ -1210,22 +1210,24 @@ export class MatrixEvent extends TypedEventEmitter Date: Tue, 7 Nov 2023 13:45:02 +0000 Subject: [PATCH 5/7] Revert "Revert "Move all related messages into main timeline on redaction"" This reverts commit 257b40bceb304001c03aaec7b140a1fd05c96d9e. --- spec/unit/models/event.spec.ts | 133 +++++++++++++++++++++++++++++++++ src/models/event.ts | 17 ++++- 2 files changed, 149 insertions(+), 1 deletion(-) diff --git a/spec/unit/models/event.spec.ts b/spec/unit/models/event.spec.ts index 696681aff..9e7720a14 100644 --- a/spec/unit/models/event.spec.ts +++ b/spec/unit/models/event.spec.ts @@ -134,6 +134,94 @@ describe("MatrixEvent", () => { expect(threadLiveEventIds(room, 0)).not.toContain(ev.getId()); }); + it("should move reactions to a redacted event into the main timeline", async () => { + // Given an event in a thread with a reaction + const mockClient = createMockClient(); + const room = new Room("!roomid:e.xyz", mockClient, "myname"); + const threadRoot = createEvent("$threadroot:server"); + const ev = createThreadedEvent("$event1:server", threadRoot.getId()!); + const reaction = createReactionEvent("$reaction:server", ev.getId()!); + + await room.addLiveEvents([threadRoot, ev, reaction]); + await room.createThreadsTimelineSets(); + expect(reaction.threadRootId).toEqual(threadRoot.getId()); + expect(mainTimelineLiveEventIds(room)).toEqual([threadRoot.getId()]); + expect(threadLiveEventIds(room, 0)).toEqual([threadRoot.getId(), ev.getId(), reaction.getId()]); + + // When I redact the event + const redaction = createRedaction(ev.getId()!); + ev.makeRedacted(redaction, room); + + // Then the reaction moves into the main timeline + expect(reaction.threadRootId).toBeUndefined(); + expect(mainTimelineLiveEventIds(room)).toEqual([threadRoot.getId(), ev.getId(), reaction.getId()]); + expect(threadLiveEventIds(room, 0)).not.toContain(reaction.getId()); + }); + + it("should move edits of a redacted event into the main timeline", async () => { + // Given an event in a thread with a reaction + const mockClient = createMockClient(); + const room = new Room("!roomid:e.xyz", mockClient, "myname"); + const threadRoot = createEvent("$threadroot:server"); + const ev = createThreadedEvent("$event1:server", threadRoot.getId()!); + const edit = createEditEvent("$edit:server", ev.getId()!); + + await room.addLiveEvents([threadRoot, ev, edit]); + await room.createThreadsTimelineSets(); + expect(edit.threadRootId).toEqual(threadRoot.getId()); + expect(mainTimelineLiveEventIds(room)).toEqual([threadRoot.getId()]); + expect(threadLiveEventIds(room, 0)).toEqual([threadRoot.getId(), ev.getId(), edit.getId()]); + + // When I redact the event + const redaction = createRedaction(ev.getId()!); + ev.makeRedacted(redaction, room); + + // Then the edit moves into the main timeline + expect(edit.threadRootId).toBeUndefined(); + expect(mainTimelineLiveEventIds(room)).toEqual([threadRoot.getId(), ev.getId(), edit.getId()]); + expect(threadLiveEventIds(room, 0)).not.toContain(edit.getId()); + }); + + it("should move reactions to replies to replies a redacted event into the main timeline", async () => { + // Given an event in a thread with a reaction + const mockClient = createMockClient(); + const room = new Room("!roomid:e.xyz", mockClient, "myname"); + const threadRoot = createEvent("$threadroot:server"); + const ev = createThreadedEvent("$event1:server", threadRoot.getId()!); + const reply1 = createReplyEvent("$reply1:server", ev.getId()!); + const reply2 = createReplyEvent("$reply2:server", reply1.getId()!); + const reaction = createReactionEvent("$reaction:server", reply2.getId()!); + + await room.addLiveEvents([threadRoot, ev, reply1, reply2, reaction]); + await room.createThreadsTimelineSets(); + expect(reaction.threadRootId).toEqual(threadRoot.getId()); + expect(mainTimelineLiveEventIds(room)).toEqual([threadRoot.getId()]); + expect(threadLiveEventIds(room, 0)).toEqual([ + threadRoot.getId(), + ev.getId(), + reply1.getId(), + reply2.getId(), + reaction.getId(), + ]); + + // When I redact the event + const redaction = createRedaction(ev.getId()!); + ev.makeRedacted(redaction, room); + + // Then the replies move to the main thread and the reaction disappears + expect(reaction.threadRootId).toBeUndefined(); + expect(mainTimelineLiveEventIds(room)).toEqual([ + threadRoot.getId(), + ev.getId(), + reply1.getId(), + reply2.getId(), + reaction.getId(), + ]); + expect(threadLiveEventIds(room, 0)).not.toContain(reply1.getId()); + expect(threadLiveEventIds(room, 0)).not.toContain(reply2.getId()); + expect(threadLiveEventIds(room, 0)).not.toContain(reaction.getId()); + }); + function createMockClient(): MatrixClient { return { supportsThreads: jest.fn().mockReturnValue(true), @@ -166,6 +254,51 @@ describe("MatrixEvent", () => { }); } + function createEditEvent(eventId: string, repliedToId: string): MatrixEvent { + return new MatrixEvent({ + type: "m.room.message", + content: { + "body": "Edited", + "m.new_content": { + body: "Edited", + }, + "m.relates_to": { + event_id: repliedToId, + rel_type: "m.replace", + }, + }, + event_id: eventId, + }); + } + + function createReplyEvent(eventId: string, repliedToId: string): MatrixEvent { + return new MatrixEvent({ + type: "m.room.message", + content: { + "m.relates_to": { + event_id: repliedToId, + key: "x", + rel_type: "m.in_reply_to", + }, + }, + event_id: eventId, + }); + } + + function createReactionEvent(eventId: string, reactedToId: string): MatrixEvent { + return new MatrixEvent({ + type: "m.reaction", + content: { + "m.relates_to": { + event_id: reactedToId, + key: "x", + rel_type: "m.annotation", + }, + }, + event_id: eventId, + }); + } + function createRedaction(redactedEventid: string): MatrixEvent { return new MatrixEvent({ type: "m.room.redaction", diff --git a/src/models/event.ts b/src/models/event.ts index 3da9d2de0..c76da4a45 100644 --- a/src/models/event.ts +++ b/src/models/event.ts @@ -1210,12 +1210,27 @@ export class MatrixEvent extends TypedEventEmitter Date: Tue, 7 Nov 2023 13:45:08 +0000 Subject: [PATCH 6/7] Revert "Revert "Don't remove thread info from a thread root when it is redacted"" This reverts commit 4dbff2a837cbc5ba37424c65ccdc833a1843deb2. --- spec/unit/models/event.spec.ts | 23 +++++++++++++++++++++++ src/models/event.ts | 5 +++-- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/spec/unit/models/event.spec.ts b/spec/unit/models/event.spec.ts index 9e7720a14..5ff9d745d 100644 --- a/spec/unit/models/event.spec.ts +++ b/spec/unit/models/event.spec.ts @@ -111,6 +111,29 @@ describe("MatrixEvent", () => { expect(mainTimelineLiveEventIds(room)).toEqual([ev.getId()]); }); + it("should keep thread roots in both timelines when redacted", async () => { + // Given a thread exists + const mockClient = createMockClient(); + const room = new Room("!roomid:e.xyz", mockClient, "myname"); + const threadRoot = createEvent("$threadroot:server"); + const ev = createThreadedEvent("$event1:server", threadRoot.getId()!); + + await room.addLiveEvents([threadRoot, ev]); + await room.createThreadsTimelineSets(); + expect(threadRoot.threadRootId).toEqual(threadRoot.getId()); + expect(mainTimelineLiveEventIds(room)).toEqual([threadRoot.getId()]); + expect(threadLiveEventIds(room, 0)).toEqual([threadRoot.getId(), ev.getId()]); + + // When I redact the thread root + const redaction = createRedaction(ev.getId()!); + threadRoot.makeRedacted(redaction, room); + + // Then it remains in the main timeline and the thread + expect(threadRoot.threadRootId).toEqual(threadRoot.getId()); + expect(mainTimelineLiveEventIds(room)).toEqual([threadRoot.getId()]); + expect(threadLiveEventIds(room, 0)).toEqual([threadRoot.getId(), ev.getId()]); + }); + it("should move into the main timeline when redacted", async () => { // Given an event in a thread const mockClient = createMockClient(); diff --git a/src/models/event.ts b/src/models/event.ts index c76da4a45..d37c98b4a 100644 --- a/src/models/event.ts +++ b/src/models/event.ts @@ -1208,8 +1208,9 @@ export class MatrixEvent extends TypedEventEmitter Date: Tue, 7 Nov 2023 13:45:14 +0000 Subject: [PATCH 7/7] Revert "Revert "Move the redaction event to main at the same time we move redacted"" This reverts commit 11755f5a0a1486fa6ad3cb9e4b8959ddc7e1d276. --- src/models/event.ts | 1 + src/models/room.ts | 14 +++++++++++--- src/models/thread.ts | 4 ++-- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/models/event.ts b/src/models/event.ts index d37c98b4a..8768c0cd9 100644 --- a/src/models/event.ts +++ b/src/models/event.ts @@ -1212,6 +1212,7 @@ export class MatrixEvent extends TypedEventEmitter void; + [RoomEvent.Redaction]: (event: MatrixEvent, room: Room, threadId?: string) => void; /** * Fires when an event that was previously redacted isn't anymore. * This happens when the redaction couldn't be sent and @@ -2113,6 +2114,12 @@ export class Room extends ReadReceipt { * Relations (other than m.thread), redactions, replies to a thread root live only in the main timeline * Relations, redactions, replies where the parent cannot be found live in no timelines but should be aggregated regardless. * Otherwise, the event lives in the main timeline only. + * + * Note: when a redaction is applied, the redacted event, events relating + * to it, and the redaction event itself, will all move to the main thread. + * This method classifies them as inside the thread of the redacted event. + * They are moved later as part of makeRedacted. + * This will change if MSC3389 is merged. */ public eventShouldLiveIn( event: MatrixEvent, @@ -2329,6 +2336,7 @@ export class Room extends ReadReceipt { // if we know about this event, redact its contents now. const redactedEvent = redactId ? this.findEventById(redactId) : undefined; if (redactedEvent) { + const threadRootId = redactedEvent.threadRootId; redactedEvent.makeRedacted(event, this); // If this is in the current state, replace it with the redacted version @@ -2342,7 +2350,7 @@ export class Room extends ReadReceipt { } } - this.emit(RoomEvent.Redaction, event, this); + this.emit(RoomEvent.Redaction, event, this, threadRootId); // TODO: we stash user displaynames (among other things) in // RoomMember objects which are then attached to other events @@ -2495,7 +2503,7 @@ export class Room extends ReadReceipt { } if (redactedEvent) { redactedEvent.markLocallyRedacted(event); - this.emit(RoomEvent.Redaction, event, this); + this.emit(RoomEvent.Redaction, event, this, redactedEvent.threadRootId); } } } else { diff --git a/src/models/thread.ts b/src/models/thread.ts index 1da3fca55..6b8069c9a 100644 --- a/src/models/thread.ts +++ b/src/models/thread.ts @@ -228,8 +228,8 @@ export class Thread extends ReadReceipt => { - if (event.threadRootId !== this.id) return; // ignore redactions for other timelines + private onRedaction = async (event: MatrixEvent, room: Room, threadRootId?: string): Promise => { + if (threadRootId !== this.id) return; // ignore redactions for other timelines if (this.replyCount <= 0) { for (const threadEvent of this.timeline) { this.clearEventMetadata(threadEvent);