diff --git a/packages/shared-components/src/i18n/strings/en_EN.json b/packages/shared-components/src/i18n/strings/en_EN.json index 016b996b6fb..96e0f045310 100644 --- a/packages/shared-components/src/i18n/strings/en_EN.json +++ b/packages/shared-components/src/i18n/strings/en_EN.json @@ -44,8 +44,10 @@ } }, "room_list": { + "appearance": "Appearance", "open_space_menu": "Open space menu", "room_options": "Room Options", + "show_message_previews": "Show message previews", "sort": "Sort", "sort_type": { "activity": "Activity", diff --git a/packages/shared-components/src/room-list/RoomListHeaderView/RoomListHeaderView.stories.tsx b/packages/shared-components/src/room-list/RoomListHeaderView/RoomListHeaderView.stories.tsx index 83babc68d16..18351d907f1 100644 --- a/packages/shared-components/src/room-list/RoomListHeaderView/RoomListHeaderView.stories.tsx +++ b/packages/shared-components/src/room-list/RoomListHeaderView/RoomListHeaderView.stories.tsx @@ -28,6 +28,7 @@ const RoomListHeaderViewWrapper = ({ inviteInSpace, openSpacePreferences, sort, + toggleMessagePreview, ...rest }: RoomListHeaderProps): JSX.Element => { const vm = useMockedViewModel(rest, { @@ -39,6 +40,7 @@ const RoomListHeaderViewWrapper = ({ inviteInSpace, sort, openSpacePreferences, + toggleMessagePreview, }); return ; }; @@ -57,6 +59,7 @@ export default { inviteInSpace: fn(), sort: fn(), openSpacePreferences: fn(), + toggleMessagePreview: fn(), }, parameters: { design: { diff --git a/packages/shared-components/src/room-list/RoomListHeaderView/RoomListHeaderView.tsx b/packages/shared-components/src/room-list/RoomListHeaderView/RoomListHeaderView.tsx index 59cd6909e5b..4659625555c 100644 --- a/packages/shared-components/src/room-list/RoomListHeaderView/RoomListHeaderView.tsx +++ b/packages/shared-components/src/room-list/RoomListHeaderView/RoomListHeaderView.tsx @@ -56,6 +56,10 @@ export interface RoomListHeaderViewSnapshot { * The currently active sort option. */ activeSortOption: SortOption; + /** + * Whether message previews are enabled in the room list. + */ + isMessagePreviewEnabled: boolean; } export interface RoomListHeaderViewActions { @@ -91,6 +95,10 @@ export interface RoomListHeaderViewActions { * Change the sort order of the room-list. */ sort: (option: SortOption) => void; + /** + * Toggle message preview display in the room list. + */ + toggleMessagePreview: () => void; } /** diff --git a/packages/shared-components/src/room-list/RoomListHeaderView/menu/OptionMenuView.test.tsx b/packages/shared-components/src/room-list/RoomListHeaderView/menu/OptionMenuView.test.tsx index c18c99b2d33..f067c1db3b6 100644 --- a/packages/shared-components/src/room-list/RoomListHeaderView/menu/OptionMenuView.test.tsx +++ b/packages/shared-components/src/room-list/RoomListHeaderView/menu/OptionMenuView.test.tsx @@ -77,4 +77,17 @@ describe("", () => { expect(vm.sort).toHaveBeenCalledWith("recent"); }); + + it("should toggle message preview", async () => { + const user = userEvent.setup(); + + const vm = new MockedViewModel({ ...defaultSnapshot, isMessagePreviewEnabled: true }); + render(); + + await user.click(screen.getByRole("button", { name: "Room Options" })); + expect(screen.getByRole("menuitemcheckbox", { name: "Show message previews" })).toBeChecked(); + + await user.click(screen.getByRole("menuitemcheckbox", { name: "Show message previews" })); + expect(vm.toggleMessagePreview).toHaveBeenCalled(); + }); }); diff --git a/packages/shared-components/src/room-list/RoomListHeaderView/menu/OptionMenuView.tsx b/packages/shared-components/src/room-list/RoomListHeaderView/menu/OptionMenuView.tsx index ba21222d696..bf6a5d80c2d 100644 --- a/packages/shared-components/src/room-list/RoomListHeaderView/menu/OptionMenuView.tsx +++ b/packages/shared-components/src/room-list/RoomListHeaderView/menu/OptionMenuView.tsx @@ -5,7 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ -import { IconButton, Menu, MenuTitle, RadioMenuItem } from "@vector-im/compound-web"; +import { CheckboxMenuItem, IconButton, Menu, MenuTitle, RadioMenuItem } from "@vector-im/compound-web"; import React, { type JSX, useState } from "react"; import OverflowHorizontalIcon from "@vector-im/compound-design-tokens/assets/web/icons/overflow-horizontal"; @@ -33,7 +33,7 @@ interface OptionMenuViewProps { export function OptionMenuView({ vm }: OptionMenuViewProps): JSX.Element { const { translate: _t } = useI18n(); const [open, setOpen] = useState(false); - const { activeSortOption } = useViewModel(vm); + const { activeSortOption, isMessagePreviewEnabled } = useViewModel(vm); return ( vm.sort("alphabetical")} /> + + ); } diff --git a/packages/shared-components/src/room-list/RoomListHeaderView/test-utils.ts b/packages/shared-components/src/room-list/RoomListHeaderView/test-utils.ts index 995e4fd7750..aedcf32ad6b 100644 --- a/packages/shared-components/src/room-list/RoomListHeaderView/test-utils.ts +++ b/packages/shared-components/src/room-list/RoomListHeaderView/test-utils.ts @@ -20,6 +20,7 @@ export class MockedViewModel extends MockViewModel i public inviteInSpace = jest.fn(); public sort = jest.fn(); public openSpacePreferences = jest.fn(); + public toggleMessagePreview = jest.fn(); } export const defaultSnapshot: RoomListHeaderViewSnapshot = { @@ -31,4 +32,5 @@ export const defaultSnapshot: RoomListHeaderViewSnapshot = { canInviteInSpace: true, canAccessSpaceSettings: true, activeSortOption: "recent", + isMessagePreviewEnabled: true, }; 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 5cc78eca693..ff1710091c9 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 @@ -8,6 +8,8 @@ import { type Page } from "@playwright/test"; import { expect, test } from "../../../element-web-test"; +import { type Bot } from "../../../pages/bot"; +import { type ElementAppPage } from "../../../pages/ElementAppPage"; test.describe("Room list", () => { test.use({ @@ -392,13 +394,8 @@ 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 }) => { - await app.settings.openUserSettings("Preferences"); - await page.getByRole("switch", { name: "Show message previews" }).click(); - await app.closeDialog(); - + async function checkMessagePreview(page: Page, app: ElementAppPage, bot: Bot) { const roomListView = getRoomList(page); - const roomId = await app.client.createRoom({ name: "activity" }); // focus the user menu to avoid to have hover decoration @@ -411,7 +408,30 @@ test.describe("Room list", () => { const room = roomListView.getByRole("option", { 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 a message preview when enable in settings", + { tag: "@screenshot" }, + async ({ page, app, user, bot }) => { + await app.settings.openUserSettings("Preferences"); + await page.getByRole("switch", { name: "Show message previews" }).click(); + await app.closeDialog(); + + await checkMessagePreview(page, app, bot); + }, + ); + + test( + "should render a message preview when enabled in header", + { tag: "@screenshot" }, + async ({ page, app, user, bot }) => { + await page.getByRole("button", { name: "Room Options" }).click(); + await page.getByRole("menuitemcheckbox", { name: "Show message previews" }).click(); + + await checkMessagePreview(page, app, bot); + }, + ); test("should render an activity decoration", { tag: "@screenshot" }, async ({ page, app, user, bot }) => { const roomListView = getRoomList(page); diff --git a/src/viewmodels/room-list/RoomListHeaderViewModel.ts b/src/viewmodels/room-list/RoomListHeaderViewModel.ts index d5587a5629a..6d81df100c3 100644 --- a/src/viewmodels/room-list/RoomListHeaderViewModel.ts +++ b/src/viewmodels/room-list/RoomListHeaderViewModel.ts @@ -30,12 +30,16 @@ import { createRoom, hasCreateRoomRights } from "../../components/viewmodels/roo import SettingsStore from "../../settings/SettingsStore"; import RoomListStoreV3 from "../../stores/room-list-v3/RoomListStoreV3"; import { SortingAlgorithm } from "../../stores/room-list-v3/skip-list/sorters"; +import { SettingLevel } from "../../settings/SettingLevel"; export interface Props { /** * The Matrix client instance. */ matrixClient: MatrixClient; + /** + * The space store instance. + */ spaceStore: SpaceStoreClass; } @@ -170,6 +174,12 @@ export class RoomListHeaderViewModel RoomListStoreV3.instance.resort(sortingAlgorithm); this.snapshot.merge({ activeSortOption: option }); }; + + public toggleMessagePreview = (): void => { + const isMessagePreviewEnabled = SettingsStore.getValue("RoomList.showMessagePreview"); + SettingsStore.setValue("RoomList.showMessagePreview", null, SettingLevel.DEVICE, !isMessagePreviewEnabled); + this.snapshot.merge({ isMessagePreviewEnabled }); + }; } /** @@ -182,9 +192,11 @@ function getInitialSnapshot(spaceStore: SpaceStoreClass, matrixClient: MatrixCli const sortingAlgorithm = SettingsStore.getValue("RoomList.preferredSorting"); const activeSortOption = sortingAlgorithm === SortingAlgorithm.Recency ? ("recent" as const) : ("alphabetical" as const); + const isMessagePreviewEnabled = SettingsStore.getValue("RoomList.showMessagePreview"); return { activeSortOption, + isMessagePreviewEnabled, ...computeHeaderSpaceState(spaceStore, matrixClient), }; } @@ -216,7 +228,7 @@ function getCanCreateVideoRoom(canCreateRoom: boolean): boolean { function computeHeaderSpaceState( spaceStore: SpaceStoreClass, matrixClient: MatrixClient, -): Omit { +): Omit { const activeSpace = spaceStore.activeSpaceRoom; const title = getHeaderTitle(spaceStore); diff --git a/test/viewmodels/room-list/RoomListHeaderViewModel-test.ts b/test/viewmodels/room-list/RoomListHeaderViewModel-test.ts index f9d65441302..3d6083bc366 100644 --- a/test/viewmodels/room-list/RoomListHeaderViewModel-test.ts +++ b/test/viewmodels/room-list/RoomListHeaderViewModel-test.ts @@ -148,6 +148,16 @@ describe("RoomListHeaderViewModel", () => { vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance }); expect(vm.getSnapshot().canAccessSpaceSettings).toBe(false); }); + + it("should show message preview when RoomList.showMessagePreview is enabled", () => { + jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName: string) => { + if (settingName === "RoomList.showMessagePreview") return true; + return false; + }); + + vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance }); + expect(vm.getSnapshot().isMessagePreviewEnabled).toBe(true); + }); }); describe("event listeners", () => { @@ -268,5 +278,20 @@ describe("RoomListHeaderViewModel", () => { expect(resortSpy).toHaveBeenCalledWith(expectedAlgorithm); }); + + it("should toggle message preview from enabled to disabled", () => { + jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName: string) => { + if (settingName === "RoomList.showMessagePreview") return true; + return false; + }); + const setValueSpy = jest.spyOn(SettingsStore, "setValue").mockImplementation(jest.fn()); + + vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance }); + expect(vm.getSnapshot().isMessagePreviewEnabled).toBe(true); + + vm.toggleMessagePreview(); + + expect(setValueSpy).toHaveBeenCalledWith("RoomList.showMessagePreview", null, expect.anything(), false); + }); }); });