RoomList: add back Favourites and Low Priority filters when sections are disabled (#34162)

* Add back Favourites and Low Priority filter into shared components

* Wireup RoomList.showSections and filters in room list vm

* Add e2e test for filters and showSection setting
This commit is contained in:
Florian Duros
2026-07-09 10:25:45 +00:00
committed by GitHub
parent 707d5a4353
commit f21a3f5413
6 changed files with 155 additions and 2 deletions
@@ -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" },
@@ -64,10 +64,30 @@ const filterKeyToIdMap: Map<FilterEnum, FilterId> = 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<FilterId> = new Set<FilterId>(["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<string, string> = {
[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.
@@ -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", () => {