diff --git a/apps/web/src/components/views/rooms/EventPreview.tsx b/apps/web/src/components/views/rooms/EventPreview.tsx index bf44211d29..144f1d5aa0 100644 --- a/apps/web/src/components/views/rooms/EventPreview.tsx +++ b/apps/web/src/components/views/rooms/EventPreview.tsx @@ -11,7 +11,7 @@ import { type IContent, M_POLL_START, type MatrixEvent, MatrixEventEvent, MsgTyp import classNames from "classnames"; import { _t } from "../../../languageHandler"; -import { MessagePreviewStore } from "../../../stores/room-list/MessagePreviewStore"; +import { MessagePreviewStore } from "../../../stores/message-preview"; import { useAsyncMemo } from "../../../hooks/useAsyncMemo"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import { useTypedEventEmitter } from "../../../hooks/useEventEmitter.ts"; diff --git a/apps/web/src/components/views/rooms/RoomTile.tsx b/apps/web/src/components/views/rooms/RoomTile.tsx index 6f771206cb..74bfc63221 100644 --- a/apps/web/src/components/views/rooms/RoomTile.tsx +++ b/apps/web/src/components/views/rooms/RoomTile.tsx @@ -21,7 +21,7 @@ import { Action } from "../../../dispatcher/actions"; import { _t } from "../../../languageHandler"; import { ChevronFace, ContextMenuTooltipButton, type MenuProps } from "../../structures/ContextMenu"; import { DefaultTagID, type TagID } from "../../../stores/room-list-v3/skip-list/tag"; -import { type MessagePreview, MessagePreviewStore } from "../../../stores/room-list/MessagePreviewStore"; +import { type MessagePreview, MessagePreviewStore } from "../../../stores/message-preview"; import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar"; import { RoomNotifState } from "../../../RoomNotifs"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; diff --git a/apps/web/src/components/views/rooms/RoomTileSubtitle.tsx b/apps/web/src/components/views/rooms/RoomTileSubtitle.tsx index 715a09c9c4..6e11c91e0e 100644 --- a/apps/web/src/components/views/rooms/RoomTileSubtitle.tsx +++ b/apps/web/src/components/views/rooms/RoomTileSubtitle.tsx @@ -10,7 +10,7 @@ import React from "react"; import classNames from "classnames"; import { ThreadsIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; -import { type MessagePreview } from "../../../stores/room-list/MessagePreviewStore"; +import { type MessagePreview } from "../../../stores/message-preview"; import { type Call } from "../../../models/Call"; import { RoomTileCallSummary } from "./RoomTileCallSummary"; diff --git a/apps/web/src/stores/room-list/MessagePreviewStore.ts b/apps/web/src/stores/message-preview/MessagePreviewStore.ts similarity index 99% rename from apps/web/src/stores/room-list/MessagePreviewStore.ts rename to apps/web/src/stores/message-preview/MessagePreviewStore.ts index 70db1f6cbd..a0fced0193 100644 --- a/apps/web/src/stores/room-list/MessagePreviewStore.ts +++ b/apps/web/src/stores/message-preview/MessagePreviewStore.ts @@ -29,7 +29,7 @@ import { LegacyCallHangupEvent } from "./previews/LegacyCallHangupEvent"; import { StickerEventPreview } from "./previews/StickerEventPreview"; import { ReactionEventPreview } from "./previews/ReactionEventPreview"; import { UPDATE_EVENT } from "../AsyncStore"; -import { type IPreview } from "./previews/IPreview"; +import { type Preview } from "./previews"; import shouldHideEvent from "../../shouldHideEvent"; import SettingsStore from "../../settings/SettingsStore"; @@ -41,7 +41,7 @@ const PREVIEWS: Record< string, { isState: boolean; - previewer: IPreview; + previewer: Preview; } > = { "m.room.message": { diff --git a/apps/web/src/stores/message-preview/index.ts b/apps/web/src/stores/message-preview/index.ts new file mode 100644 index 0000000000..529f72888d --- /dev/null +++ b/apps/web/src/stores/message-preview/index.ts @@ -0,0 +1,8 @@ +/* + * 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. + */ + +export { type MessagePreview, MessagePreviewStore } from "./MessagePreviewStore"; diff --git a/apps/web/src/stores/room-list/previews/LegacyCallAnswerEventPreview.ts b/apps/web/src/stores/message-preview/previews/LegacyCallAnswerEventPreview.ts similarity index 89% rename from apps/web/src/stores/room-list/previews/LegacyCallAnswerEventPreview.ts rename to apps/web/src/stores/message-preview/previews/LegacyCallAnswerEventPreview.ts index 3c2f28b20c..aec8f71a20 100644 --- a/apps/web/src/stores/room-list/previews/LegacyCallAnswerEventPreview.ts +++ b/apps/web/src/stores/message-preview/previews/LegacyCallAnswerEventPreview.ts @@ -8,12 +8,12 @@ Please see LICENSE files in the repository root for full details. import { type MatrixEvent } from "matrix-js-sdk/src/matrix"; -import { type IPreview } from "./IPreview"; +import { type Preview } from "./Preview"; import { type TagID } from "../../room-list-v3/skip-list/tag"; import { getSenderName, isSelf, shouldPrefixMessagesIn } from "./utils"; import { _t } from "../../../languageHandler"; -export class LegacyCallAnswerEventPreview implements IPreview { +export class LegacyCallAnswerEventPreview implements Preview { public getTextFor(event: MatrixEvent, tagId?: TagID): string { if (shouldPrefixMessagesIn(event.getRoomId()!, tagId)) { if (isSelf(event)) { diff --git a/apps/web/src/stores/room-list/previews/LegacyCallHangupEvent.ts b/apps/web/src/stores/message-preview/previews/LegacyCallHangupEvent.ts similarity index 90% rename from apps/web/src/stores/room-list/previews/LegacyCallHangupEvent.ts rename to apps/web/src/stores/message-preview/previews/LegacyCallHangupEvent.ts index bceb017ffc..9f82ad32bb 100644 --- a/apps/web/src/stores/room-list/previews/LegacyCallHangupEvent.ts +++ b/apps/web/src/stores/message-preview/previews/LegacyCallHangupEvent.ts @@ -8,12 +8,12 @@ Please see LICENSE files in the repository root for full details. import { type MatrixEvent } from "matrix-js-sdk/src/matrix"; -import { type IPreview } from "./IPreview"; +import { type Preview } from "./Preview"; import { type TagID } from "../../room-list-v3/skip-list/tag"; import { getSenderName, isSelf, shouldPrefixMessagesIn } from "./utils"; import { _t } from "../../../languageHandler"; -export class LegacyCallHangupEvent implements IPreview { +export class LegacyCallHangupEvent implements Preview { public getTextFor(event: MatrixEvent, tagId?: TagID): string { if (shouldPrefixMessagesIn(event.getRoomId()!, tagId)) { if (isSelf(event)) { diff --git a/apps/web/src/stores/room-list/previews/LegacyCallInviteEventPreview.ts b/apps/web/src/stores/message-preview/previews/LegacyCallInviteEventPreview.ts similarity index 91% rename from apps/web/src/stores/room-list/previews/LegacyCallInviteEventPreview.ts rename to apps/web/src/stores/message-preview/previews/LegacyCallInviteEventPreview.ts index 427785c38e..e1de9e009e 100644 --- a/apps/web/src/stores/room-list/previews/LegacyCallInviteEventPreview.ts +++ b/apps/web/src/stores/message-preview/previews/LegacyCallInviteEventPreview.ts @@ -8,12 +8,12 @@ Please see LICENSE files in the repository root for full details. import { type MatrixEvent } from "matrix-js-sdk/src/matrix"; -import { type IPreview } from "./IPreview"; +import { type Preview } from "./Preview"; import { type TagID } from "../../room-list-v3/skip-list/tag"; import { getSenderName, isSelf, shouldPrefixMessagesIn } from "./utils"; import { _t } from "../../../languageHandler"; -export class LegacyCallInviteEventPreview implements IPreview { +export class LegacyCallInviteEventPreview implements Preview { public getTextFor(event: MatrixEvent, tagId?: TagID): string { if (shouldPrefixMessagesIn(event.getRoomId()!, tagId)) { if (isSelf(event)) { diff --git a/apps/web/src/stores/room-list/previews/MessageEventPreview.ts b/apps/web/src/stores/message-preview/previews/MessageEventPreview.ts similarity index 96% rename from apps/web/src/stores/room-list/previews/MessageEventPreview.ts rename to apps/web/src/stores/message-preview/previews/MessageEventPreview.ts index a5d58b3fb2..41de5f0b1a 100644 --- a/apps/web/src/stores/room-list/previews/MessageEventPreview.ts +++ b/apps/web/src/stores/message-preview/previews/MessageEventPreview.ts @@ -8,14 +8,14 @@ Please see LICENSE files in the repository root for full details. import { type MatrixEvent, MsgType, RelationType } from "matrix-js-sdk/src/matrix"; -import { type IPreview } from "./IPreview"; +import { type Preview } from "./Preview"; import { type TagID } from "../../room-list-v3/skip-list/tag"; import { _t, sanitizeForTranslation } from "../../../languageHandler"; import { getSenderName, isSelf, shouldPrefixMessagesIn } from "./utils"; import { getHtmlText } from "../../../HtmlUtils"; import { stripHTMLReply, stripPlainReply } from "../../../utils/Reply"; -export class MessageEventPreview implements IPreview { +export class MessageEventPreview implements Preview { public getTextFor(event: MatrixEvent, tagId?: TagID, isThread?: boolean): string | null { let eventContent = event.getContent(); diff --git a/apps/web/src/stores/room-list/previews/PollStartEventPreview.ts b/apps/web/src/stores/message-preview/previews/PollStartEventPreview.ts similarity index 95% rename from apps/web/src/stores/room-list/previews/PollStartEventPreview.ts rename to apps/web/src/stores/message-preview/previews/PollStartEventPreview.ts index 17bd5ffdf0..bc234aca4e 100644 --- a/apps/web/src/stores/room-list/previews/PollStartEventPreview.ts +++ b/apps/web/src/stores/message-preview/previews/PollStartEventPreview.ts @@ -10,13 +10,13 @@ import { type MatrixEvent, type PollStartEventContent } from "matrix-js-sdk/src/ import { InvalidEventError } from "matrix-js-sdk/src/extensible_events_v1/InvalidEventError"; import { PollStartEvent } from "matrix-js-sdk/src/extensible_events_v1/PollStartEvent"; -import { type IPreview } from "./IPreview"; +import { type Preview } from "./Preview"; import { type TagID } from "../../room-list-v3/skip-list/tag"; import { _t, sanitizeForTranslation } from "../../../languageHandler"; import { getSenderName, isSelf, shouldPrefixMessagesIn } from "./utils"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; -export class PollStartEventPreview implements IPreview { +export class PollStartEventPreview implements Preview { public static contextType = MatrixClientContext; declare public context: React.ContextType; diff --git a/apps/web/src/stores/room-list/previews/IPreview.ts b/apps/web/src/stores/message-preview/previews/Preview.ts similarity index 96% rename from apps/web/src/stores/room-list/previews/IPreview.ts rename to apps/web/src/stores/message-preview/previews/Preview.ts index 3601831980..fb2bba821d 100644 --- a/apps/web/src/stores/room-list/previews/IPreview.ts +++ b/apps/web/src/stores/message-preview/previews/Preview.ts @@ -13,7 +13,7 @@ import { type TagID } from "../../room-list-v3/skip-list/tag"; /** * Represents an event preview. */ -export interface IPreview { +export interface Preview { /** * Gets the text which represents the event as a preview. * @param event The event to preview. diff --git a/apps/web/src/stores/room-list/previews/ReactionEventPreview.ts b/apps/web/src/stores/message-preview/previews/ReactionEventPreview.ts similarity index 94% rename from apps/web/src/stores/room-list/previews/ReactionEventPreview.ts rename to apps/web/src/stores/message-preview/previews/ReactionEventPreview.ts index c43666f019..8b405f2fc8 100644 --- a/apps/web/src/stores/room-list/previews/ReactionEventPreview.ts +++ b/apps/web/src/stores/message-preview/previews/ReactionEventPreview.ts @@ -8,14 +8,14 @@ Please see LICENSE files in the repository root for full details. import { type MatrixEvent } from "matrix-js-sdk/src/matrix"; -import { type IPreview } from "./IPreview"; +import { type Preview } from "./Preview"; import { type TagID } from "../../room-list-v3/skip-list/tag"; import { getSenderName, isSelf } from "./utils"; import { _t } from "../../../languageHandler"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { MessagePreviewStore } from "../MessagePreviewStore"; -export class ReactionEventPreview implements IPreview { +export class ReactionEventPreview implements Preview { public getTextFor(event: MatrixEvent, tagId?: TagID, isThread?: boolean): string | null { const roomId = event.getRoomId(); if (!roomId) return null; // not a room event diff --git a/apps/web/src/stores/room-list/previews/StickerEventPreview.ts b/apps/web/src/stores/message-preview/previews/StickerEventPreview.ts similarity index 90% rename from apps/web/src/stores/room-list/previews/StickerEventPreview.ts rename to apps/web/src/stores/message-preview/previews/StickerEventPreview.ts index 3e1342faad..7c7b753e10 100644 --- a/apps/web/src/stores/room-list/previews/StickerEventPreview.ts +++ b/apps/web/src/stores/message-preview/previews/StickerEventPreview.ts @@ -8,12 +8,12 @@ Please see LICENSE files in the repository root for full details. import { type MatrixEvent } from "matrix-js-sdk/src/matrix"; -import { type IPreview } from "./IPreview"; +import { type Preview } from "./Preview"; import { type TagID } from "../../room-list-v3/skip-list/tag"; import { getSenderName, isSelf, shouldPrefixMessagesIn } from "./utils"; import { _t } from "../../../languageHandler"; -export class StickerEventPreview implements IPreview { +export class StickerEventPreview implements Preview { public getTextFor(event: MatrixEvent, tagId?: TagID, isThread?: boolean): string | null { const stickerName = event.getContent()["body"]; if (!stickerName) return null; diff --git a/apps/web/src/stores/message-preview/previews/index.ts b/apps/web/src/stores/message-preview/previews/index.ts new file mode 100644 index 0000000000..7882838c93 --- /dev/null +++ b/apps/web/src/stores/message-preview/previews/index.ts @@ -0,0 +1,14 @@ +/* + * 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. + */ + +export type * from "./Preview"; +export * from "./LegacyCallAnswerEventPreview"; +export * from "./LegacyCallHangupEvent"; +export * from "./MessageEventPreview"; +export * from "./StickerEventPreview"; +export * from "./PollStartEventPreview"; +export * from "./ReactionEventPreview"; diff --git a/apps/web/src/stores/room-list/previews/utils.ts b/apps/web/src/stores/message-preview/previews/utils.ts similarity index 100% rename from apps/web/src/stores/room-list/previews/utils.ts rename to apps/web/src/stores/message-preview/previews/utils.ts diff --git a/apps/web/src/viewmodels/room-list/RoomListItemViewModel.ts b/apps/web/src/viewmodels/room-list/RoomListItemViewModel.ts index aad87b2837..938a9d37f3 100644 --- a/apps/web/src/viewmodels/room-list/RoomListItemViewModel.ts +++ b/apps/web/src/viewmodels/room-list/RoomListItemViewModel.ts @@ -18,7 +18,7 @@ import type { Room, MatrixClient, RoomMember } from "matrix-js-sdk/src/matrix"; import type { RoomNotificationState } from "../../stores/notifications/RoomNotificationState"; import { RoomNotificationStateStore } from "../../stores/notifications/RoomNotificationStateStore"; import { NotificationStateEvents } from "../../stores/notifications/NotificationState"; -import { MessagePreviewStore } from "../../stores/room-list/MessagePreviewStore"; +import { MessagePreviewStore } from "../../stores/message-preview"; import { UPDATE_EVENT } from "../../stores/AsyncStore"; import { DefaultTagID } from "../../stores/room-list-v3/skip-list/tag"; import DMRoomMap from "../../utils/DMRoomMap"; diff --git a/apps/web/test/unit-tests/components/views/rooms/RoomTile-test.tsx b/apps/web/test/unit-tests/components/views/rooms/RoomTile-test.tsx index 54fcc93f23..0b583d1de2 100644 --- a/apps/web/test/unit-tests/components/views/rooms/RoomTile-test.tsx +++ b/apps/web/test/unit-tests/components/views/rooms/RoomTile-test.tsx @@ -42,7 +42,7 @@ import { TestSdkContext } from "../../../TestSdkContext"; import { SDKContext } from "../../../../../src/contexts/SDKContext"; import { shouldShowComponent } from "../../../../../src/customisations/helpers/UIComponents"; import { UIComponent } from "../../../../../src/settings/UIFeature"; -import { MessagePreviewStore } from "../../../../../src/stores/room-list/MessagePreviewStore"; +import { MessagePreviewStore } from "../../../../../src/stores/message-preview"; import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg"; import SettingsStore from "../../../../../src/settings/SettingsStore"; import { ConnectionState } from "../../../../../src/models/Call"; diff --git a/apps/web/test/unit-tests/stores/room-list/MessagePreviewStore-test.ts b/apps/web/test/unit-tests/stores/message-preview/MessagePreviewStore-test.ts similarity index 99% rename from apps/web/test/unit-tests/stores/room-list/MessagePreviewStore-test.ts rename to apps/web/test/unit-tests/stores/message-preview/MessagePreviewStore-test.ts index 007ae760e3..89d87d362b 100644 --- a/apps/web/test/unit-tests/stores/room-list/MessagePreviewStore-test.ts +++ b/apps/web/test/unit-tests/stores/message-preview/MessagePreviewStore-test.ts @@ -18,7 +18,7 @@ import { Room, } from "matrix-js-sdk/src/matrix"; -import { MessagePreviewStore } from "../../../../src/stores/room-list/MessagePreviewStore"; +import { MessagePreviewStore } from "../../../../src/stores/message-preview"; import { mkEvent, mkMessage, mkReaction, setupAsyncStoreWithClient, stubClient } from "../../../test-utils"; import { DefaultTagID } from "../../../../src/stores/room-list-v3/skip-list/tag"; import { mkThread } from "../../../test-utils/threads"; diff --git a/apps/web/test/unit-tests/stores/message-preview/previews/LegacyCallAnswerEventPreview-test.ts b/apps/web/test/unit-tests/stores/message-preview/previews/LegacyCallAnswerEventPreview-test.ts new file mode 100644 index 0000000000..68bf4b6b17 --- /dev/null +++ b/apps/web/test/unit-tests/stores/message-preview/previews/LegacyCallAnswerEventPreview-test.ts @@ -0,0 +1,79 @@ +/* + * 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 { Room } from "matrix-js-sdk/src/matrix"; +import { mocked } from "jest-mock"; + +import { LegacyCallAnswerEventPreview } from "../../../../../src/stores/message-preview/previews/LegacyCallAnswerEventPreview"; +import { DefaultTagID } from "../../../../../src/stores/room-list-v3/skip-list/tag"; +import { mkEvent, stubClient } from "../../../../test-utils"; +import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg"; + +describe("LegacyCallAnswerEventPreview", () => { + const preview = new LegacyCallAnswerEventPreview(); + const roomId = "!room:example.com"; + + beforeAll(() => { + stubClient(); + }); + + describe("getTextFor", () => { + describe("in a room that should be prefixed (non-DM)", () => { + // Default stub: getRoom returns null → shouldPrefixMessagesIn returns true + + it("returns 'You joined the call' when the event is from self", () => { + const selfUserId = MatrixClientPeg.safeGet().getSafeUserId(); + const event = mkEvent({ + event: true, + type: "m.call.answer", + content: {}, + user: selfUserId, + room: roomId, + }); + expect(preview.getTextFor(event)).toBe("You joined the call"); + }); + + it("returns ' joined the call' when the event is from someone else", () => { + const otherUserId = "@other:example.com"; + const event = mkEvent({ + event: true, + type: "m.call.answer", + content: {}, + user: otherUserId, + room: roomId, + }); + expect(preview.getTextFor(event)).toBe(`${otherUserId} joined the call`); + }); + }); + + describe("in a DM room (should not be prefixed)", () => { + beforeEach(() => { + const cli = MatrixClientPeg.safeGet(); + // Make a 1:1 room so shouldPrefixMessagesIn returns false + const room = new Room(roomId, cli, cli.getSafeUserId()); + jest.spyOn(room.currentState, "getJoinedMemberCount").mockReturnValue(2); + mocked(cli.getRoom).mockReturnValue(room); + }); + + afterEach(() => { + mocked(MatrixClientPeg.safeGet().getRoom).mockReturnValue(null); + }); + + it("returns 'Call in progress' regardless of sender", () => { + const otherUserId = "@other:example.com"; + const event = mkEvent({ + event: true, + type: "m.call.answer", + content: {}, + user: otherUserId, + room: roomId, + }); + expect(preview.getTextFor(event, DefaultTagID.DM)).toBe("Call in progress"); + }); + }); + }); +}); diff --git a/apps/web/test/unit-tests/stores/message-preview/previews/LegacyCallHangupEvent-test.ts b/apps/web/test/unit-tests/stores/message-preview/previews/LegacyCallHangupEvent-test.ts new file mode 100644 index 0000000000..f98752673a --- /dev/null +++ b/apps/web/test/unit-tests/stores/message-preview/previews/LegacyCallHangupEvent-test.ts @@ -0,0 +1,77 @@ +/* + * 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 { Room } from "matrix-js-sdk/src/matrix"; +import { mocked } from "jest-mock"; + +import { LegacyCallHangupEvent } from "../../../../../src/stores/message-preview/previews/LegacyCallHangupEvent"; +import { DefaultTagID } from "../../../../../src/stores/room-list-v3/skip-list/tag"; +import { mkEvent, stubClient } from "../../../../test-utils"; +import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg"; + +describe("LegacyCallHangupEvent", () => { + const preview = new LegacyCallHangupEvent(); + const roomId = "!room:example.com"; + + beforeAll(() => { + stubClient(); + }); + + describe("getTextFor", () => { + describe("in a room that should be prefixed (non-DM)", () => { + it("returns 'You ended the call' when the event is from self", () => { + const selfUserId = MatrixClientPeg.safeGet().getSafeUserId(); + const event = mkEvent({ + event: true, + type: "m.call.hangup", + content: {}, + user: selfUserId, + room: roomId, + }); + expect(preview.getTextFor(event)).toBe("You ended the call"); + }); + + it("returns ' ended the call' when the event is from someone else", () => { + const otherUserId = "@other:example.com"; + const event = mkEvent({ + event: true, + type: "m.call.hangup", + content: {}, + user: otherUserId, + room: roomId, + }); + expect(preview.getTextFor(event)).toBe(`${otherUserId} ended the call`); + }); + }); + + describe("in a DM room (should not be prefixed)", () => { + beforeEach(() => { + const cli = MatrixClientPeg.safeGet(); + // Make a 1:1 room so shouldPrefixMessagesIn returns false + const room = new Room(roomId, cli, cli.getSafeUserId()); + jest.spyOn(room.currentState, "getJoinedMemberCount").mockReturnValue(2); + mocked(cli.getRoom).mockReturnValue(room); + }); + + afterEach(() => { + mocked(MatrixClientPeg.safeGet().getRoom).mockReturnValue(null); + }); + + it("returns 'Call ended' regardless of sender", () => { + const otherUserId = "@other:example.com"; + const event = mkEvent({ + event: true, + type: "m.call.hangup", + content: {}, + user: otherUserId, + room: roomId, + }); + expect(preview.getTextFor(event, DefaultTagID.DM)).toBe("Call ended"); + }); + }); + }); +}); diff --git a/apps/web/test/unit-tests/stores/message-preview/previews/LegacyCallInviteEventPreview-test.ts b/apps/web/test/unit-tests/stores/message-preview/previews/LegacyCallInviteEventPreview-test.ts new file mode 100644 index 0000000000..a65c944da2 --- /dev/null +++ b/apps/web/test/unit-tests/stores/message-preview/previews/LegacyCallInviteEventPreview-test.ts @@ -0,0 +1,89 @@ +/* + * 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 { Room } from "matrix-js-sdk/src/matrix"; +import { mocked } from "jest-mock"; + +import { LegacyCallInviteEventPreview } from "../../../../../src/stores/message-preview/previews/LegacyCallInviteEventPreview"; +import { DefaultTagID } from "../../../../../src/stores/room-list-v3/skip-list/tag"; +import { mkEvent, stubClient } from "../../../../test-utils"; +import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg"; + +describe("LegacyCallInviteEventPreview", () => { + const preview = new LegacyCallInviteEventPreview(); + const roomId = "!room:example.com"; + + beforeAll(() => { + stubClient(); + }); + + describe("getTextFor", () => { + describe("in a room that should be prefixed (non-DM)", () => { + it("returns 'You started a call' when the event is from self", () => { + const selfUserId = MatrixClientPeg.safeGet().getSafeUserId(); + const event = mkEvent({ + event: true, + type: "m.call.invite", + content: {}, + user: selfUserId, + room: roomId, + }); + expect(preview.getTextFor(event)).toBe("You started a call"); + }); + + it("returns ' started a call' when the event is from someone else", () => { + const otherUserId = "@other:example.com"; + const event = mkEvent({ + event: true, + type: "m.call.invite", + content: {}, + user: otherUserId, + room: roomId, + }); + expect(preview.getTextFor(event)).toBe(`${otherUserId} started a call`); + }); + }); + + describe("in a DM room (should not be prefixed)", () => { + beforeEach(() => { + const cli = MatrixClientPeg.safeGet(); + // Make a 1:1 room so shouldPrefixMessagesIn returns false + const room = new Room(roomId, cli, cli.getSafeUserId()); + jest.spyOn(room.currentState, "getJoinedMemberCount").mockReturnValue(2); + mocked(cli.getRoom).mockReturnValue(room); + }); + + afterEach(() => { + mocked(MatrixClientPeg.safeGet().getRoom).mockReturnValue(null); + }); + + it("returns 'Waiting for answer' when the event is from self", () => { + const selfUserId = MatrixClientPeg.safeGet().getSafeUserId(); + const event = mkEvent({ + event: true, + type: "m.call.invite", + content: {}, + user: selfUserId, + room: roomId, + }); + expect(preview.getTextFor(event, DefaultTagID.DM)).toBe("Waiting for answer"); + }); + + it("returns ' is calling' when the event is from someone else", () => { + const otherUserId = "@other:example.com"; + const event = mkEvent({ + event: true, + type: "m.call.invite", + content: {}, + user: otherUserId, + room: roomId, + }); + expect(preview.getTextFor(event, DefaultTagID.DM)).toBe(`${otherUserId} is calling`); + }); + }); + }); +}); diff --git a/apps/web/test/unit-tests/stores/room-list/previews/MessageEventPreview-test.ts b/apps/web/test/unit-tests/stores/message-preview/previews/MessageEventPreview-test.ts similarity index 92% rename from apps/web/test/unit-tests/stores/room-list/previews/MessageEventPreview-test.ts rename to apps/web/test/unit-tests/stores/message-preview/previews/MessageEventPreview-test.ts index 78539d14fb..5ee7a74457 100644 --- a/apps/web/test/unit-tests/stores/room-list/previews/MessageEventPreview-test.ts +++ b/apps/web/test/unit-tests/stores/message-preview/previews/MessageEventPreview-test.ts @@ -8,7 +8,8 @@ Please see LICENSE files in the repository root for full details. import { RelationType } from "matrix-js-sdk/src/matrix"; -import { MessageEventPreview } from "../../../../../src/stores/room-list/previews/MessageEventPreview"; +// Import directly from the file to avoid circular dependencies with MessagePreviewStore +import { MessageEventPreview } from "../../../../../src/stores/message-preview/previews/MessageEventPreview"; import { mkEvent, stubClient } from "../../../../test-utils"; describe("MessageEventPreview", () => { diff --git a/apps/web/test/unit-tests/stores/room-list/previews/PollStartEventPreview-test.ts b/apps/web/test/unit-tests/stores/message-preview/previews/PollStartEventPreview-test.ts similarity index 91% rename from apps/web/test/unit-tests/stores/room-list/previews/PollStartEventPreview-test.ts rename to apps/web/test/unit-tests/stores/message-preview/previews/PollStartEventPreview-test.ts index bf6f82a5d8..24c0c0ddd3 100644 --- a/apps/web/test/unit-tests/stores/room-list/previews/PollStartEventPreview-test.ts +++ b/apps/web/test/unit-tests/stores/message-preview/previews/PollStartEventPreview-test.ts @@ -8,7 +8,8 @@ Please see LICENSE files in the repository root for full details. import { type MatrixClient } from "matrix-js-sdk/src/matrix"; -import { PollStartEventPreview } from "../../../../../src/stores/room-list/previews/PollStartEventPreview"; +// Import directly from the file to avoid circular dependencies with MessagePreviewStore +import { PollStartEventPreview } from "../../../../../src/stores/message-preview/previews/PollStartEventPreview"; import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg"; import { makePollStartEvent } from "../../../../test-utils"; diff --git a/apps/web/test/unit-tests/stores/room-list/previews/ReactionEventPreview-test.ts b/apps/web/test/unit-tests/stores/message-preview/previews/ReactionEventPreview-test.ts similarity index 97% rename from apps/web/test/unit-tests/stores/room-list/previews/ReactionEventPreview-test.ts rename to apps/web/test/unit-tests/stores/message-preview/previews/ReactionEventPreview-test.ts index 8e01460abf..977ee61ebe 100644 --- a/apps/web/test/unit-tests/stores/room-list/previews/ReactionEventPreview-test.ts +++ b/apps/web/test/unit-tests/stores/message-preview/previews/ReactionEventPreview-test.ts @@ -10,7 +10,8 @@ import { RelationType, Room, RoomMember } from "matrix-js-sdk/src/matrix"; import { mocked } from "jest-mock"; import { mkEvent, stubClient } from "../../../../test-utils"; -import { ReactionEventPreview } from "../../../../../src/stores/room-list/previews/ReactionEventPreview"; +// Import directly from the file to avoid circular dependencies with MessagePreviewStore +import { ReactionEventPreview } from "../../../../../src/stores/message-preview/previews/ReactionEventPreview"; import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg"; describe("ReactionEventPreview", () => { diff --git a/apps/web/test/unit-tests/stores/message-preview/previews/StickerEventPreview-test.ts b/apps/web/test/unit-tests/stores/message-preview/previews/StickerEventPreview-test.ts new file mode 100644 index 0000000000..27454c841e --- /dev/null +++ b/apps/web/test/unit-tests/stores/message-preview/previews/StickerEventPreview-test.ts @@ -0,0 +1,115 @@ +/* + * 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 { Room } from "matrix-js-sdk/src/matrix"; +import { mocked } from "jest-mock"; + +import { StickerEventPreview } from "../../../../../src/stores/message-preview/previews/StickerEventPreview"; +import { DefaultTagID } from "../../../../../src/stores/room-list-v3/skip-list/tag"; +import { mkEvent, stubClient } from "../../../../test-utils"; +import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg"; + +describe("StickerEventPreview", () => { + const preview = new StickerEventPreview(); + const roomId = "!room:example.com"; + + beforeAll(() => { + stubClient(); + }); + + describe("getTextFor", () => { + it("returns null when the event has no body", () => { + const event = mkEvent({ + event: true, + type: "m.sticker", + content: {}, + user: "@other:example.com", + room: roomId, + }); + expect(preview.getTextFor(event)).toBeNull(); + }); + + it("returns null when the body is an empty string", () => { + const event = mkEvent({ + event: true, + type: "m.sticker", + content: { body: "" }, + user: "@other:example.com", + room: roomId, + }); + expect(preview.getTextFor(event)).toBeNull(); + }); + + describe("in a room that should be prefixed (non-DM)", () => { + // Default stub: getRoom returns null → shouldPrefixMessagesIn returns true + + it("returns ': ' when the event is from someone else", () => { + const otherUserId = "@other:example.com"; + const event = mkEvent({ + event: true, + type: "m.sticker", + content: { body: "wave" }, + user: otherUserId, + room: roomId, + }); + expect(preview.getTextFor(event)).toBe(`${otherUserId}: wave`); + }); + + it("returns just the sticker name when the event is from self", () => { + const selfUserId = MatrixClientPeg.safeGet().getSafeUserId(); + const event = mkEvent({ + event: true, + type: "m.sticker", + content: { body: "wave" }, + user: selfUserId, + room: roomId, + }); + expect(preview.getTextFor(event)).toBe("wave"); + }); + }); + + describe("in a DM room (should not be prefixed)", () => { + beforeEach(() => { + const cli = MatrixClientPeg.safeGet(); + // Make a 1:1 room so shouldPrefixMessagesIn returns false + const room = new Room(roomId, cli, cli.getSafeUserId()); + jest.spyOn(room.currentState, "getJoinedMemberCount").mockReturnValue(2); + mocked(cli.getRoom).mockReturnValue(room); + }); + + afterEach(() => { + mocked(MatrixClientPeg.safeGet().getRoom).mockReturnValue(null); + }); + + it("returns just the sticker name regardless of sender", () => { + const otherUserId = "@other:example.com"; + const event = mkEvent({ + event: true, + type: "m.sticker", + content: { body: "wave" }, + user: otherUserId, + room: roomId, + }); + expect(preview.getTextFor(event, DefaultTagID.DM)).toBe("wave"); + }); + }); + + describe("in a thread", () => { + it("returns just the sticker name regardless of sender", () => { + const otherUserId = "@other:example.com"; + const event = mkEvent({ + event: true, + type: "m.sticker", + content: { body: "wave" }, + user: otherUserId, + room: roomId, + }); + expect(preview.getTextFor(event, undefined, true)).toBe("wave"); + }); + }); + }); +}); diff --git a/apps/web/test/viewmodels/room-list/RoomListItemViewModel-test.tsx b/apps/web/test/viewmodels/room-list/RoomListItemViewModel-test.tsx index 0fdacf00b4..9806365e9a 100644 --- a/apps/web/test/viewmodels/room-list/RoomListItemViewModel-test.tsx +++ b/apps/web/test/viewmodels/room-list/RoomListItemViewModel-test.tsx @@ -19,7 +19,7 @@ import { createTestClient, flushPromises } from "../../test-utils"; import { RoomNotificationState } from "../../../src/stores/notifications/RoomNotificationState"; import { RoomNotificationStateStore } from "../../../src/stores/notifications/RoomNotificationStateStore"; import { NotificationStateEvents } from "../../../src/stores/notifications/NotificationState"; -import { type MessagePreview, MessagePreviewStore } from "../../../src/stores/room-list/MessagePreviewStore"; +import { type MessagePreview, MessagePreviewStore } from "../../../src/stores/message-preview"; import { UPDATE_EVENT } from "../../../src/stores/AsyncStore"; import SettingsStore from "../../../src/settings/SettingsStore"; import DMRoomMap from "../../../src/utils/DMRoomMap";