Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
Expand Down
40 changes: 39 additions & 1 deletion apps/web/src/viewmodels/room-list/RoomListViewModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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()) {
Expand Down Expand Up @@ -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.
Expand Down
69 changes: 69 additions & 0 deletions apps/web/test/viewmodels/room-list/RoomListViewModel-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,75 @@
"!room3:server",
]);
});

describe("Favourites and Low Priority filters (RoomList.showSections)", () => {
function mockShowSections(showSections: boolean): void {

Check warning on line 355 in apps/web/test/viewmodels/room-list/RoomListViewModel-test.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Move function 'mockShowSections' to the outer scope.

See more on https://sonarcloud.io/project/issues?id=element-web&issues=AZ88Fznpa4kS-y57pM3Z&open=AZ88Fznpa4kS-y57pM3Z&pullRequest=34162
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", () => {
Expand Down
2 changes: 2 additions & 0 deletions packages/shared-components/src/i18n/strings/en_EN.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,14 @@
return _t("room_list|filters|people");
case "rooms":
return _t("room_list|filters|rooms");
case "favourite":
return _t("room_list|filters|favourite");

Check warning on line 30 in packages/shared-components/src/room-list/RoomListPrimaryFilters/RoomListPrimaryFilters.tsx

View workflow job for this annotation

GitHub Actions / Tests

Uncovered Line

Line 30 is not covered by tests
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");

Check warning on line 36 in packages/shared-components/src/room-list/RoomListPrimaryFilters/RoomListPrimaryFilters.tsx

View workflow job for this annotation

GitHub Actions / Tests

Uncovered Line

Line 36 is not covered by tests
}
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading