From c2e5aa7adce7f023ffa076f7eba7378f24f60d8b Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Thu, 30 Apr 2026 16:32:43 +0200 Subject: [PATCH] Room list: add collapse/expand all sections (#33318) * chore: update compound design tokens * feat(sc): add collapse/expand button to room list header * feat: add new events to broadcast section state * feat(vm): add expand/collpase event to room list events * test: add e2e tests * chore: fix company name in copyright * chore: use two differant actions for collapse/expand * Update apps/web/src/viewmodels/room-list/RoomListHeaderViewModel.ts Co-authored-by: Michael Telatynski <7t3chguy@gmail.com> * test: fix existing tests --------- Co-authored-by: Michael Telatynski <7t3chguy@gmail.com> --- .../room-list-custom-sections.spec.ts | 41 +++++++ apps/web/src/dispatcher/actions.ts | 16 +++ ...ListSectionsCollapseStateChangedPayload.ts | 20 ++++ .../room-list/RoomListHeaderViewModel.ts | 22 ++++ .../viewmodels/room-list/RoomListViewModel.ts | 42 +++++++ .../room-list/RoomListHeaderViewModel-test.ts | 83 ++++++++++++++ .../room-list/RoomListViewModel-test.tsx | 104 ++++++++++++++++++ .../collapse-sections-auto.png | Bin 0 -> 19279 bytes .../src/i18n/strings/en_EN.json | 2 + .../RoomListHeaderView.stories.tsx | 9 ++ .../RoomListHeaderView.test.tsx | 9 +- .../RoomListHeaderView/RoomListHeaderView.tsx | 32 +++++- .../src/room-list/RoomListHeaderView/index.ts | 1 + .../RoomListHeaderView/test-utils.ts | 1 + pnpm-lock.yaml | 22 ++-- pnpm-workspace.yaml | 2 +- 16 files changed, 392 insertions(+), 14 deletions(-) create mode 100644 apps/web/src/dispatcher/payloads/RoomListSectionsCollapseStateChangedPayload.ts create mode 100644 packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListHeaderView/RoomListHeaderView.stories.tsx/collapse-sections-auto.png diff --git a/apps/web/playwright/e2e/left-panel/room-list-panel/room-list-custom-sections.spec.ts b/apps/web/playwright/e2e/left-panel/room-list-panel/room-list-custom-sections.spec.ts index 742b611faf..e40b1a6c0d 100644 --- a/apps/web/playwright/e2e/left-panel/room-list-panel/room-list-custom-sections.spec.ts +++ b/apps/web/playwright/e2e/left-panel/room-list-panel/room-list-custom-sections.spec.ts @@ -258,6 +258,47 @@ test.describe("Room list custom sections", () => { }); }); + test.describe("Collapse and expand all sections", () => { + test("should collapse all sections when 'Collapse all sections' button is clicked", async ({ page, app }) => { + await app.client.createRoom({ name: "my room" }); + await createCustomSection(page, "Work"); + + const roomList = getRoomList(page); + const header = getRoomListHeader(page); + + await expect(getSectionHeader(page, "Chats")).toBeVisible(); + await expect(getSectionHeader(page, "Work")).toBeVisible(); + + const collapseButton = header.getByRole("button", { name: "Collapse all sections" }); + await expect(collapseButton).toBeVisible(); + + await expect(roomList.getByRole("row", { name: "Open room my room" })).toBeVisible(); + + await collapseButton.click(); + + await expect(getSectionHeader(page, "Chats")).toHaveAttribute("aria-expanded", "false"); + await expect(getSectionHeader(page, "Work")).toHaveAttribute("aria-expanded", "false"); + }); + + test("should expand all sections when 'Expand all sections' button is clicked", async ({ page, app }) => { + await app.client.createRoom({ name: "my room" }); + await createCustomSection(page, "Work"); + + const roomList = getRoomList(page); + const header = getRoomListHeader(page); + + await expect(getSectionHeader(page, "Chats")).toBeVisible(); + + await header.getByRole("button", { name: "Collapse all sections" }).click(); + await expect(roomList.getByRole("row", { name: "Open room my room" })).not.toBeVisible(); + + await header.getByRole("button", { name: "Expand all sections" }).click(); + + await expect(getSectionHeader(page, "Chats")).toHaveAttribute("aria-expanded", "true"); + await expect(getSectionHeader(page, "Work")).toHaveAttribute("aria-expanded", "true"); + }); + }); + test.describe("Adding a room to a custom section", () => { test("should add a room to a custom section via the More Options menu", async ({ page, app }) => { await app.client.createRoom({ name: "my room" }); diff --git a/apps/web/src/dispatcher/actions.ts b/apps/web/src/dispatcher/actions.ts index 49a3ce8868..ad19d1c7d8 100644 --- a/apps/web/src/dispatcher/actions.ts +++ b/apps/web/src/dispatcher/actions.ts @@ -403,4 +403,20 @@ export enum Action { * or keyboard event). */ UserActivity = "user_activity", + + /** + * Fired to request collapsing all room list sections. + */ + RoomListCollapseAllSections = "room_list_collapse_all_sections", + + /** + * Fired to request expanding all room list sections. + */ + RoomListExpandAllSections = "room_list_expand_all_sections", + + /** + * Fired to report the collapse state of a given room list section. + * Payload: {@link RoomListSectionsCollapseStateChangedPayload} + */ + RoomListSectionsCollapseStateChanged = "room_list_sections_collapse_state_changed", } diff --git a/apps/web/src/dispatcher/payloads/RoomListSectionsCollapseStateChangedPayload.ts b/apps/web/src/dispatcher/payloads/RoomListSectionsCollapseStateChangedPayload.ts new file mode 100644 index 0000000000..d3dbe21cf4 --- /dev/null +++ b/apps/web/src/dispatcher/payloads/RoomListSectionsCollapseStateChangedPayload.ts @@ -0,0 +1,20 @@ +/* + * 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 { type CollapseSectionsOption } from "@element-hq/web-shared-components"; + +import { type ActionPayload } from "../payloads"; +import { type Action } from "../actions"; + +export interface RoomListSectionsCollapseStateChangedPayload extends ActionPayload { + action: Action.RoomListSectionsCollapseStateChanged; + /** + * The new collapse state for the room list sections. + * If undefined, the feature is disabled. + */ + collapseSections?: CollapseSectionsOption; +} diff --git a/apps/web/src/viewmodels/room-list/RoomListHeaderViewModel.ts b/apps/web/src/viewmodels/room-list/RoomListHeaderViewModel.ts index 55626cb252..2e48532078 100644 --- a/apps/web/src/viewmodels/room-list/RoomListHeaderViewModel.ts +++ b/apps/web/src/viewmodels/room-list/RoomListHeaderViewModel.ts @@ -26,6 +26,7 @@ import { showSpaceSettings, } from "../../utils/space"; import type { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload"; +import type { RoomListSectionsCollapseStateChangedPayload } from "../../dispatcher/payloads/RoomListSectionsCollapseStateChangedPayload"; import SettingsStore from "../../settings/SettingsStore"; import RoomListStoreV3 from "../../stores/room-list-v3/RoomListStoreV3"; import { SortingAlgorithm } from "../../stores/room-list-v3/skip-list/sorters"; @@ -77,6 +78,10 @@ export class RoomListHeaderViewModel if (this.activeSpace) { this.disposables.trackListener(this.activeSpace, RoomEvent.Name, this.onSpaceNameChange); } + + // Listen for section collapse state changes from RoomListViewModel + const dispatcherRef = defaultDispatcher.register(this.onDispatch); + this.disposables.track(() => defaultDispatcher.unregister(dispatcherRef)); } /** @@ -203,6 +208,23 @@ export class RoomListHeaderViewModel public createSection = (): void => { RoomListStoreV3.instance.createSection(); }; + + public collapseOrExpandSections = (): void => { + const action = + this.snapshot.current.collapseSections === "expand" + ? Action.RoomListExpandAllSections + : Action.RoomListCollapseAllSections; + defaultDispatcher.fire(action); + }; + + private readonly onDispatch = (payload: { action: string }): void => { + if (payload.action === Action.RoomListSectionsCollapseStateChanged) { + const { collapseSections } = payload as RoomListSectionsCollapseStateChangedPayload; + this.snapshot.merge({ + collapseSections: collapseSections && (collapseSections === "collapse" ? "expand" : "collapse"), + }); + } + }; } /** * Get the initial snapshot for the RoomListHeaderViewModel. diff --git a/apps/web/src/viewmodels/room-list/RoomListViewModel.ts b/apps/web/src/viewmodels/room-list/RoomListViewModel.ts index e711e340b0..6e28e61ed7 100644 --- a/apps/web/src/viewmodels/room-list/RoomListViewModel.ts +++ b/apps/web/src/viewmodels/room-list/RoomListViewModel.ts @@ -21,6 +21,7 @@ import { Action } from "../../dispatcher/actions"; import dispatcher from "../../dispatcher/dispatcher"; import { type ViewRoomDeltaPayload } from "../../dispatcher/payloads/ViewRoomDeltaPayload"; import { type ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload"; +import { type RoomListSectionsCollapseStateChangedPayload } from "../../dispatcher/payloads/RoomListSectionsCollapseStateChangedPayload"; import SpaceStore from "../../stores/spaces/SpaceStore"; import RoomListStoreV3, { CHATS_TAG, @@ -325,9 +326,24 @@ export class RoomListViewModel // Handle keyboard navigation shortcuts (Alt+ArrowUp/Down) // This was previously handled by useRoomListNavigation hook this.handleViewRoomDelta(payload as ViewRoomDeltaPayload); + } else if (payload.action === Action.RoomListCollapseAllSections) { + this.onCollapseAllSections(false); + } else if (payload.action === Action.RoomListExpandAllSections) { + this.onCollapseAllSections(true); } }; + /** + * Handles the collapse or expansion of all sections in the room list. + * @param expand - Whether to expand or collapse all sections + */ + private onCollapseAllSections(expand: boolean): void { + for (const sectionHeaderVM of this.roomSectionHeaderViewModels.values()) { + sectionHeaderVM.isExpanded = expand; + } + this.updateRoomListData(); + } + /** * Handle keyboard navigation shortcuts (Alt+ArrowUp/Down) to move between rooms. * Supports both regular navigation and unread-only navigation. @@ -581,6 +597,32 @@ export class RoomListViewModel sections: keepIfSame(previousSections, viewSections), isFlatList, }); + + this.notifyCollapseState(isFlatList); + } + + /** + * Notify the dispatcher about the current collapse state of the room list sections. + * @param isFlatList - Whether the room list is currently displayed as a flat list + */ + private notifyCollapseState(isFlatList: boolean): void { + // Hide collapse/expand all button if sections are disabled or if it's a flat list + if (!SettingsStore.getValue("feature_room_list_sections") || isFlatList) { + dispatcher.dispatch({ + action: Action.RoomListSectionsCollapseStateChanged, + collapseSections: undefined, + }); + return; + } + + // Determine if all sections are currently collapsed + const allCollapsed = this.snapshot.current.sections.every( + ({ id }) => !(this.roomSectionHeaderViewModels.get(id)?.isExpanded ?? true), + ); + dispatcher.dispatch({ + action: Action.RoomListSectionsCollapseStateChanged, + collapseSections: allCollapsed ? "collapse" : "expand", + }); } public createChatRoom = (): void => { diff --git a/apps/web/test/viewmodels/room-list/RoomListHeaderViewModel-test.ts b/apps/web/test/viewmodels/room-list/RoomListHeaderViewModel-test.ts index 53f76d884f..e4afd6e7b8 100644 --- a/apps/web/test/viewmodels/room-list/RoomListHeaderViewModel-test.ts +++ b/apps/web/test/viewmodels/room-list/RoomListHeaderViewModel-test.ts @@ -323,6 +323,89 @@ describe("RoomListHeaderViewModel", () => { expect(createSectionSpy).toHaveBeenCalled(); }); + describe("collapseOrExpandSections", () => { + it("should dispatch RoomListCollapseAllSections when collapseSections is not 'expand'", () => { + const fireSpy = jest.spyOn(defaultDispatcher, "fire"); + vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance }); + + vm.collapseOrExpandSections(); + + expect(fireSpy).toHaveBeenCalledWith(Action.RoomListCollapseAllSections); + }); + + it("should dispatch RoomListExpandAllSections when collapseSections is 'expand'", () => { + const fireSpy = jest.spyOn(defaultDispatcher, "fire"); + vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance }); + + // Drive the VM into the "expand" state by simulating all sections collapsed + defaultDispatcher.dispatch( + { + action: Action.RoomListSectionsCollapseStateChanged, + collapseSections: "collapse", + }, + true, + ); + expect(vm.getSnapshot().collapseSections).toBe("expand"); + vm.collapseOrExpandSections(); + + expect(fireSpy).toHaveBeenCalledWith(Action.RoomListExpandAllSections); + }); + }); + + describe("RoomListSectionsCollapseStateChanged handling", () => { + it("should set collapseSections to 'expand' when collapseSections is collapse", () => { + vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance }); + + defaultDispatcher.dispatch( + { + action: Action.RoomListSectionsCollapseStateChanged, + collapseSections: "collapse", + }, + true, + ); + + expect(vm.getSnapshot().collapseSections).toBe("expand"); + }); + + it("should set collapseSections to 'collapse' when collapseSections is expand", () => { + vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance }); + + defaultDispatcher.dispatch( + { + action: Action.RoomListSectionsCollapseStateChanged, + collapseSections: "expand", + }, + true, + ); + + expect(vm.getSnapshot().collapseSections).toBe("collapse"); + }); + + it("should set collapseSections to undefined when collapseSections is undefined", () => { + vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance }); + + // First drive it into a non-undefined state + defaultDispatcher.dispatch( + { + action: Action.RoomListSectionsCollapseStateChanged, + collapseSections: "collapse", + }, + true, + ); + expect(vm.getSnapshot().collapseSections).toBe("expand"); + + defaultDispatcher.dispatch( + { + action: Action.RoomListSectionsCollapseStateChanged, + collapseSections: undefined, + }, + true, + ); + + expect(vm.getSnapshot().collapseSections).toBeUndefined(); + }); + }); + it("should toggle message preview from enabled to disabled", () => { jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName: string) => { if (settingName === "RoomList.showMessagePreview") return true; diff --git a/apps/web/test/viewmodels/room-list/RoomListViewModel-test.tsx b/apps/web/test/viewmodels/room-list/RoomListViewModel-test.tsx index c0eacbdf35..c3fc431646 100644 --- a/apps/web/test/viewmodels/room-list/RoomListViewModel-test.tsx +++ b/apps/web/test/viewmodels/room-list/RoomListViewModel-test.tsx @@ -465,6 +465,20 @@ describe("RoomListViewModel", () => { }); }); + describe("notifyCollapseState", () => { + it("should dispatch collapseSections=undefined when feature_room_list_sections is disabled", () => { + viewModel = new RoomListViewModel({ client: matrixClient }); + + const dispatchSpy = jest.spyOn(dispatcher, "dispatch"); + RoomListStoreV3.instance.emit(RoomListStoreV3Event.ListsUpdate); + + expect(dispatchSpy).toHaveBeenCalledWith({ + action: Action.RoomListSectionsCollapseStateChanged, + collapseSections: undefined, + }); + }); + }); + describe("Keyboard navigation (ViewRoomDelta)", () => { beforeEach(() => { // stubClient sets up MatrixClientPeg which is needed when ViewRoom action is dispatched @@ -971,6 +985,96 @@ describe("RoomListViewModel", () => { expect(favSection!.roomIds).toEqual(["!fav1:server"]); }); + describe("Collapse/expand all sections", () => { + it("should collapse all sections when Action.RoomListCollapseAllSections is dispatched", async () => { + viewModel = new RoomListViewModel({ client: matrixClient }); + + const favHeader = viewModel.getSectionHeaderViewModel(DefaultTagID.Favourite); + const chatsHeader = viewModel.getSectionHeaderViewModel(CHATS_TAG); + expect(favHeader.isExpanded).toBe(true); + + dispatcher.dispatch({ action: Action.RoomListCollapseAllSections }); + await flushPromisesWithFakeTimers(); + + expect(favHeader.isExpanded).toBe(false); + expect(chatsHeader.isExpanded).toBe(false); + + const snapshot = viewModel.getSnapshot(); + expect(snapshot.sections.find((s) => s.id === DefaultTagID.Favourite)!.roomIds).toEqual([]); + expect(snapshot.sections.find((s) => s.id === CHATS_TAG)!.roomIds).toEqual([]); + }); + + it("should expand all sections when Action.RoomListExpandAllSections is dispatched", async () => { + viewModel = new RoomListViewModel({ client: matrixClient }); + + // Collapse first + const favHeader = viewModel.getSectionHeaderViewModel(DefaultTagID.Favourite); + favHeader.onClick(); + expect(favHeader.isExpanded).toBe(false); + + dispatcher.dispatch({ action: Action.RoomListExpandAllSections }); + await flushPromisesWithFakeTimers(); + + expect(favHeader.isExpanded).toBe(true); + const snapshot = viewModel.getSnapshot(); + expect(snapshot.sections.find((s) => s.id === DefaultTagID.Favourite)!.roomIds).toEqual([ + "!fav1:server", + "!fav2:server", + ]); + }); + }); + + describe("notifyCollapseState", () => { + it("should dispatch collapseSections=expand when all sections are expanded (default)", () => { + viewModel = new RoomListViewModel({ client: matrixClient }); + + const dispatchSpy = jest.spyOn(dispatcher, "dispatch"); + RoomListStoreV3.instance.emit(RoomListStoreV3Event.ListsUpdate); + + expect(dispatchSpy).toHaveBeenCalledWith({ + action: Action.RoomListSectionsCollapseStateChanged, + collapseSections: "expand", + }); + }); + + it("should dispatch collapseSection=collapse when all sections are collapsed", () => { + viewModel = new RoomListViewModel({ client: matrixClient }); + + // Collapse all sections + viewModel.getSectionHeaderViewModel(DefaultTagID.Favourite).isExpanded = false; + viewModel.getSectionHeaderViewModel(CHATS_TAG).isExpanded = false; + viewModel.getSectionHeaderViewModel(DefaultTagID.LowPriority).isExpanded = false; + + const dispatchSpy = jest.spyOn(dispatcher, "dispatch"); + RoomListStoreV3.instance.emit(RoomListStoreV3Event.ListsUpdate); + + expect(dispatchSpy).toHaveBeenCalledWith({ + action: Action.RoomListSectionsCollapseStateChanged, + collapseSections: "collapse", + }); + }); + + it("should dispatch collapseSection=undefined when it is a flat list", () => { + jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({ + spaceId: "home", + sections: [ + { tag: DefaultTagID.Favourite, rooms: [] }, + { tag: CHATS_TAG, rooms: [regularRoom1] }, + { tag: DefaultTagID.LowPriority, rooms: [] }, + ], + }); + viewModel = new RoomListViewModel({ client: matrixClient }); + + const dispatchSpy = jest.spyOn(dispatcher, "dispatch"); + RoomListStoreV3.instance.emit(RoomListStoreV3Event.ListsUpdate); + + expect(dispatchSpy).toHaveBeenCalledWith({ + action: Action.RoomListSectionsCollapseStateChanged, + collapseSections: undefined, + }); + }); + }); + it("should apply sticky room within the correct section", async () => { stubClient(); viewModel = new RoomListViewModel({ client: matrixClient }); diff --git a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListHeaderView/RoomListHeaderView.stories.tsx/collapse-sections-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListHeaderView/RoomListHeaderView.stories.tsx/collapse-sections-auto.png new file mode 100644 index 0000000000000000000000000000000000000000..333171f50e10c00665670a37a1349e78afdfdb95 GIT binary patch literal 19279 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≤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 literal 0 HcmV?d00001 diff --git a/packages/shared-components/src/i18n/strings/en_EN.json b/packages/shared-components/src/i18n/strings/en_EN.json index d747318b37..fe6d4e0865 100644 --- a/packages/shared-components/src/i18n/strings/en_EN.json +++ b/packages/shared-components/src/i18n/strings/en_EN.json @@ -100,6 +100,7 @@ }, "appearance": "Appearance", "chat_moved": "Chat moved", + "collapse_all_sections": "Collapse all sections", "collapse_filters": "Collapse filter list", "empty": { "no_chats": "No chats yet", @@ -118,6 +119,7 @@ "show_activity": "See all activity", "show_chats": "Show all chats" }, + "expand_all_sections": "Expand all sections", "expand_filters": "Expand filter list", "filters": { "favourite": "Favourites", 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 83551bc21b..d622503c1d 100644 --- a/packages/shared-components/src/room-list/RoomListHeaderView/RoomListHeaderView.stories.tsx +++ b/packages/shared-components/src/room-list/RoomListHeaderView/RoomListHeaderView.stories.tsx @@ -31,6 +31,7 @@ const RoomListHeaderViewWrapperImpl = ({ sort, toggleMessagePreview, createSection, + collapseOrExpandSections, ...rest }: RoomListHeaderProps): JSX.Element => { const vm = useMockedViewModel(rest, { @@ -44,6 +45,7 @@ const RoomListHeaderViewWrapperImpl = ({ openSpacePreferences, toggleMessagePreview, createSection, + collapseOrExpandSections, }); return ; }; @@ -65,6 +67,7 @@ const meta = { openSpacePreferences: fn(), toggleMessagePreview: fn(), createSection: fn(), + collapseOrExpandSections: fn(), }, parameters: { design: { @@ -109,3 +112,9 @@ export const PlusIcon: Story = { useComposeIcon: false, }, }; + +export const CollapseSections: Story = { + args: { + collapseSections: "collapse", + }, +}; diff --git a/packages/shared-components/src/room-list/RoomListHeaderView/RoomListHeaderView.test.tsx b/packages/shared-components/src/room-list/RoomListHeaderView/RoomListHeaderView.test.tsx index 48904171cd..d6ce1e9e3f 100644 --- a/packages/shared-components/src/room-list/RoomListHeaderView/RoomListHeaderView.test.tsx +++ b/packages/shared-components/src/room-list/RoomListHeaderView/RoomListHeaderView.test.tsx @@ -12,7 +12,7 @@ import React from "react"; import * as stories from "./RoomListHeaderView.stories"; -const { Default, NoComposeMenu, NoSpaceMenu } = composeStories(stories); +const { Default, NoComposeMenu, NoSpaceMenu, CollapseSections } = composeStories(stories); describe("RoomListHeaderView", () => { it("renders the default state", () => { @@ -29,4 +29,11 @@ describe("RoomListHeaderView", () => { const { container } = render(); expect(container).toMatchSnapshot(); }); + + it("should bind the collapse all sections action", () => { + const { getByRole } = render(); + const collapseButton = getByRole("button", { name: "Collapse all sections" }); + collapseButton.click(); + expect(CollapseSections.args?.collapseOrExpandSections).toHaveBeenCalled(); + }); }); diff --git a/packages/shared-components/src/room-list/RoomListHeaderView/RoomListHeaderView.tsx b/packages/shared-components/src/room-list/RoomListHeaderView/RoomListHeaderView.tsx index 05254899ea..d68fd25b0e 100644 --- a/packages/shared-components/src/room-list/RoomListHeaderView/RoomListHeaderView.tsx +++ b/packages/shared-components/src/room-list/RoomListHeaderView/RoomListHeaderView.tsx @@ -9,6 +9,7 @@ import React, { type JSX } from "react"; import { IconButton, H1 } from "@vector-im/compound-web"; import ComposeIcon from "@vector-im/compound-design-tokens/assets/web/icons/compose"; import PlusIcon from "@vector-im/compound-design-tokens/assets/web/icons/plus"; +import CollapseAllIcon from "@vector-im/compound-design-tokens/assets/web/icons/collapse-all"; import { type ViewModel, useViewModel } from "../../core/viewmodel"; import { Flex } from "../../core/utils/Flex"; @@ -21,6 +22,11 @@ import styles from "./RoomListHeaderView.module.css"; */ export type SortOption = "recent" | "alphabetical" | "unread-first"; +/** + * The available options for collapsing sections in the room list. + */ +export type CollapseSectionsOption = "collapse" | "expand"; + export interface RoomListHeaderViewSnapshot { /** * The title of the room list @@ -68,6 +74,12 @@ export interface RoomListHeaderViewSnapshot { * Whether to use the compose icon instead of the create icon. */ useComposeIcon: boolean; + /** + * If "collapse", an icon to collapse all sections is shown. + * If "expand", an icon to expand all sections is shown. + * If undefined, no icon are shown. + */ + collapseSections?: CollapseSectionsOption; } export interface RoomListHeaderViewActions { @@ -111,6 +123,10 @@ export interface RoomListHeaderViewActions { * Create a new section in the room list. */ createSection: () => void; + /** + * Collapse or expand all sections in the room list depending on the current state. + */ + collapseOrExpandSections: () => void; } /** @@ -136,7 +152,7 @@ interface RoomListHeaderViewProps { */ export function RoomListHeaderView({ vm }: Readonly): JSX.Element { const { translate: _t } = useI18n(); - const { title, displaySpaceMenu, displayComposeMenu, useComposeIcon } = useViewModel(vm); + const { title, displaySpaceMenu, displayComposeMenu, useComposeIcon, collapseSections } = useViewModel(vm); return ( ): J + {collapseSections && ( + vm.collapseOrExpandSections()} + tooltip={ + collapseSections === "collapse" + ? _t("room_list|collapse_all_sections") + : _t("room_list|expand_all_sections") + } + > + + + )} {/* If we don't display the compose menu, it means that the user can only send DM */} {displayComposeMenu ? ( diff --git a/packages/shared-components/src/room-list/RoomListHeaderView/index.ts b/packages/shared-components/src/room-list/RoomListHeaderView/index.ts index a0b6edee11..9e7bf46c2b 100644 --- a/packages/shared-components/src/room-list/RoomListHeaderView/index.ts +++ b/packages/shared-components/src/room-list/RoomListHeaderView/index.ts @@ -10,5 +10,6 @@ export type { RoomListHeaderViewSnapshot, RoomListHeaderViewActions, SortOption, + CollapseSectionsOption, } from "./RoomListHeaderView"; export { RoomListHeaderView } from "./RoomListHeaderView"; 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 37b53af6f3..70c51c45c8 100644 --- a/packages/shared-components/src/room-list/RoomListHeaderView/test-utils.ts +++ b/packages/shared-components/src/room-list/RoomListHeaderView/test-utils.ts @@ -24,6 +24,7 @@ export class MockedViewModel extends MockViewModel i public openSpacePreferences = vi.fn<() => void>(); public toggleMessagePreview = vi.fn<() => void>(); public createSection = vi.fn<() => void>(); + public collapseOrExpandSections = vi.fn<() => void>(); } export { defaultSnapshot } from "./default-snapshot"; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 92878b13f0..841534b316 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,8 +13,8 @@ catalogs: specifier: 1.59.1 version: 1.59.1 '@vector-im/compound-design-tokens': - specifier: 10.1.0 - version: 10.1.0 + specifier: 10.1.1 + version: 10.1.1 '@vector-im/compound-web': specifier: 9.2.1 version: 9.2.1 @@ -374,10 +374,10 @@ importers: version: 1.0.2 '@vector-im/compound-design-tokens': specifier: 'catalog:' - version: 10.1.0(@types/react@19.2.14)(react@19.2.5) + version: 10.1.1(@types/react@19.2.14)(react@19.2.5) '@vector-im/compound-web': specifier: 'catalog:' - version: 9.2.1(@fontsource/inconsolata@5.2.8)(@fontsource/inter@5.2.8)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(@vector-im/compound-design-tokens@10.1.0(@types/react@19.2.14)(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + version: 9.2.1(@fontsource/inconsolata@5.2.8)(@fontsource/inter@5.2.8)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(@vector-im/compound-design-tokens@10.1.1(@types/react@19.2.14)(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@vector-im/matrix-wysiwyg': specifier: 2.40.0 version: 2.40.0(patch_hash=7bdf6150f2905bc2f055a6bcaa7b9d78fa7ffde82e800bcc454ac7b0096bd65e)(react@19.2.5) @@ -1043,7 +1043,7 @@ importers: version: 1.16.0 '@vector-im/compound-design-tokens': specifier: 'catalog:' - version: 10.1.0(@types/react@19.2.14)(react@19.2.5) + version: 10.1.1(@types/react@19.2.14)(react@19.2.5) classnames: specifier: ^2.5.1 version: 2.5.1 @@ -1158,7 +1158,7 @@ importers: version: 8.58.2(eslint@8.57.1)(typescript@6.0.3) '@vector-im/compound-web': specifier: 'catalog:' - version: 9.2.1(@fontsource/inconsolata@5.2.8)(@fontsource/inter@5.2.8)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(@vector-im/compound-design-tokens@10.1.0(@types/react@19.2.14)(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + version: 9.2.1(@fontsource/inconsolata@5.2.8)(@fontsource/inter@5.2.8)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(@vector-im/compound-design-tokens@10.1.1(@types/react@19.2.14)(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@vitest/browser-playwright': specifier: ^4.0.17 version: 4.1.5(playwright@1.59.1)(vite@8.0.10(@types/node@18.19.130)(esbuild@0.27.4)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.10))(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.5) @@ -5904,8 +5904,8 @@ packages: '@upsetjs/venn.js@2.0.0': resolution: {integrity: sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==} - '@vector-im/compound-design-tokens@10.1.0': - resolution: {integrity: sha512-o+7DGx+NygpT2NPE1Jo//7NZDuyjzRH06eRchS0ZlkJicKx/impEmShmHU/XiE4P84BIFOo9eZ1Ws+rAym6Tuw==} + '@vector-im/compound-design-tokens@10.1.1': + resolution: {integrity: sha512-f2rdTilbPeOjrX7Mh9iTPcp5VergY7JLLWzKVjwMvpT0wtoFKwn59D1hwX2QInpiG70QTCxEdQFYLxQKvJQ74Q==} peerDependencies: '@types/react': ^19.2.10 react: ^17 || ^18 || ^19.0.0 @@ -18677,12 +18677,12 @@ snapshots: d3-selection: 3.0.0 d3-transition: 3.0.1(d3-selection@3.0.0) - '@vector-im/compound-design-tokens@10.1.0(@types/react@19.2.14)(react@19.2.5)': + '@vector-im/compound-design-tokens@10.1.1(@types/react@19.2.14)(react@19.2.5)': optionalDependencies: '@types/react': 19.2.14 react: 19.2.5 - '@vector-im/compound-web@9.2.1(@fontsource/inconsolata@5.2.8)(@fontsource/inter@5.2.8)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(@vector-im/compound-design-tokens@10.1.0(@types/react@19.2.14)(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + '@vector-im/compound-web@9.2.1(@fontsource/inconsolata@5.2.8)(@fontsource/inter@5.2.8)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(@vector-im/compound-design-tokens@10.1.1(@types/react@19.2.14)(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: '@floating-ui/react': 0.27.17(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@fontsource/inconsolata': 5.2.8 @@ -18693,7 +18693,7 @@ snapshots: '@radix-ui/react-progress': 1.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@radix-ui/react-separator': 1.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@radix-ui/react-slot': 1.2.4(@types/react@19.2.14)(react@19.2.5) - '@vector-im/compound-design-tokens': 10.1.0(@types/react@19.2.14)(react@19.2.5) + '@vector-im/compound-design-tokens': 10.1.1(@types/react@19.2.14)(react@19.2.5) classnames: 2.5.1 react: 19.2.5 vaul: 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index c2e8a9c23a..6da463bce7 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -16,7 +16,7 @@ catalog: "@playwright/test": 1.59.1 "playwright-core": 1.59.1 # Compound - "@vector-im/compound-design-tokens": 10.1.0 + "@vector-im/compound-design-tokens": 10.1.1 "@vector-im/compound-web": 9.2.1 # i18n matrix-web-i18n: 3.6.0