/* * Copyright 2026 Element Creations Ltd. * * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial * Please see LICENSE files in the repository root for full details. */ import { EventType, MsgType, type MatrixClient, type MatrixEvent, type Room } from "matrix-js-sdk/src/matrix"; import { formatFullDate } from "../../../../../src/DateUtils"; import { _t } from "../../../../../src/languageHandler"; import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg"; import SettingsStore from "../../../../../src/settings/SettingsStore"; import { RedactedBodyViewModel } from "../../../../../src/viewmodels/message-body/RedactedBodyViewModel"; import { mkEvent, mkRoom, stubClient } from "../../../../test-utils"; describe("RedactedBodyViewModel", () => { let client: MatrixClient; let room: Room; let showTwelveHourSettingWatcher: ((...args: any[]) => void) | undefined; const makeRedactedBecauseEvent = ({ sender, originServerTs }: { sender: string; originServerTs: number }) => ({ content: {}, event_id: "$redaction:example.com", origin_server_ts: originServerTs, redacts: "$message:example.com", room_id: room.roomId, sender, type: EventType.RoomRedaction, unsigned: {}, }); const makeRedactedEvent = ({ sender = "@alice:example.com", redactedBecauseSender = sender, originServerTs = Date.UTC(2022, 10, 17, 15, 58, 32), }: { sender?: string; redactedBecauseSender?: string; originServerTs?: number; } = {}): MatrixEvent => mkEvent({ event: true, type: EventType.RoomMessage, user: sender, room: room.roomId, content: { msgtype: MsgType.Text, body: "Message", }, unsigned: { redacted_because: makeRedactedBecauseEvent({ sender: redactedBecauseSender, originServerTs, }), }, }); beforeEach(() => { client = stubClient(); room = mkRoom(client, "!room:example.com"); jest.spyOn(MatrixClientPeg, "get").mockReturnValue(client); jest.spyOn(client, "getRoom").mockReturnValue(room); jest.spyOn(SettingsStore, "getValue").mockImplementation( (settingName) => settingName === "showTwelveHourTimestamps", ); jest.spyOn(SettingsStore, "watchSetting").mockImplementation((_settingName, _roomId, callbackFn) => { showTwelveHourSettingWatcher = callbackFn as (...args: any[]) => void; return "mock-show-twelve-hour-watcher"; }); jest.spyOn(SettingsStore, "unwatchSetting").mockImplementation(jest.fn()); }); it("builds self-redaction text and tooltip from the event", () => { const event = makeRedactedEvent(); const vm = new RedactedBodyViewModel({ mxEvent: event }); expect(vm.getSnapshot()).toEqual({ text: "Message deleted", tooltip: _t("timeline|redacted|tooltip", { date: formatFullDate(new Date(Date.UTC(2022, 10, 17, 15, 58, 32)), true), }), }); }); it("uses the redacting member name when another user removed the message", () => { jest.spyOn(room, "getMember").mockReturnValue({ name: "Alice" } as any); const event = makeRedactedEvent({ redactedBecauseSender: "@alice-redactor:example.com", }); const vm = new RedactedBodyViewModel({ mxEvent: event }); expect(vm.getSnapshot().text).toBe("Message deleted by Alice"); }); it("updates the tooltip when showTwelveHourTimestamps changes", () => { jest.spyOn(SettingsStore, "getValue").mockReturnValue(false); const event = makeRedactedEvent(); const vm = new RedactedBodyViewModel({ mxEvent: event }); const listener = jest.fn(); vm.subscribe(listener); showTwelveHourSettingWatcher?.("showTwelveHourTimestamps", null, undefined, false, false); expect(listener).not.toHaveBeenCalled(); showTwelveHourSettingWatcher?.("showTwelveHourTimestamps", null, undefined, true, true); expect(listener).toHaveBeenCalledTimes(1); expect(vm.getSnapshot().tooltip).toBe( _t("timeline|redacted|tooltip", { date: formatFullDate(new Date(Date.UTC(2022, 10, 17, 15, 58, 32)), true), }), ); }); it("setEvent is a no-op for the same event and updates for a different event", () => { const originalEvent = makeRedactedEvent(); const updatedEvent = makeRedactedEvent({ redactedBecauseSender: "@moderator:example.com", originServerTs: Date.UTC(2023, 0, 1, 12, 0, 0), }); const vm = new RedactedBodyViewModel({ mxEvent: originalEvent }); const listener = jest.fn(); jest.spyOn(room, "getMember").mockReturnValue({ name: "Moderator" } as any); vm.subscribe(listener); vm.setEvent(originalEvent); expect(listener).not.toHaveBeenCalled(); vm.setEvent(updatedEvent); expect(listener).toHaveBeenCalledTimes(1); expect(vm.getSnapshot().text).toBe("Message deleted by Moderator"); }); it("unwatches the timestamp setting when disposed", () => { const vm = new RedactedBodyViewModel({ mxEvent: makeRedactedEvent() }); vm.dispose(); expect(SettingsStore.unwatchSetting).toHaveBeenCalledWith("mock-show-twelve-hour-watcher"); }); });