Move events related to a redacted event into the main timeline (#3800)
* Move redaction event tests into their own describe block * Factor out utils in redaction tests * Factor out the code for moving an event to the main timeline * Move all related messages into main timeline on redaction
This commit is contained in:
+218
-89
@@ -70,112 +70,241 @@ 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",
|
||||
describe("redaction", () => {
|
||||
it("should prune clearEvent when being redacted", () => {
|
||||
const ev = createEvent("$event1:server", "Test");
|
||||
|
||||
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<MatrixClient>;
|
||||
const room = new Room("!roomid:e.xyz", mockClient, "myname");
|
||||
const redaction = createRedaction(ev.getId()!);
|
||||
|
||||
ev.makeRedacted(redaction, room);
|
||||
expect(ev.getContent().body).toBeUndefined();
|
||||
expect(ev.getWireContent().body).toBeUndefined();
|
||||
expect(ev.getWireContent().ciphertext).toBeUndefined();
|
||||
});
|
||||
|
||||
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");
|
||||
it("should remain in the main timeline when redacted", async () => {
|
||||
// Given an event in the main timeline
|
||||
const mockClient = createMockClient();
|
||||
const room = new Room("!roomid:e.xyz", mockClient, "myname");
|
||||
const ev = createEvent("$event1:server");
|
||||
|
||||
const mockClient = {} as unknown as MockedObject<MatrixClient>;
|
||||
const room = new Room("!roomid:e.xyz", mockClient, "myname");
|
||||
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([ev.getId()]);
|
||||
|
||||
// When I redact it
|
||||
const redaction = createRedaction(ev.getId()!);
|
||||
ev.makeRedacted(redaction, room);
|
||||
|
||||
// Then it remains in the main timeline
|
||||
expect(ev.threadRootId).toBeUndefined();
|
||||
expect(mainTimelineLiveEventIds(room)).toEqual([ev.getId()]);
|
||||
});
|
||||
|
||||
ev.makeRedacted(redaction, room);
|
||||
expect(ev.getContent().body).toBeUndefined();
|
||||
expect(ev.getWireContent().body).toBeUndefined();
|
||||
expect(ev.getWireContent().ciphertext).toBeUndefined();
|
||||
});
|
||||
it("should move into the main timeline when redacted", async () => {
|
||||
// Given an event in a thread
|
||||
const mockClient = createMockClient();
|
||||
const room = new Room("!roomid:e.xyz", mockClient, "myname");
|
||||
const threadRoot = createEvent("$threadroot:server");
|
||||
const ev = createThreadedEvent("$event1:server", threadRoot.getId()!);
|
||||
|
||||
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<MatrixClient>;
|
||||
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([threadRoot, ev]);
|
||||
await room.createThreadsTimelineSets();
|
||||
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 = 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.getId(), ev.getId()]);
|
||||
expect(threadLiveEventIds(room, 0)).not.toContain(ev.getId());
|
||||
});
|
||||
|
||||
await room.addLiveEvents([ev]);
|
||||
await room.createThreadsTimelineSets();
|
||||
expect(ev.threadRootId).toBeUndefined();
|
||||
expect(mainTimelineLiveEventIds(room)).toEqual(["$event1:server"]);
|
||||
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()!);
|
||||
|
||||
// When I redact it
|
||||
const redaction = new MatrixEvent({
|
||||
type: "m.room.redaction",
|
||||
redacts: 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());
|
||||
});
|
||||
ev.makeRedacted(redaction, room);
|
||||
|
||||
// Then it remains in the main timeline
|
||||
expect(ev.threadRootId).toBeUndefined();
|
||||
expect(mainTimelineLiveEventIds(room)).toEqual(["$event1:server"]);
|
||||
});
|
||||
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()!);
|
||||
|
||||
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<MatrixClient>;
|
||||
const room = new Room("!roomid:e.xyz", mockClient, "myname");
|
||||
const threadRoot = new MatrixEvent({
|
||||
type: "m.room.message",
|
||||
content: {
|
||||
body: "threadRoot",
|
||||
},
|
||||
event_id: "$threadroot:server",
|
||||
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());
|
||||
});
|
||||
const ev = new MatrixEvent({
|
||||
type: "m.room.message",
|
||||
content: {
|
||||
"body": "Test",
|
||||
"m.relates_to": {
|
||||
rel_type: THREAD_RELATION_TYPE.name,
|
||||
event_id: "$threadroot:server",
|
||||
|
||||
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),
|
||||
decryptEventIfNeeded: jest.fn().mockReturnThis(),
|
||||
getUserId: jest.fn().mockReturnValue("@user:server"),
|
||||
} as unknown as MockedObject<MatrixClient>;
|
||||
}
|
||||
|
||||
function createEvent(eventId: string, body?: string): MatrixEvent {
|
||||
return new MatrixEvent({
|
||||
type: "m.room.message",
|
||||
content: {
|
||||
body: body ?? eventId,
|
||||
},
|
||||
},
|
||||
event_id: "$event1:server",
|
||||
});
|
||||
event_id: eventId,
|
||||
});
|
||||
}
|
||||
|
||||
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"]);
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
// When I redact it
|
||||
const redaction = new MatrixEvent({
|
||||
type: "m.room.redaction",
|
||||
redacts: ev.getId(),
|
||||
});
|
||||
ev.makeRedacted(redaction, room);
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
// 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");
|
||||
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",
|
||||
redacts: redactedEventid,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe("applyVisibilityEvent", () => {
|
||||
|
||||
+28
-11
@@ -1195,22 +1195,39 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
|
||||
|
||||
// If the redacted event was in a thread
|
||||
if (room && this.threadRootId && this.threadRootId !== this.getId()) {
|
||||
// Remove it from its thread
|
||||
this.thread?.timelineSet.removeEvent(this.getId()!);
|
||||
this.setThread(undefined);
|
||||
|
||||
// And insert it into the main timeline
|
||||
const timeline = room.getLiveTimeline();
|
||||
// We use insertEventIntoTimeline to insert it in timestamp order,
|
||||
// because we don't know where it should go (until we have MSC4033).
|
||||
timeline
|
||||
.getTimelineSet()
|
||||
.insertEventIntoTimeline(this, timeline, timeline.getState(EventTimeline.FORWARDS)!);
|
||||
this.moveAllRelatedToMainTimeline(room);
|
||||
}
|
||||
|
||||
this.invalidateExtensibleEvent();
|
||||
}
|
||||
|
||||
private moveAllRelatedToMainTimeline(room: Room): void {
|
||||
const thread = this.thread;
|
||||
this.moveToMainTimeline(room);
|
||||
|
||||
// If we dont have access to the thread, we can only move this
|
||||
// event, not things related to it.
|
||||
if (thread) {
|
||||
for (const event of thread.events) {
|
||||
if (event.getRelation()?.event_id === this.getId()) {
|
||||
event.moveAllRelatedToMainTimeline(room);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private moveToMainTimeline(room: Room): void {
|
||||
// Remove it from its thread
|
||||
this.thread?.timelineSet.removeEvent(this.getId()!);
|
||||
this.setThread(undefined);
|
||||
|
||||
// And insert it into the main timeline
|
||||
const timeline = room.getLiveTimeline();
|
||||
// We use insertEventIntoTimeline to insert it in timestamp order,
|
||||
// because we don't know where it should go (until we have MSC4033).
|
||||
timeline.getTimelineSet().insertEventIntoTimeline(this, timeline, timeline.getState(EventTimeline.FORWARDS)!);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this event has been redacted
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user