diff --git a/package.json b/package.json index 994d332f85..51cfb8df7e 100644 --- a/package.json +++ b/package.json @@ -93,7 +93,7 @@ "@types/png-chunks-extract": "^1.0.2", "@types/react-virtualized": "^9.21.30", "@vector-im/compound-design-tokens": "^4.0.0", - "@vector-im/compound-web": "^7.10.1", + "@vector-im/compound-web": "^7.10.2", "@vector-im/matrix-wysiwyg": "2.38.3", "@zxcvbn-ts/core": "^3.0.4", "@zxcvbn-ts/language-common": "^3.0.4", diff --git a/playwright/e2e/left-panel/room-list-panel/room-list.spec.ts b/playwright/e2e/left-panel/room-list-panel/room-list.spec.ts index 4d973dbb49..a84a8b09d0 100644 --- a/playwright/e2e/left-panel/room-list-panel/room-list.spec.ts +++ b/playwright/e2e/left-panel/room-list-panel/room-list.spec.ts @@ -271,6 +271,22 @@ test.describe("Room list", () => { await expect(room).toMatchScreenshot("room-list-item-mention.png"); }); + test("should render a message preview", { tag: "@screenshot" }, async ({ page, app, user, bot }) => { + const roomListView = getRoomList(page); + + await page.getByRole("button", { name: "Room Options" }).click(); + await page.getByRole("menuitemcheckbox", { name: "Show message previews" }).click(); + + const roomId = await app.client.createRoom({ name: "activity" }); + await app.client.inviteUser(roomId, bot.credentials.userId); + await bot.joinRoom(roomId); + await bot.sendMessage(roomId, "I am a robot. Beep."); + + const room = roomListView.getByRole("gridcell", { name: "activity" }); + await expect(room.getByText("I am a robot. Beep.")).toBeVisible(); + await expect(room).toMatchScreenshot("room-list-item-message-preview.png"); + }); + test("should render an activity decoration", { tag: "@screenshot" }, async ({ page, app, user, bot }) => { const roomListView = getRoomList(page); diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/room-panel-empty-room-list-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/room-panel-empty-room-list-linux.png index 6bc9d4696e..edef3547e4 100644 Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/room-panel-empty-room-list-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/room-panel-empty-room-list-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list-header.spec.ts/room-list-header-space-menu-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list-header.spec.ts/room-list-header-space-menu-linux.png index 49cf6ef08a..6234afc8d5 100644 Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list-header.spec.ts/room-list-header-space-menu-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list-header.spec.ts/room-list-header-space-menu-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list-panel.spec.ts/room-list-panel-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list-panel.spec.ts/room-list-panel-linux.png index 6f2daa3017..480e4dfc59 100644 Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list-panel.spec.ts/room-list-panel-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list-panel.spec.ts/room-list-panel-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-message-preview-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-message-preview-linux.png new file mode 100644 index 0000000000..a7b37ac40a Binary files /dev/null and b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-message-preview-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-more-options-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-more-options-linux.png index 6257aa7af8..91b7934df1 100644 Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-more-options-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-more-options-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-notification-options-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-notification-options-linux.png index cad0f83b4a..66a3646f37 100644 Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-notification-options-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-notification-options-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-notification-options-selection-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-notification-options-selection-linux.png index fb8aabd6bf..2ebf409cb5 100644 Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-notification-options-selection-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-notification-options-selection-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-linux.png index 1f7a84c9a6..85cd7360bf 100644 Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-scrolled-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-scrolled-linux.png index 87c88e8e46..d194b52540 100644 Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-scrolled-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-scrolled-linux.png differ diff --git a/res/css/_components.pcss b/res/css/_components.pcss index 666a1c69dd..38bce14b06 100644 --- a/res/css/_components.pcss +++ b/res/css/_components.pcss @@ -279,6 +279,7 @@ @import "./views/rooms/RoomListPanel/_RoomListPanel.pcss"; @import "./views/rooms/RoomListPanel/_RoomListPrimaryFilters.pcss"; @import "./views/rooms/RoomListPanel/_RoomListSearch.pcss"; +@import "./views/rooms/RoomListPanel/_RoomListSecondaryFilters.pcss"; @import "./views/rooms/_AppsDrawer.pcss"; @import "./views/rooms/_Autocomplete.pcss"; @import "./views/rooms/_AuxPanel.pcss"; diff --git a/res/css/views/rooms/RoomListPanel/_RoomListItemView.pcss b/res/css/views/rooms/RoomListPanel/_RoomListItemView.pcss index ee228ec262..a832a4af40 100644 --- a/res/css/views/rooms/RoomListPanel/_RoomListItemView.pcss +++ b/res/css/views/rooms/RoomListPanel/_RoomListItemView.pcss @@ -35,11 +35,23 @@ box-sizing: border-box; min-width: 0; + .mx_RoomListItemView_text { + max-width: 100%; + } + .mx_RoomListItemView_roomName { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } + + .mx_RoomListItemView_messagePreview { + font: var(--cpd-font-body-sm-regular); + color: var(--cpd-color-text-secondary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } } } } diff --git a/res/css/views/rooms/RoomListPanel/_RoomListSecondaryFilters.pcss b/res/css/views/rooms/RoomListPanel/_RoomListSecondaryFilters.pcss new file mode 100644 index 0000000000..c94fb54007 --- /dev/null +++ b/res/css/views/rooms/RoomListPanel/_RoomListSecondaryFilters.pcss @@ -0,0 +1,25 @@ +/* + * Copyright 2025 New Vector 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. + */ + +.mx_RoomListSecondaryFilters { + font: var(--cpd-font-body-md-medium); + margin: var(--cpd-space-2x); + margin-left: var(--cpd-space-1x); +} + +.mx_RoomListSecondaryFilters_roomOptionsButton { + /* Size the button appropriately (should this be in em, maybe, + * so it gets bigger with font size? These values taken from the figma. + */ + width: 28px; + height: 28px; + margin-left: auto; + + svg { + color: var(--cpd-color-icon-primary); + } +} diff --git a/src/components/viewmodels/roomlist/RoomListItemViewModel.tsx b/src/components/viewmodels/roomlist/RoomListItemViewModel.tsx index e543dd46b2..c1b07b1558 100644 --- a/src/components/viewmodels/roomlist/RoomListItemViewModel.tsx +++ b/src/components/viewmodels/roomlist/RoomListItemViewModel.tsx @@ -16,11 +16,14 @@ import { _t } from "../../../languageHandler"; import { type RoomNotificationState } from "../../../stores/notifications/RoomNotificationState"; import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore"; import { useMatrixClientContext } from "../../../contexts/MatrixClientContext"; -import { useEventEmitterState, useTypedEventEmitter } from "../../../hooks/useEventEmitter"; +import { useEventEmitter, useEventEmitterState, useTypedEventEmitter } from "../../../hooks/useEventEmitter"; import { DefaultTagID } from "../../../stores/room-list/models"; import { useCall, useConnectionState, useParticipantCount } from "../../../hooks/useCall"; import { type ConnectionState } from "../../../models/Call"; import { NotificationStateEvents } from "../../../stores/notifications/NotificationState"; +import DMRoomMap from "../../../utils/DMRoomMap"; +import { MessagePreviewStore } from "../../../stores/room-list/MessagePreviewStore"; +import { useMessagePreviewToggle } from "./useMessagePreviewToggle"; export interface RoomListItemViewState { /** @@ -60,6 +63,11 @@ export interface RoomListItemViewState { * Whether there are participants in the call. */ hasParticipantInCall: boolean; + /** + * Pre-rendered and translated preview for the latest message in the room, or undefined + * if no preview should be shown. + */ + messagePreview: string | undefined; /** * Whether the notification decoration should be shown. */ @@ -104,6 +112,8 @@ export function useRoomListItemViewModel(room: Room): RoomListItemViewState { !invited && (hasAccessToOptionsMenu(room) || hasAccessToNotificationMenu(room, matrixClient.isGuest(), isArchived)); + const messagePreview = useRoomMessagePreview(room); + // Video room const isVideoRoom = room.isElementVideoRoom() || room.isCallRoom(); // EC video call or video room @@ -134,6 +144,7 @@ export function useRoomListItemViewModel(room: Room): RoomListItemViewState { isVideoRoom, callConnectionState, hasParticipantInCall, + messagePreview, showNotificationDecoration, }; } @@ -190,3 +201,36 @@ function getA11yLabel(roomName: string, notificationState: RoomNotificationState return _t("room_list|room|open_room", { roomName }); } } + +function useRoomMessagePreview(room: Room): string | undefined { + const { shouldShowMessagePreview } = useMessagePreviewToggle(); + const [previewText, setPreviewText] = useState(undefined); + + const updatePreview = useCallback(async () => { + if (!shouldShowMessagePreview) { + setPreviewText(undefined); + return; + } + + const roomIsDM = Boolean(DMRoomMap.shared().getUserIdForRoomId(room.roomId)); + // For the tag, we only care about whether the room is a DM or not as we don't show + // display names in previewsd for DMs, so anything else we just say is 'untagged' + // (even though it could actually be have other tags: we don't care about them). + const messagePreview = await MessagePreviewStore.instance.getPreviewForRoom( + room, + roomIsDM ? DefaultTagID.DM : DefaultTagID.Untagged, + ); + if (messagePreview) setPreviewText(messagePreview.text); + }, [room, shouldShowMessagePreview]); + + // MessagePreviewStore and the other AsyncStores need to be converted to TypedEventEmitter + useEventEmitter(MessagePreviewStore.instance, MessagePreviewStore.getPreviewChangedEventName(room), () => { + updatePreview(); + }); + + useEffect(() => { + updatePreview(); + }, [updatePreview]); + + return previewText; +} diff --git a/src/components/viewmodels/roomlist/useMessagePreviewToggle.tsx b/src/components/viewmodels/roomlist/useMessagePreviewToggle.tsx index 27ccd40106..efb58b3e04 100644 --- a/src/components/viewmodels/roomlist/useMessagePreviewToggle.tsx +++ b/src/components/viewmodels/roomlist/useMessagePreviewToggle.tsx @@ -4,10 +4,11 @@ * 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 { useCallback, useState } from "react"; +import { useCallback } from "react"; import SettingsStore from "../../../settings/SettingsStore"; import { SettingLevel } from "../../../settings/SettingLevel"; +import { useSettingValue } from "../../../hooks/useSettings"; interface MessagePreviewToggleState { shouldShowMessagePreview: boolean; @@ -20,17 +21,12 @@ interface MessagePreviewToggleState { * - Provides a function to toggle message previews. */ export function useMessagePreviewToggle(): MessagePreviewToggleState { - const [shouldShowMessagePreview, setShouldShowMessagePreview] = useState(() => - SettingsStore.getValue("RoomList.showMessagePreview"), - ); + const shouldShowMessagePreview = useSettingValue("RoomList.showMessagePreview"); const toggleMessagePreview = useCallback((): void => { - setShouldShowMessagePreview((current) => { - const toggled = !current; - SettingsStore.setValue("RoomList.showMessagePreview", null, SettingLevel.DEVICE, toggled); - return toggled; - }); - }, []); + const toggled = !shouldShowMessagePreview; + SettingsStore.setValue("RoomList.showMessagePreview", null, SettingLevel.DEVICE, toggled); + }, [shouldShowMessagePreview]); return { toggleMessagePreview, shouldShowMessagePreview }; } diff --git a/src/components/views/rooms/RoomListPanel/RoomListItemView.tsx b/src/components/views/rooms/RoomListPanel/RoomListItemView.tsx index 36839d75b0..ce77f5eb76 100644 --- a/src/components/views/rooms/RoomListPanel/RoomListItemView.tsx +++ b/src/components/views/rooms/RoomListPanel/RoomListItemView.tsx @@ -15,7 +15,7 @@ import { RoomListItemMenuView } from "./RoomListItemMenuView"; import { NotificationDecoration } from "../NotificationDecoration"; import { RoomAvatarView } from "../../avatars/RoomAvatarView"; -interface RoomListItemViewPropsProps extends React.HTMLAttributes { +interface RoomListItemViewProps extends React.HTMLAttributes { /** * The room to display */ @@ -33,7 +33,7 @@ export const RoomListItemView = memo(function RoomListItemView({ room, isSelected, ...props -}: RoomListItemViewPropsProps): JSX.Element { +}: RoomListItemViewProps): JSX.Element { const vm = useRoomListItemViewModel(room); const [isHover, setIsHover] = useState(false); @@ -73,9 +73,12 @@ export const RoomListItemView = memo(function RoomListItemView({ justify="space-between" > {/* We truncate the room name when too long. Title here is to show the full name on hover */} - - {vm.name} - +
+
+ {vm.name} +
+
{vm.messagePreview}
+
{showHoverDecoration ? ( { + ref?: Ref; +} + +const MenuTrigger = ({ ref, ...props }: MenuTriggerProps): JSX.Element => ( + + + + + +); + +interface Props { + /** + * The view model for the room list view + */ + vm: RoomListViewState; +} + +export function RoomListOptionsMenu({ vm }: Props): JSX.Element { + const [open, setOpen] = useState(false); + + return ( + } + > + + + + ); +} diff --git a/src/components/views/rooms/RoomListPanel/RoomListSecondaryFilters.tsx b/src/components/views/rooms/RoomListPanel/RoomListSecondaryFilters.tsx new file mode 100644 index 0000000000..f162c38f77 --- /dev/null +++ b/src/components/views/rooms/RoomListPanel/RoomListSecondaryFilters.tsx @@ -0,0 +1,36 @@ +/* + * Copyright 2025 New Vector 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 React, { type JSX } from "react"; + +import type { RoomListViewState } from "../../../viewmodels/roomlist/RoomListViewModel"; +import { Flex } from "../../../utils/Flex"; +import { _t } from "../../../../languageHandler"; +import { RoomListOptionsMenu } from "./RoomListOptionsMenu"; + +interface Props { + /** + * The view model for the room list + */ + vm: RoomListViewState; +} + +/** + * The secondary filters for the room list (eg. mentions only / invites only). + */ +export function RoomListSecondaryFilters({ vm }: Props): JSX.Element { + return ( + + + + ); +} diff --git a/src/components/views/rooms/RoomListPanel/RoomListView.tsx b/src/components/views/rooms/RoomListPanel/RoomListView.tsx index f4800f7009..98273345a8 100644 --- a/src/components/views/rooms/RoomListPanel/RoomListView.tsx +++ b/src/components/views/rooms/RoomListPanel/RoomListView.tsx @@ -11,6 +11,7 @@ import { useRoomListViewModel } from "../../../viewmodels/roomlist/RoomListViewM import { RoomList } from "./RoomList"; import { EmptyRoomList } from "./EmptyRoomList"; import { RoomListPrimaryFilters } from "./RoomListPrimaryFilters"; +import { RoomListSecondaryFilters } from "./RoomListSecondaryFilters"; /** * Host the room list and the (future) room filters @@ -22,6 +23,7 @@ export function RoomListView(): JSX.Element { return ( <> + {isRoomListEmpty ? : } ); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 9dcf5aa21d..234e6d7eb3 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2102,6 +2102,7 @@ "room_list": { "add_room_label": "Add room", "add_space_label": "Add space", + "appearance": "Appearance", "breadcrumbs_empty": "No recently visited rooms", "breadcrumbs_label": "Recently visited rooms", "empty": { @@ -2152,7 +2153,10 @@ "more_options": "More Options", "open_room": "Open room %(roomName)s" }, + "room_options": "Room Options", + "secondary_filters": "Secondary filters", "show_less": "Show less", + "show_message_previews": "Show message previews", "show_n_more": { "one": "Show %(count)s more", "other": "Show %(count)s more" diff --git a/test/unit-tests/components/viewmodels/roomlist/RoomListItemViewModel-test.tsx b/test/unit-tests/components/viewmodels/roomlist/RoomListItemViewModel-test.tsx index c00c38227d..342a24366a 100644 --- a/test/unit-tests/components/viewmodels/roomlist/RoomListItemViewModel-test.tsx +++ b/test/unit-tests/components/viewmodels/roomlist/RoomListItemViewModel-test.tsx @@ -5,7 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ -import { renderHook } from "jest-matrix-react"; +import { renderHook, waitFor } from "jest-matrix-react"; import { type Room } from "matrix-js-sdk/src/matrix"; import { mocked } from "jest-mock"; @@ -20,22 +20,40 @@ import { import { RoomNotificationState } from "../../../../../src/stores/notifications/RoomNotificationState"; import { RoomNotificationStateStore } from "../../../../../src/stores/notifications/RoomNotificationStateStore"; import * as UseCallModule from "../../../../../src/hooks/useCall"; +import { type MessagePreview, MessagePreviewStore } from "../../../../../src/stores/room-list/MessagePreviewStore"; +import DMRoomMap from "../../../../../src/utils/DMRoomMap"; +import { useMessagePreviewToggle } from "../../../../../src/components/viewmodels/roomlist/useMessagePreviewToggle"; jest.mock("../../../../../src/components/viewmodels/roomlist/utils", () => ({ hasAccessToOptionsMenu: jest.fn().mockReturnValue(false), hasAccessToNotificationMenu: jest.fn().mockReturnValue(false), })); +jest.mock("../../../../../src/components/viewmodels/roomlist/useMessagePreviewToggle", () => ({ + useMessagePreviewToggle: jest.fn().mockReturnValue({ shouldShowMessagePreview: true }), +})); + describe("RoomListItemViewModel", () => { let room: Room; beforeEach(() => { const matrixClient = createTestClient(); room = mkStubRoom("roomId", "roomName", matrixClient); + + const dmRoomMap = { + getUserIdForRoomId: jest.fn(), + getDMRoomsForUserId: jest.fn(), + } as unknown as DMRoomMap; + DMRoomMap.setShared(dmRoomMap); + + mocked(useMessagePreviewToggle).mockReturnValue({ + shouldShowMessagePreview: false, + toggleMessagePreview: jest.fn(), + }); }); afterEach(() => { - jest.resetAllMocks(); + jest.restoreAllMocks(); }); it("should dispatch view room action on openRoom", async () => { @@ -87,6 +105,39 @@ describe("RoomListItemViewModel", () => { expect(vm.current.showHoverMenu).toBe(true); }); + it("should return a message preview if one is available and they are enabled", async () => { + jest.spyOn(MessagePreviewStore.instance, "getPreviewForRoom").mockResolvedValue({ + text: "Message look like this", + } as MessagePreview); + mocked(useMessagePreviewToggle).mockReturnValue({ + shouldShowMessagePreview: true, + toggleMessagePreview: jest.fn(), + }); + + const { result: vm } = renderHook( + () => useRoomListItemViewModel(room), + withClientContextRenderOptions(room.client), + ); + await waitFor(() => expect(vm.current.messagePreview).toBe("Message look like this")); + }); + + it("should hide message previews when disabled", async () => { + jest.spyOn(MessagePreviewStore.instance, "getPreviewForRoom").mockResolvedValue({ + text: "Message look like this", + } as MessagePreview); + + const { result: vm, rerender } = renderHook( + () => useRoomListItemViewModel(room), + withClientContextRenderOptions(room.client), + ); + + // This doesn't seem to test that the hook actually triggers an update, + // but I can't see how to test that. + rerender(); + + expect(vm.current.messagePreview).toBe(undefined); + }); + describe("notification", () => { let notificationState: RoomNotificationState; beforeEach(() => { diff --git a/test/unit-tests/components/viewmodels/roomlist/RoomListViewModel-test.tsx b/test/unit-tests/components/viewmodels/roomlist/RoomListViewModel-test.tsx index 309d721d37..d0fd6c420b 100644 --- a/test/unit-tests/components/viewmodels/roomlist/RoomListViewModel-test.tsx +++ b/test/unit-tests/components/viewmodels/roomlist/RoomListViewModel-test.tsx @@ -17,13 +17,14 @@ import { FilterKey } from "../../../../../src/stores/room-list-v3/skip-list/filt import { SecondaryFilters } from "../../../../../src/components/viewmodels/roomlist/useFilteredRooms"; import { SortingAlgorithm } from "../../../../../src/stores/room-list-v3/skip-list/sorters"; import { SortOption } from "../../../../../src/components/viewmodels/roomlist/useSorter"; -import SettingsStore from "../../../../../src/settings/SettingsStore"; +import SettingsStore, { type CallbackFn } from "../../../../../src/settings/SettingsStore"; import { hasCreateRoomRights, createRoom } from "../../../../../src/components/viewmodels/roomlist/utils"; import dispatcher from "../../../../../src/dispatcher/dispatcher"; import { Action } from "../../../../../src/dispatcher/actions"; import { SdkContextClass } from "../../../../../src/contexts/SDKContext"; import SpaceStore from "../../../../../src/stores/spaces/SpaceStore"; import { UPDATE_SELECTED_SPACE } from "../../../../../src/stores/spaces"; +import { SettingLevel } from "../../../../../src/settings/SettingLevel"; jest.mock("../../../../../src/components/viewmodels/roomlist/utils", () => ({ hasCreateRoomRights: jest.fn().mockReturnValue(false), @@ -308,6 +309,25 @@ describe("RoomListViewModel", () => { expect(vm.current.shouldShowMessagePreview).toEqual(true); }); + it("should update when setting changes", () => { + jest.spyOn(SettingsStore, "getValue").mockImplementation(() => true); + + let watchFn: CallbackFn; + jest.spyOn(SettingsStore, "watchSetting").mockImplementation((_settingname, _roomId, fn) => { + watchFn = fn; + return ""; + }); + mockAndCreateRooms(); + const { result: vm } = renderHook(() => useRoomListViewModel()); + expect(vm.current.shouldShowMessagePreview).toEqual(true); + + jest.spyOn(SettingsStore, "getValue").mockImplementation(() => false); + act(() => { + watchFn("RoomList.showMessagePreview", "", SettingLevel.DEVICE, false, false); + }); + expect(vm.current.shouldShowMessagePreview).toEqual(false); + }); + it("should change setting on toggle", () => { jest.spyOn(SettingsStore, "getValue").mockImplementation(() => true); const fn = jest.spyOn(SettingsStore, "setValue").mockImplementation(async () => {}); @@ -317,8 +337,7 @@ describe("RoomListViewModel", () => { act(() => { vm.current.toggleMessagePreview(); }); - expect(vm.current.shouldShowMessagePreview).toEqual(false); - expect(fn).toHaveBeenCalled(); + expect(fn).toHaveBeenCalledWith("RoomList.showMessagePreview", null, "device", false); }); }); diff --git a/test/unit-tests/components/views/rooms/RoomListPanel/RoomListItemView-test.tsx b/test/unit-tests/components/views/rooms/RoomListPanel/RoomListItemView-test.tsx index 0cbd34811b..472258374e 100644 --- a/test/unit-tests/components/views/rooms/RoomListPanel/RoomListItemView-test.tsx +++ b/test/unit-tests/components/views/rooms/RoomListPanel/RoomListItemView-test.tsx @@ -51,6 +51,7 @@ describe("", () => { hasParticipantInCall: false, name: room.name, showNotificationDecoration: false, + messagePreview: undefined, }; mocked(useRoomListItemViewModel).mockReturnValue(defaultValue); @@ -62,6 +63,14 @@ describe("", () => { expect(asFragment()).toMatchSnapshot(); }); + test("should render a room item with a message preview", () => { + defaultValue.messagePreview = "The message looks list this"; + + const onClick = jest.fn(); + const { asFragment } = render(); + expect(asFragment()).toMatchSnapshot(); + }); + test("should call openRoom when clicked", async () => { const user = userEvent.setup(); render(); diff --git a/test/unit-tests/components/views/rooms/RoomListPanel/RoomListSecondaryFilters-test.tsx b/test/unit-tests/components/views/rooms/RoomListPanel/RoomListSecondaryFilters-test.tsx new file mode 100644 index 0000000000..b3ae99c3d2 --- /dev/null +++ b/test/unit-tests/components/views/rooms/RoomListPanel/RoomListSecondaryFilters-test.tsx @@ -0,0 +1,41 @@ +/* + * Copyright 2025 New Vector 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 React from "react"; +import { render, screen } from "jest-matrix-react"; + +import { type RoomListViewState } from "../../../../../../src/components/viewmodels/roomlist/RoomListViewModel"; +import { SecondaryFilters } from "../../../../../../src/components/viewmodels/roomlist/useFilteredRooms"; +import { SortOption } from "../../../../../../src/components/viewmodels/roomlist/useSorter"; +import { RoomListSecondaryFilters } from "../../../../../../src/components/views/rooms/RoomListPanel/RoomListSecondaryFilters"; + +describe("", () => { + let vm: RoomListViewState; + + beforeEach(() => { + vm = { + rooms: [], + canCreateRoom: true, + createRoom: jest.fn(), + createChatRoom: jest.fn(), + primaryFilters: [], + activateSecondaryFilter: () => {}, + activeSecondaryFilter: SecondaryFilters.AllActivity, + sort: jest.fn(), + activeSortOption: SortOption.Activity, + shouldShowMessagePreview: false, + toggleMessagePreview: jest.fn(), + activeIndex: undefined, + }; + }); + + it("should render 'room options' button", async () => { + const { asFragment } = render(); + expect(screen.getByRole("button", { name: "Room Options" })).toBeInTheDocument(); + expect(asFragment()).toMatchSnapshot(); + }); +}); diff --git a/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomList-test.tsx.snap b/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomList-test.tsx.snap index e6053ccdbb..85511067cd 100644 --- a/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomList-test.tsx.snap +++ b/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomList-test.tsx.snap @@ -57,12 +57,19 @@ exports[` should render a room list 1`] = ` class="mx_Flex mx_RoomListItemView_content" style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;" > - - room0 - +
+ room0 +
+
+
@@ -101,12 +108,19 @@ exports[` should render a room list 1`] = ` class="mx_Flex mx_RoomListItemView_content" style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;" > - - room1 - +
+ room1 +
+
+
@@ -145,12 +159,19 @@ exports[` should render a room list 1`] = ` class="mx_Flex mx_RoomListItemView_content" style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;" > - - room2 - +
+ room2 +
+
+
@@ -189,12 +210,19 @@ exports[` should render a room list 1`] = ` class="mx_Flex mx_RoomListItemView_content" style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;" > - - room3 - +
+ room3 +
+
+
@@ -233,12 +261,19 @@ exports[` should render a room list 1`] = ` class="mx_Flex mx_RoomListItemView_content" style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;" > - - room4 - +
+ room4 +
+
+
@@ -277,12 +312,19 @@ exports[` should render a room list 1`] = ` class="mx_Flex mx_RoomListItemView_content" style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;" > - - room5 - +
+ room5 +
+
+
@@ -321,12 +363,19 @@ exports[` should render a room list 1`] = ` class="mx_Flex mx_RoomListItemView_content" style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;" > - - room6 - +
+ room6 +
+
+
@@ -365,12 +414,19 @@ exports[` should render a room list 1`] = ` class="mx_Flex mx_RoomListItemView_content" style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;" > - - room7 - +
+ room7 +
+
+
@@ -409,12 +465,19 @@ exports[` should render a room list 1`] = ` class="mx_Flex mx_RoomListItemView_content" style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;" > - - room8 - +
+ room8 +
+
+
@@ -453,12 +516,19 @@ exports[` should render a room list 1`] = ` class="mx_Flex mx_RoomListItemView_content" style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;" > - - room9 - +
+ room9 +
+
+
diff --git a/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomListItemView-test.tsx.snap b/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomListItemView-test.tsx.snap index e5f4217a53..d24553fa9f 100644 --- a/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomListItemView-test.tsx.snap +++ b/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomListItemView-test.tsx.snap @@ -35,12 +35,19 @@ exports[` should be selected if isSelected=true 1`] = ` class="mx_Flex mx_RoomListItemView_content" style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;" > - - room1 - +
+ room1 +
+
+
@@ -82,12 +89,19 @@ exports[` should display notification decoration 1`] = ` class="mx_Flex mx_RoomListItemView_content" style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;" > - - room1 - +
+ room1 +
+
+
+ + + +`; + +exports[` should render a room item with a message preview 1`] = ` + + diff --git a/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomListPanel-test.tsx.snap b/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomListPanel-test.tsx.snap index 8a409a8f6c..9bb7061ba6 100644 --- a/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomListPanel-test.tsx.snap +++ b/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomListPanel-test.tsx.snap @@ -113,6 +113,43 @@ exports[` should not render the RoomListSearch component when U +
+ +
should render the RoomListSearch component when UICom +
+ +
should render 'room options' button 1`] = ` + +
+ +
+
+`; diff --git a/yarn.lock b/yarn.lock index 9bd1bc5550..e09a6a945e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3653,10 +3653,10 @@ resolved "https://registry.yarnpkg.com/@vector-im/compound-design-tokens/-/compound-design-tokens-4.0.2.tgz#27363d26446eaa21880ab126fa51fec112e6fd86" integrity sha512-y13bhPyJ5OzbGRl21F6+Y2adrjyK+mu67yKTx+o8MfmIpJzMSn4KkHZtcujMquWSh0e5ZAufsnk4VYvxbSpr1A== -"@vector-im/compound-web@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@vector-im/compound-web/-/compound-web-7.10.1.tgz#9aa7fc93550b4b064484fa30226439b2d07bb35e" - integrity sha512-3tVIPCNxXCrMz6TqJc5GiOndPC7bjCRdYIcSKIb7T3B0gVo81aAD2wWL5xSb33yDbXc/tdlKCiav57eQB8dRsQ== +"@vector-im/compound-web@^7.10.2": + version "7.10.2" + resolved "https://registry.yarnpkg.com/@vector-im/compound-web/-/compound-web-7.10.2.tgz#2f62c6ab83269e5b957f53bb53413a74fb65e04d" + integrity sha512-K9gA1Ah9CTJMeZTkcDFpAdVRNbu/rQEgV3PoDcEPI3e9iDds8Dhbo7EfOciPvtXCZw6Hr83lnhWDnwTFHVlahQ== dependencies: "@floating-ui/react" "^0.27.0" "@radix-ui/react-context-menu" "^2.2.1" @@ -3677,7 +3677,7 @@ resolved "https://registry.yarnpkg.com/@vector-im/matrix-wysiwyg/-/matrix-wysiwyg-2.38.3.tgz#cc54d8b3e9472bcd8e622126ba364ee31952cd8a" integrity sha512-fqo8P55Vc/t0vxpFar9RDJN5gKEjJmzrLo+O4piDbFda6VrRoqrWAtiu0Au0g6B4hRDPKIuFupk8v9Ja7q8Hvg== dependencies: - "@vector-im/matrix-wysiwyg-wasm" "link:../../../.cache/yarn/v6/npm-@vector-im-matrix-wysiwyg-2.38.3-cc54d8b3e9472bcd8e622126ba364ee31952cd8a-integrity/node_modules/bindings/wysiwyg-wasm" + "@vector-im/matrix-wysiwyg-wasm" "link:../../Library/Caches/Yarn/v6/npm-@vector-im-matrix-wysiwyg-2.38.3-cc54d8b3e9472bcd8e622126ba364ee31952cd8a-integrity/node_modules/bindings/wysiwyg-wasm" "@webassemblyjs/ast@1.14.1", "@webassemblyjs/ast@^1.14.1": version "1.14.1"