From b94d137398d600ad208450732b6f12aed177221b Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Fri, 13 Oct 2023 16:12:29 +0100 Subject: [PATCH] Move redacted messages out of any thread, into main timeline. For consistency with the spec at room version 11. See https://github.com/matrix-org/matrix-spec-proposals/pull/3389 for a proposal to make this unnecessary. --- 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 efb2404f6a6..bcc2b42a254 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 435d2e33ddc..6c60d08f908 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 de4e24b1ef2..b1db43f5dcd 100644 --- a/spec/unit/room.spec.ts +++ b/spec/unit/room.spec.ts @@ -3564,7 +3564,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 dcd11b53e1c..e0194006773 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"; @@ -1132,13 +1134,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()) {