From 2a7c587f9f63494cff3e09af800fa4e3d23155b8 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Tue, 7 Jul 2026 12:08:19 +0200 Subject: [PATCH 1/3] Add back Favourites and Low Priority filter into shared components --- packages/shared-components/src/i18n/strings/en_EN.json | 2 ++ .../RoomListPrimaryFilters/RoomListPrimaryFilters.tsx | 4 ++++ .../src/room-list/RoomListPrimaryFilters/useVisibleFilters.ts | 2 +- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/shared-components/src/i18n/strings/en_EN.json b/packages/shared-components/src/i18n/strings/en_EN.json index dded80a263b..16eb73fd375 100644 --- a/packages/shared-components/src/i18n/strings/en_EN.json +++ b/packages/shared-components/src/i18n/strings/en_EN.json @@ -148,7 +148,9 @@ "expand_all_sections": "Expand all sections", "expand_filters": "Expand filter list", "filters": { + "favourite": "Favourites", "invites": "Invites", + "low_priority": "Low priority", "mentions": "Mentions", "people": "People", "rooms": "Rooms", diff --git a/packages/shared-components/src/room-list/RoomListPrimaryFilters/RoomListPrimaryFilters.tsx b/packages/shared-components/src/room-list/RoomListPrimaryFilters/RoomListPrimaryFilters.tsx index f0cde895856..5b325156b59 100644 --- a/packages/shared-components/src/room-list/RoomListPrimaryFilters/RoomListPrimaryFilters.tsx +++ b/packages/shared-components/src/room-list/RoomListPrimaryFilters/RoomListPrimaryFilters.tsx @@ -26,10 +26,14 @@ const filterIdToLabel = (filterId: FilterId): string => { return _t("room_list|filters|people"); case "rooms": return _t("room_list|filters|rooms"); + case "favourite": + return _t("room_list|filters|favourite"); case "mentions": return _t("room_list|filters|mentions"); case "invites": return _t("room_list|filters|invites"); + case "low_priority": + return _t("room_list|filters|low_priority"); } }; diff --git a/packages/shared-components/src/room-list/RoomListPrimaryFilters/useVisibleFilters.ts b/packages/shared-components/src/room-list/RoomListPrimaryFilters/useVisibleFilters.ts index 8034d5d127b..73a580b4d9e 100644 --- a/packages/shared-components/src/room-list/RoomListPrimaryFilters/useVisibleFilters.ts +++ b/packages/shared-components/src/room-list/RoomListPrimaryFilters/useVisibleFilters.ts @@ -11,7 +11,7 @@ import { useEffect, useState } from "react"; * Standard filter identifiers that can be used across implementations. * These are stable keys - the view layer maps them to translated labels. */ -export type FilterId = "unread" | "people" | "rooms" | "mentions" | "invites"; +export type FilterId = "unread" | "people" | "rooms" | "favourite" | "mentions" | "invites" | "low_priority"; /** * A hook to sort the filter IDs by active state. From 1cab08092021b088ae2972df5f5f8da934a7f11d Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Tue, 7 Jul 2026 12:14:21 +0200 Subject: [PATCH 2/3] Wireup RoomList.showSections and filters in room list vm --- .../viewmodels/room-list/RoomListViewModel.ts | 40 ++++++++++- .../room-list/RoomListViewModel-test.tsx | 69 +++++++++++++++++++ 2 files changed, 108 insertions(+), 1 deletion(-) diff --git a/apps/web/src/viewmodels/room-list/RoomListViewModel.ts b/apps/web/src/viewmodels/room-list/RoomListViewModel.ts index 14abe51017b..f30fe6a8991 100644 --- a/apps/web/src/viewmodels/room-list/RoomListViewModel.ts +++ b/apps/web/src/viewmodels/room-list/RoomListViewModel.ts @@ -64,10 +64,30 @@ const filterKeyToIdMap: Map = new Map([ [FilterEnum.UnreadFilter, "unread"], [FilterEnum.PeopleFilter, "people"], [FilterEnum.RoomsFilter, "rooms"], + [FilterEnum.FavouriteFilter, "favourite"], [FilterEnum.MentionsFilter, "mentions"], [FilterEnum.InvitesFilter, "invites"], + [FilterEnum.LowPriorityFilter, "low_priority"], ]); +/** + * Filters that are redundant when sections are enabled: Favourites and Low Priority rooms + * already have their own sections, so these filters are only shown as chips when sectioning + * is disabled (see {@link getVisibleFilterIds}). + */ +const SECTION_ONLY_FILTER_IDS: ReadonlySet = new Set(["favourite", "low_priority"]); + +/** + * Compute the filter ids to display as primary filter chips. + * When sections are enabled, the Favourites and Low Priority filters are hidden because those + * rooms are surfaced as dedicated sections instead. + */ +function getVisibleFilterIds(): FilterId[] { + const areSectionsEnabled = SettingsStore.getValue("RoomList.showSections"); + const filterIds = [...filterKeyToIdMap.values()]; + return areSectionsEnabled ? filterIds.filter((id) => !SECTION_ONLY_FILTER_IDS.has(id)) : filterIds; +} + const TAG_TO_TITLE_MAP: Record = { [DefaultTagID.Favourite]: _t("room_list|section|favourites"), [CHATS_TAG]: _t("room_list|section|chats"), @@ -144,7 +164,7 @@ export class RoomListViewModel const roomsResult = RoomListStoreV3.instance.getSortedRoomsInActiveSpace(undefined); const canCreateRoom = hasCreateRoomRights(props.client, activeSpace); - const filterIds = [...filterKeyToIdMap.values()]; + const filterIds = getVisibleFilterIds(); // By default, all sections are expanded const { sections, isFlatList } = computeSections(roomsResult, (tag) => true); @@ -214,6 +234,10 @@ export class RoomListViewModel dispatcher.unregister(dispatcherRef); }); + // Recompute the lis when setting changes + const showSectionsRef = SettingsStore.watchSetting("RoomList.showSections", null, this.onShowSectionsChange); + this.disposables.track(() => SettingsStore.unwatchSetting(showSectionsRef)); + // Track cleanup of all child view models this.disposables.track(() => { for (const viewModel of this.roomItemViewModels.values()) { @@ -249,6 +273,20 @@ export class RoomListViewModel this.updateRoomListData(); }; + /** + * Handle changes to the {@link RoomList.showSections} setting. + * Toggling sections is a rare action, so we simply reset the filters and rebuild + * the list from scratch rather than trying to reconcile the previous state. + */ + private readonly onShowSectionsChange = (): void => { + this.activeFilter = undefined; + this.clearViewModels(); + this.roomsResult = RoomListStoreV3.instance.getSortedRoomsInActiveSpace(); + this.updateRoomsMap(this.roomsResult); + this.snapshot.merge({ filterIds: getVisibleFilterIds() }); + this.updateRoomListData(); + }; + /** * Add rooms from the RoomsResult to the roomsMap for quick lookup. * This does not clear the roomsMap. diff --git a/apps/web/test/viewmodels/room-list/RoomListViewModel-test.tsx b/apps/web/test/viewmodels/room-list/RoomListViewModel-test.tsx index 2f0cdecf9e7..5213e4089fb 100644 --- a/apps/web/test/viewmodels/room-list/RoomListViewModel-test.tsx +++ b/apps/web/test/viewmodels/room-list/RoomListViewModel-test.tsx @@ -350,6 +350,75 @@ describe("RoomListViewModel", () => { "!room3:server", ]); }); + + describe("Favourites and Low Priority filters (RoomList.showSections)", () => { + function mockShowSections(showSections: boolean): void { + jest.spyOn(SettingsStore, "getValue").mockImplementation((setting) => { + if (setting === "RoomList.showSections") return showSections; + if (setting === "RoomList.CustomSectionData") return {}; + if (setting === "RoomList.OrderedCustomSections") return []; + return undefined as any; + }); + } + + it("hides the Favourites and Low Priority filters when sections are enabled", () => { + mockShowSections(true); + viewModel = new RoomListViewModel({ client: matrixClient }); + + const { filterIds } = viewModel.getSnapshot(); + expect(filterIds).not.toContain("favourite"); + expect(filterIds).not.toContain("low_priority"); + }); + + it("shows the Favourites and Low Priority filters when sections are disabled", () => { + mockShowSections(false); + viewModel = new RoomListViewModel({ client: matrixClient }); + + const { filterIds } = viewModel.getSnapshot(); + expect(filterIds).toContain("favourite"); + expect(filterIds).toContain("low_priority"); + }); + + it("recomputes the filters and clears the active filter when the setting changes", () => { + let showSections = false; + let watchCallback: () => void = () => {}; + jest.spyOn(SettingsStore, "getValue").mockImplementation((setting) => { + if (setting === "RoomList.showSections") return showSections; + if (setting === "RoomList.CustomSectionData") return {}; + if (setting === "RoomList.OrderedCustomSections") return []; + return undefined as any; + }); + jest.spyOn(SettingsStore, "watchSetting").mockImplementation((setting, _room, callback) => { + if (setting === "RoomList.showSections") watchCallback = callback as () => void; + return "watcher-id"; + }); + + viewModel = new RoomListViewModel({ client: matrixClient }); + expect(viewModel.getSnapshot().filterIds).toContain("favourite"); + + // Activate the Favourites filter + jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({ + spaceId: "home", + sections: [{ tag: CHATS_TAG, rooms: [room1] }], + filterKeys: [FilterEnum.FavouriteFilter], + }); + viewModel.onToggleFilter("favourite"); + expect(viewModel.getSnapshot().activeFilterId).toBe("favourite"); + + // Enabling sections hides the Favourites filter and resets the active filter + showSections = true; + jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({ + spaceId: "home", + sections: [{ tag: CHATS_TAG, rooms: [room1, room2, room3] }], + }); + watchCallback(); + + const snapshot = viewModel.getSnapshot(); + expect(snapshot.filterIds).not.toContain("favourite"); + expect(snapshot.filterIds).not.toContain("low_priority"); + expect(snapshot.activeFilterId).toBeUndefined(); + }); + }); }); describe("Room item view models", () => { From 952aad593293dfcd233beea1cd09f8df81550672 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Tue, 7 Jul 2026 12:14:39 +0200 Subject: [PATCH 3/3] Add e2e test for filters and showSection setting --- .../room-list-sections.spec.ts | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) 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 c2098ae0d14..b92722381e8 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 @@ -129,6 +129,46 @@ test.describe("Room list sections", () => { }); }); + test.describe("Filters when sections are disabled", () => { + test.beforeEach(async ({ app }) => { + await app.settings.setValue("RoomList.showSections", null, SettingLevel.ACCOUNT, false); + + // A favourite room, a low priority room, and a regular room. + const favouriteId = await app.client.createRoom({ name: "favourite room" }); + await app.client.evaluate(async (client, roomId) => { + await client.setRoomTag(roomId, "m.favourite"); + }, favouriteId); + const lowPrioId = await app.client.createRoom({ name: "low prio room" }); + await app.client.evaluate(async (client, roomId) => { + await client.setRoomTag(roomId, "m.lowpriority"); + }, lowPrioId); + await app.client.createRoom({ name: "regular room" }); + }); + + test("shows the Favourites and Low Priority filters and filters the flat list", async ({ page }) => { + const roomList = getRoomList(page); + const primaryFilters = getPrimaryFilters(page); + + // Expand the filter list to reveal all filters + await primaryFilters.getByRole("button", { name: "Expand filter list" }).click(); + + // The Favourites and Low Priority filters are available again when sections are disabled + await expect(primaryFilters.getByRole("option", { name: "Favourites" })).toBeVisible(); + await expect(primaryFilters.getByRole("option", { name: "Low priority" })).toBeVisible(); + + // Filtering by Favourites shows only the favourite room + await primaryFilters.getByRole("option", { name: "Favourites" }).click(); + await expect(roomList.getByRole("option", { name: "Open room favourite room" })).toBeVisible(); + await expect(roomList.getByRole("option", { name: "Open room regular room" })).not.toBeVisible(); + await expect(roomList.getByRole("option", { name: "Open room low prio room" })).not.toBeVisible(); + + // Switching to the Low Priority filter shows only the low priority room + await primaryFilters.getByRole("option", { name: "Low priority" }).click(); + await expect(roomList.getByRole("option", { name: "Open room low prio room" })).toBeVisible(); + await expect(roomList.getByRole("option", { name: "Open room favourite room" })).not.toBeVisible(); + }); + }); + test.describe("Section collapse and expand", () => { [ { section: "Favourites", roomName: "favourite room", tag: "m.favourite" },