diff --git a/apps/web/playwright/e2e/left-panel/room-list-panel/room-list-sections.spec.ts b/apps/web/playwright/e2e/left-panel/room-list-panel/room-list-sections.spec.ts index d042371e47..c2098ae0d1 100644 --- a/apps/web/playwright/e2e/left-panel/room-list-panel/room-list-sections.spec.ts +++ b/apps/web/playwright/e2e/left-panel/room-list-panel/room-list-sections.spec.ts @@ -8,6 +8,7 @@ import { rejectToast } from "@element-hq/element-web-playwright-common"; import { expect, test } from "../../../element-web-test"; +import { SettingLevel } from "../../../../src/settings/SettingLevel"; import { assertRoomInSection, dragRoomToSection, getPrimaryFilters, getRoomList, getSectionHeader } from "./utils"; test.describe("Room list sections", () => { @@ -89,6 +90,45 @@ test.describe("Room list sections", () => { }); }); + test.describe("Show sections setting", () => { + test.beforeEach(async ({ app }) => { + // A favourite room and a regular room so that, when sections are enabled, we get + // two meaningful sections (Favourites + Chats). + const favouriteId = await app.client.createRoom({ name: "favourite room" }); + await app.client.evaluate(async (client, roomId) => { + await client.setRoomTag(roomId, "m.favourite"); + }, favouriteId); + await app.client.createRoom({ name: "regular room" }); + }); + + test("toggling RoomList.showSections switches between a sectioned and a flat list", async ({ page, app }) => { + const roomList = getRoomList(page); + + // Sections are enabled by default: section headers are visible and rooms render as treegrid rows. + await expect(getSectionHeader(page, "Favourites")).toBeVisible(); + await expect(getSectionHeader(page, "Chats")).toBeVisible(); + await expect(roomList.getByRole("row", { name: "Open room favourite room" })).toBeVisible(); + + // Disable sections + await app.settings.setValue("RoomList.showSections", null, SettingLevel.ACCOUNT, false); + + // The list becomes flat: no section headers, rooms render as listbox options. + await expect(getSectionHeader(page, "Favourites")).not.toBeVisible(); + await expect(getSectionHeader(page, "Chats")).not.toBeVisible(); + await expect(page.getByRole("listbox", { name: "Room list", exact: true })).toBeVisible(); + await expect(roomList.getByRole("option", { name: "Open room favourite room" })).toBeVisible(); + await expect(roomList.getByRole("option", { name: "Open room regular room" })).toBeVisible(); + + // Re-enable sections + await app.settings.setValue("RoomList.showSections", null, SettingLevel.ACCOUNT, true); + + // The sections reappear. + await expect(getSectionHeader(page, "Favourites")).toBeVisible(); + await expect(getSectionHeader(page, "Chats")).toBeVisible(); + await expect(roomList.getByRole("row", { name: "Open room favourite room" })).toBeVisible(); + }); + }); + test.describe("Section collapse and expand", () => { [ { section: "Favourites", roomName: "favourite room", tag: "m.favourite" }, diff --git a/apps/web/playwright/snapshots/settings/preferences-user-settings-tab.spec.ts/Preferences-user-settings-tab-should-be-rendered-properly-1-linux.png b/apps/web/playwright/snapshots/settings/preferences-user-settings-tab.spec.ts/Preferences-user-settings-tab-should-be-rendered-properly-1-linux.png index bf65dbb7b8..55d93f397f 100644 Binary files a/apps/web/playwright/snapshots/settings/preferences-user-settings-tab.spec.ts/Preferences-user-settings-tab-should-be-rendered-properly-1-linux.png and b/apps/web/playwright/snapshots/settings/preferences-user-settings-tab.spec.ts/Preferences-user-settings-tab-should-be-rendered-properly-1-linux.png differ diff --git a/apps/web/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx b/apps/web/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx index 563045274a..2cf3dfb9ee 100644 --- a/apps/web/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx +++ b/apps/web/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx @@ -272,6 +272,7 @@ export default class PreferencesUserSettingsTab extends React.Component + diff --git a/apps/web/src/i18n/strings/en_EN.json b/apps/web/src/i18n/strings/en_EN.json index f5ec7e5f67..da7ef74f66 100644 --- a/apps/web/src/i18n/strings/en_EN.json +++ b/apps/web/src/i18n/strings/en_EN.json @@ -2958,6 +2958,7 @@ "show_nsfw_content": "Show NSFW content", "show_read_receipts": "Show read receipts sent by other users", "show_redaction_placeholder": "Show a placeholder for removed messages", + "show_sections": "Show sections", "show_stickers_button": "Show stickers button", "show_typing_notifications": "Show typing notifications", "showbold": "Show all activity in the room list (dots or number of unread messages)", diff --git a/apps/web/src/settings/Settings.tsx b/apps/web/src/settings/Settings.tsx index f379bead93..80f2364a7b 100644 --- a/apps/web/src/settings/Settings.tsx +++ b/apps/web/src/settings/Settings.tsx @@ -366,6 +366,7 @@ export interface Settings { "Developer.elementCallUrl": IBaseSetting; "RoomList.CustomSectionData": IBaseSetting; "RoomList.OrderedCustomSections": IBaseSetting; + "RoomList.showSections": IBaseSetting; } export type SettingKey = keyof Settings; @@ -1225,6 +1226,11 @@ export const SETTINGS: Settings = { default: false, displayName: _td("settings|show_message_previews"), }, + "RoomList.showSections": { + supportedLevels: LEVELS_ACCOUNT_SETTINGS, + default: true, + displayName: _td("settings|show_sections"), + }, "RightPanel.phasesGlobal": { supportedLevels: [SettingLevel.DEVICE], default: null, diff --git a/apps/web/src/stores/room-list-v3/RoomListStoreV3.ts b/apps/web/src/stores/room-list-v3/RoomListStoreV3.ts index 3ccdb4b5d0..200abab9be 100644 --- a/apps/web/src/stores/room-list-v3/RoomListStoreV3.ts +++ b/apps/web/src/stores/room-list-v3/RoomListStoreV3.ts @@ -137,6 +137,8 @@ export class RoomListStoreV3Class extends AsyncStoreWithClient { SpaceStore.instance.on(UPDATE_HOME_BEHAVIOUR, () => this.onActiveSpaceChanged()); SettingsStore.watchSetting("RoomList.OrderedCustomSections", null, () => this.onOrderedCustomSectionsChange()); this.loadCustomSections(); + + SettingsStore.watchSetting("RoomList.showSections", null, () => this.scheduleEmit()); } /** @@ -172,8 +174,11 @@ export class RoomListStoreV3Class extends AsyncStoreWithClient { */ public getSortedRoomsInActiveSpace(filterKeys?: FilterKey[]): RoomsResult { const spaceId = SpaceStore.instance.activeSpace; + const areSectionsEnabled = SettingsStore.getValue("RoomList.showSections"); - const sections = this.getSections(filterKeys); + const sections = areSectionsEnabled + ? this.getSections(filterKeys) + : [{ tag: CHATS_TAG, rooms: Array.from(this.roomSkipList?.getRoomsInActiveSpace(filterKeys) ?? []) }]; return { spaceId: spaceId, diff --git a/apps/web/src/viewmodels/room-list/RoomListHeaderViewModel.ts b/apps/web/src/viewmodels/room-list/RoomListHeaderViewModel.ts index 7a9119c2dd..548aa8f03d 100644 --- a/apps/web/src/viewmodels/room-list/RoomListHeaderViewModel.ts +++ b/apps/web/src/viewmodels/room-list/RoomListHeaderViewModel.ts @@ -70,6 +70,13 @@ export class RoomListHeaderViewModel ); this.disposables.track(() => SettingsStore.unwatchSetting(settingsFeatureVideoRef)); + const settingsShowSectionsRef = SettingsStore.watchSetting( + "RoomList.showSections", + null, + this.onShowSectionsChange, + ); + this.disposables.track(() => SettingsStore.unwatchSetting(settingsShowSectionsRef)); + // Listen for space changes this.disposables.trackListener(props.spaceStore, UPDATE_SELECTED_SPACE, this.onSpaceChange); this.disposables.trackListener(props.spaceStore, UPDATE_HOME_BEHAVIOUR, this.onHomeBehaviourChange); @@ -133,6 +140,15 @@ export class RoomListHeaderViewModel }); }; + /** + * Handles show sections setting change events. + */ + private readonly onShowSectionsChange = (): void => { + this.snapshot.merge({ + areSectionsEnabled: SettingsStore.getValue("RoomList.showSections"), + }); + }; + public createChatRoom = (e: Event): void => { defaultDispatcher.fire(Action.CreateChat); PosthogTrackers.trackInteraction("WebRoomListHeaderPlusMenuCreateChatItem", e); @@ -310,6 +326,7 @@ function computeHeaderSpaceState( ): Omit { const displaySectionReleaseAnnouncement = ReleaseAnnouncementStore.instance.getReleaseAnnouncement() === "room_list_section"; + const areSectionsEnabled = SettingsStore.getValue("RoomList.showSections"); const activeSpace = spaceStore.activeSpaceRoom; const title = getHeaderTitle(spaceStore); @@ -330,5 +347,6 @@ function computeHeaderSpaceState( canInviteInSpace, canAccessSpaceSettings, displaySectionReleaseAnnouncement, + areSectionsEnabled, }; } diff --git a/apps/web/src/viewmodels/room-list/RoomListItemViewModel.ts b/apps/web/src/viewmodels/room-list/RoomListItemViewModel.ts index d4bc29a809..f7f285271a 100644 --- a/apps/web/src/viewmodels/room-list/RoomListItemViewModel.ts +++ b/apps/web/src/viewmodels/room-list/RoomListItemViewModel.ts @@ -110,6 +110,14 @@ export class RoomListItemViewModel SettingsStore.unwatchSetting(settingsWatchRef); }); + // Subscribe to settings changes for section toggle + const settingsShowSectionsRef = SettingsStore.watchSetting( + "RoomList.showSections", + null, + this.onShowSectionsChange, + ); + this.disposables.track(() => SettingsStore.unwatchSetting(settingsShowSectionsRef)); + // Subscribe to call state changes this.disposables.trackListener(CallStore.instance, CallStoreEvent.Call, this.onCallStateChanged); // If there is an active call for this room, listen to participant changes @@ -153,6 +161,11 @@ export class RoomListItemViewModel void this.loadAndSetMessagePreview(); }; + private readonly onShowSectionsChange = (): void => { + const areSectionsEnabled = SettingsStore.getValue("RoomList.showSections"); + this.snapshot.merge({ areSectionsEnabled }); + }; + /** * Handler for call participant changes. Only updates the item if the call moves between having participants and not having participants, to avoid unnecessary updates. * @param participants The current call participants @@ -321,6 +334,7 @@ export class RoomListItemViewModel // Build sections list for the "Move to section" submenu const sections: Section[] = RoomListItemViewModel.buildSections(roomTags, availableSections); + const areSectionsEnabled = SettingsStore.getValue("RoomList.showSections"); return { id: room.roomId, @@ -350,6 +364,7 @@ export class RoomListItemViewModel canMarkAsUnread, roomNotifState, sections, + areSectionsEnabled, }; } diff --git a/apps/web/src/viewmodels/room-list/RoomListViewModel.ts b/apps/web/src/viewmodels/room-list/RoomListViewModel.ts index 01f8cd92f5..14abe51017 100644 --- a/apps/web/src/viewmodels/room-list/RoomListViewModel.ts +++ b/apps/web/src/viewmodels/room-list/RoomListViewModel.ts @@ -42,6 +42,7 @@ import { RoomListSectionHeaderViewModel } from "./RoomListSectionHeaderViewModel import { getCustomSectionData, isCustomSectionTag, CHATS_TAG } from "../../stores/room-list-v3/section"; import { tagRoom } from "../../utils/room/tagRoom"; import { getSectionTagForRoom } from "../../utils/room/getSectionTagForRoom"; +import SettingsStore from "../../settings/SettingsStore"; /** * Tracks the position of the active room within a specific section. @@ -799,6 +800,10 @@ export class RoomListViewModel }; public onRoomTagged = (): void => { + const areSectionsEnabled = SettingsStore.getValue("RoomList.showSections"); + // Only show the "chat moved" toast if sections are enabled + if (!areSectionsEnabled) return; + this.showToast("chat_moved"); }; diff --git a/apps/web/test/unit-tests/components/views/settings/tabs/user/__snapshots__/PreferencesUserSettingsTab-test.tsx.snap b/apps/web/test/unit-tests/components/views/settings/tabs/user/__snapshots__/PreferencesUserSettingsTab-test.tsx.snap index 6e7aecbe86..5eab0932df 100644 --- a/apps/web/test/unit-tests/components/views/settings/tabs/user/__snapshots__/PreferencesUserSettingsTab-test.tsx.snap +++ b/apps/web/test/unit-tests/components/views/settings/tabs/user/__snapshots__/PreferencesUserSettingsTab-test.tsx.snap @@ -117,6 +117,39 @@ exports[`PreferencesUserSettingsTab should render 1`] = ` +
+
+
+ +
+
+
+
+ +
+
@@ -150,7 +183,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = ` @@ -164,7 +197,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = ` > @@ -228,7 +261,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = ` @@ -242,7 +275,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = ` > @@ -307,38 +340,6 @@ exports[`PreferencesUserSettingsTab should render 1`] = ` -
-
-
- -
-
-
-
- -
-
@@ -367,7 +368,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = ` class="_label_1o4d9_60" for="mx_SettingsFlag_MRMwbPDmfGtm" > - Always show message timestamps + Show timestamps in 12 hour format (e.g. 2:30pm)
@@ -398,6 +399,38 @@ exports[`PreferencesUserSettingsTab should render 1`] = ` + + +
+
+
+ +
+
+
+
+ @@ -452,7 +485,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = ` checked="" class="_input_udcm8_24" disabled="" - id="mx_SettingsFlag_IAu5CsiHRD7n" + id="mx_SettingsFlag_yrA2ohjWVJIP" role="switch" type="checkbox" /> @@ -466,7 +499,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = ` > @@ -491,7 +524,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = ` checked="" class="_input_udcm8_24" disabled="" - id="mx_SettingsFlag_yrA2ohjWVJIP" + id="mx_SettingsFlag_auy1OmnTidX4" role="switch" type="checkbox" /> @@ -505,7 +538,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = ` > @@ -542,39 +575,6 @@ exports[`PreferencesUserSettingsTab should render 1`] = ` class="_container_udcm8_10" > -
-
-
-
- -
-
-
-
-
- - Enable Markdown + Automatically replace plain text Emoji - - - Start messages with - - /plain - - to send without markdown. - -
- Enable Emoji suggestions while typing + Enable Markdown + + + Start messages with + + /plain + + to send without markdown. + +
- Use Ctrl + Enter to send a message + Enable Emoji suggestions while typing
@@ -702,7 +703,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = ` class="_label_1o4d9_60" for="mx_SettingsFlag_5nfv5bOEPN1s" > - Surround selected text when typing special characters + Use Ctrl + Enter to send a message @@ -716,7 +717,6 @@ exports[`PreferencesUserSettingsTab should render 1`] = ` class="_container_udcm8_10" > - Show stickers button + Surround selected text when typing special characters @@ -767,6 +767,39 @@ exports[`PreferencesUserSettingsTab should render 1`] = ` + + +
+
+
+ +
+
+
+
+ @@ -805,7 +838,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = ` @@ -819,7 +852,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = ` > @@ -837,7 +870,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = ` @@ -851,7 +884,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = ` > @@ -870,7 +903,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = ` checked="" class="_input_udcm8_24" disabled="" - id="mx_SettingsFlag_GFes1UFzOK2n" + id="mx_SettingsFlag_vfGFMldL2r2v" role="switch" type="checkbox" /> @@ -884,7 +917,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = ` > @@ -932,7 +965,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = ` @@ -946,7 +979,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = ` > @@ -963,7 +996,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = ` > @@ -977,7 +1010,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = ` > @@ -1016,7 +1049,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = ` @@ -1030,7 +1063,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = ` > @@ -1048,7 +1081,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = ` @@ -1062,7 +1095,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = ` > @@ -1089,39 +1122,6 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
-
-
-
- -
-
-
-
- -
-
@@ -1151,7 +1151,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = ` class="_label_1o4d9_60" for="mx_SettingsFlag_dXFDGgBsKXay" > - Show a placeholder for removed messages + Show typing notifications
@@ -1184,7 +1184,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = ` class="_label_1o4d9_60" for="mx_SettingsFlag_7Az0xw4Bs4Tt" > - Show read receipts sent by other users + Show a placeholder for removed messages
@@ -1217,7 +1217,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = ` class="_label_1o4d9_60" for="mx_SettingsFlag_8jmzPIlPoBCv" > - Show join/leave messages (invites/removes/bans unaffected) + Show read receipts sent by other users
@@ -1250,7 +1250,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = ` class="_label_1o4d9_60" for="mx_SettingsFlag_enFRaTjdsFou" > - Show display name changes + Show join/leave messages (invites/removes/bans unaffected) @@ -1283,7 +1283,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = ` class="_label_1o4d9_60" for="mx_SettingsFlag_bfwnd5rz4XNX" > - Show chat effects (animations when receiving e.g. confetti) + Show display name changes @@ -1316,7 +1316,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = ` class="_label_1o4d9_60" for="mx_SettingsFlag_gs5uWEzYzZrS" > - Show profile picture changes + Show chat effects (animations when receiving e.g. confetti) @@ -1349,7 +1349,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = ` class="_label_1o4d9_60" for="mx_SettingsFlag_qWg7OgID1yRR" > - Show avatars in user, room and event mentions + Show profile picture changes @@ -1382,7 +1382,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = ` class="_label_1o4d9_60" for="mx_SettingsFlag_pOPewl7rtMbV" > - Enable big emoji in chat + Show avatars in user, room and event mentions @@ -1415,7 +1415,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = ` class="_label_1o4d9_60" for="mx_SettingsFlag_cmt3PZSyNp3v" > - Jump to the bottom of the timeline when you send a message + Enable big emoji in chat @@ -1447,6 +1447,39 @@ exports[`PreferencesUserSettingsTab should render 1`] = ` + + +
+
+
+ +
+
+
+
+ @@ -1696,7 +1729,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = ` @@ -1710,7 +1743,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = ` > @@ -1750,7 +1783,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = ` checked="" class="_input_udcm8_24" disabled="" - id="mx_SettingsFlag_FLEpLCb0jpp6" + id="mx_SettingsFlag_NQFWldEwbV3q" role="switch" type="checkbox" /> @@ -1764,7 +1797,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = ` > diff --git a/apps/web/test/unit-tests/stores/room-list-v3/RoomListStoreV3-test.ts b/apps/web/test/unit-tests/stores/room-list-v3/RoomListStoreV3-test.ts index 6810db183d..73e1ec82d6 100644 --- a/apps/web/test/unit-tests/stores/room-list-v3/RoomListStoreV3-test.ts +++ b/apps/web/test/unit-tests/stores/room-list-v3/RoomListStoreV3-test.ts @@ -929,6 +929,7 @@ describe("RoomListStoreV3", () => { describe("Sections", () => { function enableSections(): void { jest.spyOn(SettingsStore, "getValue").mockImplementation((setting: string) => { + if (setting === "RoomList.showSections") return true; if (setting === "RoomList.OrderedCustomSections") return []; if (setting === "RoomList.CustomSectionData") return {}; return false; @@ -961,6 +962,55 @@ describe("RoomListStoreV3", () => { expect(result.sections[2].tag).toBe(DefaultTagID.LowPriority); }); + describe("RoomList.showSections disabled", () => { + function disableSections(): void { + jest.spyOn(SettingsStore, "getValue").mockImplementation((setting: string) => { + if (setting === "RoomList.showSections") return false; + if (setting === "RoomList.OrderedCustomSections") return []; + if (setting === "RoomList.CustomSectionData") return {}; + return false; + }); + } + + it("returns a single Chats section containing the rooms", async () => { + disableSections(); + const { rooms } = getClientAndRooms(); + + rooms[3].tags[DefaultTagID.Favourite] = {}; + rooms[7].tags[DefaultTagID.LowPriority] = {}; + + const store = new RoomListStoreV3Class(dispatcher); + await store.start(); + + const { sections } = store.getSortedRoomsInActiveSpace(); + expect(sections).toHaveLength(1); + expect(sections[0].tag).toBe(CHATS_TAG); + expect(sections[0].rooms).toContain(rooms[3]); + expect(sections[0].rooms).toContain(rooms[7]); + }); + }); + + it("emits LISTS_UPDATE_EVENT when RoomList.showSections setting changes", async () => { + enableSections(); + getClientAndRooms(); + + let settingsWatcher: () => void = () => {}; + jest.spyOn(SettingsStore, "watchSetting").mockImplementation((settingName, _roomId, callback) => { + if (settingName === "RoomList.showSections") settingsWatcher = callback as () => void; + return "watcher-id"; + }); + + const store = new RoomListStoreV3Class(dispatcher); + await store.start(); + + const listsUpdateListener = jest.fn(); + store.on(LISTS_UPDATE_EVENT, listsUpdateListener); + + settingsWatcher(); + + expect(listsUpdateListener).toHaveBeenCalled(); + }); + it.each([ { tag: DefaultTagID.Favourite, label: "Favourite" }, { tag: DefaultTagID.LowPriority, label: "LowPriority" }, @@ -1209,6 +1259,7 @@ describe("RoomListStoreV3", () => { const customTag = "element.io.section.custom"; jest.spyOn(SettingsStore, "getValue").mockImplementation((setting: string) => { + if (setting === "RoomList.showSections") return true; if (setting === "RoomList.OrderedCustomSections") return []; if (setting === "RoomList.CustomSectionData") return {}; return false; @@ -1223,6 +1274,7 @@ describe("RoomListStoreV3", () => { // Mark a room with the custom tag and update the settings rooms[0].tags = { [customTag]: { order: 0 } }; jest.spyOn(SettingsStore, "getValue").mockImplementation((setting: string) => { + if (setting === "RoomList.showSections") return true; if (setting === "RoomList.OrderedCustomSections") return [customTag]; if (setting === "RoomList.CustomSectionData") return { [customTag]: { tag: customTag, name: "Custom" } }; diff --git a/apps/web/test/viewmodels/room-list/RoomListHeaderViewModel-test.ts b/apps/web/test/viewmodels/room-list/RoomListHeaderViewModel-test.ts index 18e3860e46..290003ae32 100644 --- a/apps/web/test/viewmodels/room-list/RoomListHeaderViewModel-test.ts +++ b/apps/web/test/viewmodels/room-list/RoomListHeaderViewModel-test.ts @@ -156,6 +156,36 @@ describe("RoomListHeaderViewModel", () => { expect(vm.getSnapshot().isMessagePreviewEnabled).toBe(true); }); + it("should set areSectionsEnabled to true when RoomList.showSections is enabled", () => { + jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName: string) => { + if (settingName === "RoomList.showSections") return true; + return false; + }); + + vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance }); + expect(vm.getSnapshot().areSectionsEnabled).toBe(true); + }); + + it("should update areSectionsEnabled when RoomList.showSections setting changes", () => { + let watchCallback: () => void = () => {}; + jest.spyOn(SettingsStore, "watchSetting").mockImplementation((settingName, _roomId, callback) => { + if (settingName === "RoomList.showSections") watchCallback = callback as () => void; + return "watcher-id"; + }); + + vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance }); + expect(vm.getSnapshot().areSectionsEnabled).toBe(false); + + // Enable sections + jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName: string) => { + if (settingName === "RoomList.showSections") return true; + return false; + }); + watchCallback(); + + expect(vm.getSnapshot().areSectionsEnabled).toBe(true); + }); + it("should set displaySectionReleaseAnnouncement to true when sections feature is enabled and announcement is active", () => { jest.spyOn(ReleaseAnnouncementStore.instance, "getReleaseAnnouncement").mockReturnValue( "room_list_section", diff --git a/apps/web/test/viewmodels/room-list/RoomListItemViewModel-test.tsx b/apps/web/test/viewmodels/room-list/RoomListItemViewModel-test.tsx index f2f0c48088..3abd78c1c3 100644 --- a/apps/web/test/viewmodels/room-list/RoomListItemViewModel-test.tsx +++ b/apps/web/test/viewmodels/room-list/RoomListItemViewModel-test.tsx @@ -664,6 +664,36 @@ describe("RoomListItemViewModel", () => { expect(viewModel.getSnapshot().sections.map((s) => s.tag)).toEqual([]); }); + + it("should set areSectionsEnabled to true when RoomList.showSections is enabled", () => { + jest.spyOn(SettingsStore, "getValue").mockImplementation((setting) => { + if (setting === "RoomList.showSections") return true; + return false; + }); + + viewModel = new RoomListItemViewModel({ room, client: matrixClient }); + expect(viewModel.getSnapshot().areSectionsEnabled).toBe(true); + }); + + it("should update areSectionsEnabled when RoomList.showSections setting changes", () => { + let watchCallback: CallbackFn<"RoomList.showSections"> = () => {}; + jest.spyOn(SettingsStore, "watchSetting").mockImplementation((setting, _room, callback) => { + if (setting === "RoomList.showSections") watchCallback = callback; + return "watcher-id"; + }); + + viewModel = new RoomListItemViewModel({ room, client: matrixClient }); + expect(viewModel.getSnapshot().areSectionsEnabled).toBe(false); + + // Enable sections + jest.spyOn(SettingsStore, "getValue").mockImplementation((setting) => { + if (setting === "RoomList.showSections") return true; + return false; + }); + watchCallback("RoomList.showSections", null, null as any, null, null); + + expect(viewModel.getSnapshot().areSectionsEnabled).toBe(true); + }); }); describe("Cleanup", () => { diff --git a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListHeaderView/RoomListHeaderView.stories.tsx/no-compose-menu-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListHeaderView/RoomListHeaderView.stories.tsx/no-compose-menu-auto.png new file mode 100644 index 0000000000..f4316d21a7 Binary files /dev/null and b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListHeaderView/RoomListHeaderView.stories.tsx/no-compose-menu-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListHeaderView/RoomListHeaderView.stories.tsx/sections-disabled-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListHeaderView/RoomListHeaderView.stories.tsx/sections-disabled-auto.png new file mode 100644 index 0000000000..22066efced Binary files /dev/null and b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListHeaderView/RoomListHeaderView.stories.tsx/sections-disabled-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/room-list/VirtualizedRoomListView/RoomListItemWrapper/RoomListItemView/RoomListItemView.stories.tsx/section-disabled-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room-list/VirtualizedRoomListView/RoomListItemWrapper/RoomListItemView/RoomListItemView.stories.tsx/section-disabled-auto.png new file mode 100644 index 0000000000..c17c835dff Binary files /dev/null and b/packages/shared-components/__vis__/linux/__baselines__/room-list/VirtualizedRoomListView/RoomListItemWrapper/RoomListItemView/RoomListItemView.stories.tsx/section-disabled-auto.png differ 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 a332d9242f..fd92c92ea4 100644 --- a/packages/shared-components/src/room-list/RoomListHeaderView/RoomListHeaderView.stories.tsx +++ b/packages/shared-components/src/room-list/RoomListHeaderView/RoomListHeaderView.stories.tsx @@ -142,3 +142,17 @@ export const DisplaySectionReleaseAnnouncement: Story = { }, }, }; + +export const SectionsDisabled: Story = { + args: { + areSectionsEnabled: false, + }, +}; + +export const NoComposeMenu: Story = { + args: { + canCreateRoom: false, + canCreateVideoRoom: false, + areSectionsEnabled: false, + }, +}; diff --git a/packages/shared-components/src/room-list/RoomListHeaderView/RoomListHeaderView.tsx b/packages/shared-components/src/room-list/RoomListHeaderView/RoomListHeaderView.tsx index 6a0593dcfc..689bb7efbd 100644 --- a/packages/shared-components/src/room-list/RoomListHeaderView/RoomListHeaderView.tsx +++ b/packages/shared-components/src/room-list/RoomListHeaderView/RoomListHeaderView.tsx @@ -7,7 +7,7 @@ import React, { type JSX } from "react"; import { IconButton, H1 } from "@vector-im/compound-web"; -import { CollapseAllIcon, ExpandAllIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; +import { CollapseAllIcon, ExpandAllIcon, ChatIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; import { type ViewModel, useViewModel } from "../../core/viewmodel"; import { Flex } from "../../core/utils/Flex"; @@ -59,6 +59,10 @@ export interface RoomListHeaderViewSnapshot { * Whether message previews are enabled in the room list. */ isMessagePreviewEnabled: boolean; + /** + * Whether sections are enabled in the room list. + */ + areSectionsEnabled: boolean; /** * If "collapse", an icon to collapse all sections is shown. * If "expand", an icon to expand all sections is shown. @@ -145,7 +149,9 @@ interface RoomListHeaderViewProps { */ export function RoomListHeaderView({ vm }: Readonly): JSX.Element { const { translate: _t } = useI18n(); - const { title, displaySpaceMenu, collapseSections } = useViewModel(vm); + const { title, displaySpaceMenu, collapseSections, areSectionsEnabled, canCreateRoom, canCreateVideoRoom } = + useViewModel(vm); + const canOnlyStartChat = !areSectionsEnabled && !canCreateRoom && !canCreateVideoRoom; return ( ): J - {collapseSections && ( + {areSectionsEnabled && collapseSections && ( ): J )} )} - + {canOnlyStartChat ? ( + vm.createChatRoom(e.nativeEvent)} + tooltip={_t("action|start_chat")} + > + + + ) : ( + + )} diff --git a/packages/shared-components/src/room-list/RoomListHeaderView/default-snapshot.ts b/packages/shared-components/src/room-list/RoomListHeaderView/default-snapshot.ts index 246234e605..5bfbc44093 100644 --- a/packages/shared-components/src/room-list/RoomListHeaderView/default-snapshot.ts +++ b/packages/shared-components/src/room-list/RoomListHeaderView/default-snapshot.ts @@ -17,4 +17,5 @@ export const defaultSnapshot: RoomListHeaderViewSnapshot = { activeSortOption: "recent", isMessagePreviewEnabled: true, displaySectionReleaseAnnouncement: false, + areSectionsEnabled: true, }; diff --git a/packages/shared-components/src/room-list/RoomListHeaderView/menu/ComposeMenuView.tsx b/packages/shared-components/src/room-list/RoomListHeaderView/menu/ComposeMenuView.tsx index ec5bf89127..0292d9c9bb 100644 --- a/packages/shared-components/src/room-list/RoomListHeaderView/menu/ComposeMenuView.tsx +++ b/packages/shared-components/src/room-list/RoomListHeaderView/menu/ComposeMenuView.tsx @@ -36,7 +36,8 @@ interface ComposeMenuViewProps { export function ComposeMenuView({ vm }: ComposeMenuViewProps): JSX.Element { const { translate: _t } = useI18n(); const [open, setOpen] = useState(false); - const { canCreateRoom, canCreateVideoRoom, displaySectionReleaseAnnouncement } = useViewModel(vm); + const { canCreateRoom, canCreateVideoRoom, displaySectionReleaseAnnouncement, areSectionsEnabled } = + useViewModel(vm); // 28px button with a 20px icon const button = ( @@ -80,7 +81,9 @@ export function ComposeMenuView({ vm }: ComposeMenuViewProps): JSX.Element { hideChevron /> )} - + {areSectionsEnabled && ( + + )} ); } diff --git a/packages/shared-components/src/room-list/VirtualizedRoomListView/RoomListItemWrapper/RoomListItemView/RoomListItemMoreOptionsMenu.tsx b/packages/shared-components/src/room-list/VirtualizedRoomListView/RoomListItemWrapper/RoomListItemView/RoomListItemMoreOptionsMenu.tsx index 49704b8c63..d4958c46ca 100644 --- a/packages/shared-components/src/room-list/VirtualizedRoomListView/RoomListItemWrapper/RoomListItemView/RoomListItemMoreOptionsMenu.tsx +++ b/packages/shared-components/src/room-list/VirtualizedRoomListView/RoomListItemWrapper/RoomListItemView/RoomListItemMoreOptionsMenu.tsx @@ -131,41 +131,45 @@ export function MoreOptionContent({ vm }: MoreOptionContentProps): JSX.Element { hideChevron={true} /> )} - - } - > - {snapshot.sections.map((section) => ( - vm.onToggleSection(section.tag)} - onClick={(evt) => evt.stopPropagation()} - hideChevron={true} - aria-checked={section.isSelected} + {snapshot.areSectionsEnabled && ( + <> + + } > - {section.isSelected && ( - - )} - - ))} - {hasSections && } - - - {isInSection && ( - evt.stopPropagation()} - hideChevron={true} - /> + {snapshot.sections.map((section) => ( + vm.onToggleSection(section.tag)} + onClick={(evt) => evt.stopPropagation()} + hideChevron={true} + aria-checked={section.isSelected} + > + {section.isSelected && ( + + )} + + ))} + {hasSections && } + + + {isInSection && ( + evt.stopPropagation()} + hideChevron={true} + /> + )} + )}