1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-07-31 15:24:23 +03:00

Merge pull request #3798 from matrix-org/andybalaam/move-redacted-message-to-main

Move redacted messages out of any thread, into main timeline.
This commit is contained in:
Andy Balaam
2023-10-18 08:00:29 +01:00
committed by GitHub
5 changed files with 141 additions and 7 deletions

View File

@ -14,10 +14,19 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { MockedObject } from "jest-mock";
import { MatrixEvent, MatrixEventEvent } from "../../../src/models/event"; import { MatrixEvent, MatrixEventEvent } from "../../../src/models/event";
import { emitPromise } from "../../test-utils/test-utils"; import { emitPromise } from "../../test-utils/test-utils";
import { Crypto, IEventDecryptionResult } from "../../../src/crypto"; 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", () => { describe("MatrixEvent", () => {
it("should create copies of itself", () => { it("should create copies of itself", () => {
@ -77,17 +86,98 @@ describe("MatrixEvent", () => {
expect(ev.getWireContent().body).toBeUndefined(); expect(ev.getWireContent().body).toBeUndefined();
expect(ev.getWireContent().ciphertext).toBe("xyz"); expect(ev.getWireContent().ciphertext).toBe("xyz");
const mockClient = {} as unknown as MockedObject<MatrixClient>;
const room = new Room("!roomid:e.xyz", mockClient, "myname");
const redaction = new MatrixEvent({ const redaction = new MatrixEvent({
type: "m.room.redaction", type: "m.room.redaction",
redacts: ev.getId(), redacts: ev.getId(),
}); });
ev.makeRedacted(redaction); ev.makeRedacted(redaction, room);
expect(ev.getContent().body).toBeUndefined(); expect(ev.getContent().body).toBeUndefined();
expect(ev.getWireContent().body).toBeUndefined(); expect(ev.getWireContent().body).toBeUndefined();
expect(ev.getWireContent().ciphertext).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<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([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<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",
});
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", () => { describe("applyVisibilityEvent", () => {
it("should emit VisibilityChange if a change was made", async () => { it("should emit VisibilityChange if a change was made", async () => {
const ev = new MatrixEvent({ const ev = new MatrixEvent({
@ -330,3 +420,19 @@ describe("MatrixEvent", () => {
expect(stateEvent.threadRootId).toBeUndefined(); expect(stateEvent.threadRootId).toBeUndefined();
}); });
}); });
function mainTimelineLiveEventIds(room: Room): Array<string> {
return room
.getLiveTimeline()
.getEvents()
.map((e) => e.getId()!);
}
function threadLiveEventIds(room: Room, threadIndex: number): Array<string> {
return room
.getThreads()
[threadIndex].getUnfilteredTimelineSet()
.getLiveTimeline()
.getEvents()
.map((e) => e.getId()!);
}

View File

@ -27,6 +27,7 @@ import { M_BEACON } from "../../src/@types/beacon";
import { MatrixClient } from "../../src/client"; import { MatrixClient } from "../../src/client";
import { DecryptionError } from "../../src/crypto/algorithms"; import { DecryptionError } from "../../src/crypto/algorithms";
import { defer } from "../../src/utils"; import { defer } from "../../src/utils";
import { Room } from "../../src/models/room";
describe("RoomState", function () { describe("RoomState", function () {
const roomId = "!foo:bar"; const roomId = "!foo:bar";
@ -362,9 +363,11 @@ describe("RoomState", function () {
}); });
it("does not add redacted beacon info events to state", () => { it("does not add redacted beacon info events to state", () => {
const mockClient = {} as unknown as MockedObject<MatrixClient>;
const redactedBeaconEvent = makeBeaconInfoEvent(userA, roomId); const redactedBeaconEvent = makeBeaconInfoEvent(userA, roomId);
const redactionEvent = new MatrixEvent({ type: "m.room.redaction" }); 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"); const emitSpy = jest.spyOn(state, "emit");
state.setStateEvents([redactedBeaconEvent]); state.setStateEvents([redactedBeaconEvent]);
@ -394,11 +397,13 @@ describe("RoomState", function () {
}); });
it("destroys and removes redacted beacon events", () => { it("destroys and removes redacted beacon events", () => {
const mockClient = {} as unknown as MockedObject<MatrixClient>;
const beaconId = "$beacon1"; const beaconId = "$beacon1";
const beaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, beaconId); const beaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, beaconId);
const redactedBeaconEvent = 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() }); 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]); state.setStateEvents([beaconEvent]);
const beaconInstance = state.beacons.get(getBeaconInfoIdentifier(beaconEvent)); const beaconInstance = state.beacons.get(getBeaconInfoIdentifier(beaconEvent));

View File

@ -3564,7 +3564,7 @@ describe("Room", function () {
expect(room.polls.get(pollStartEvent.getId()!)).toBeTruthy(); expect(room.polls.get(pollStartEvent.getId()!)).toBeTruthy();
const redactedEvent = new MatrixEvent({ type: "m.room.redaction" }); const redactedEvent = new MatrixEvent({ type: "m.room.redaction" });
pollStartEvent.makeRedacted(redactedEvent); pollStartEvent.makeRedacted(redactedEvent, room);
await flushPromises(); await flushPromises();

View File

@ -45,6 +45,8 @@ import { DecryptionError } from "../crypto/algorithms";
import { CryptoBackend } from "../common-crypto/CryptoBackend"; import { CryptoBackend } from "../common-crypto/CryptoBackend";
import { WITHHELD_MESSAGES } from "../crypto/OlmDevice"; import { WITHHELD_MESSAGES } from "../crypto/OlmDevice";
import { IAnnotatedPushRule } from "../@types/PushRules"; import { IAnnotatedPushRule } from "../@types/PushRules";
import { Room } from "./room";
import { EventTimeline } from "./event-timeline";
export { EventStatus } from "./event-status"; export { EventStatus } from "./event-status";
@ -1135,13 +1137,19 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
return this.visibility; return this.visibility;
} }
/**
* @deprecated In favor of the overload that includes a Room argument
*/
public makeRedacted(redactionEvent: MatrixEvent): void;
/** /**
* Update the content of an event in the same way it would be by the server * Update the content of an event in the same way it would be by the server
* if it were redacted before it was sent to us * if it were redacted before it was sent to us
* *
* @param redactionEvent - event causing the redaction * @param redactionEvent - event causing the redaction
* @param room - the room in which the event exists
*/ */
public makeRedacted(redactionEvent: MatrixEvent): void { public makeRedacted(redactionEvent: MatrixEvent, room: Room): void;
public makeRedacted(redactionEvent: MatrixEvent, room?: Room): void {
// quick sanity-check // quick sanity-check
if (!redactionEvent.event) { if (!redactionEvent.event) {
throw new Error("invalid redactionEvent in makeRedacted"); throw new Error("invalid redactionEvent in makeRedacted");
@ -1185,6 +1193,21 @@ 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.invalidateExtensibleEvent(); this.invalidateExtensibleEvent();
} }

View File

@ -2329,7 +2329,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
// if we know about this event, redact its contents now. // if we know about this event, redact its contents now.
const redactedEvent = redactId ? this.findEventById(redactId) : undefined; const redactedEvent = redactId ? this.findEventById(redactId) : undefined;
if (redactedEvent) { if (redactedEvent) {
redactedEvent.makeRedacted(event); redactedEvent.makeRedacted(event, this);
// If this is in the current state, replace it with the redacted version // If this is in the current state, replace it with the redacted version
if (redactedEvent.isState()) { if (redactedEvent.isState()) {