Skip to content

Commit 9e0cf3b

Browse files
authored
Room list: add default sections (#32785)
* feat: add sections to RLSV3 * feat: add sections in vms * feat: add room list section labs flag * fix: wrong margin for room list item when in sections * feat: hide favourites and low priority filters * fix: crash when changing filter * feat: support sticky room in sections * test: update SC snapshot * test: update SC screenshot * test: update RLS tests * test: add tests to RoomListSectionHeaderViewModel * test: fix existing test in RoomListViewModel * test: add sections tests for RoomListViewModel * test: add e2e tests for sections * fix: incorrect selected room when expanding/collasping a section * fix: typo in `roomSkipList` * feat: use one skip list with all filters instead of one list by tag * chore: put back comment about `roomIndexInSection` * chore: add missing `readonly` * chore: add doc about possible undefined value for room item vm
1 parent aa45aa2 commit 9e0cf3b

28 files changed

Lines changed: 1298 additions & 201 deletions

File tree

Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
/*
2+
* Copyright 2025 New Vector Ltd.
3+
*
4+
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
5+
* Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
import { type Locator, type Page } from "@playwright/test";
9+
10+
import { expect, test } from "../../../element-web-test";
11+
12+
test.describe("Room list sections", () => {
13+
test.use({
14+
displayName: "Alice",
15+
labsFlags: ["feature_new_room_list", "feature_room_list_sections"],
16+
botCreateOpts: {
17+
displayName: "BotBob",
18+
autoAcceptInvites: true,
19+
},
20+
});
21+
22+
/**
23+
* Get the room list
24+
* @param page
25+
*/
26+
function getRoomList(page: Page): Locator {
27+
return page.getByTestId("room-list");
28+
}
29+
30+
/**
31+
* Get the primary filters
32+
* @param page
33+
*/
34+
function getPrimaryFilters(page: Page): Locator {
35+
return page.getByTestId("primary-filters");
36+
}
37+
38+
/**
39+
* Get a section header toggle button by section name
40+
* @param page
41+
* @param sectionName The display name of the section (e.g. "Favourites", "Chats", "Low Priority")
42+
*/
43+
function getSectionHeader(page: Page, sectionName: string): Locator {
44+
return getRoomList(page).getByRole("gridcell", { name: `Toggle ${sectionName} section` });
45+
}
46+
47+
test.beforeEach(async ({ page, app, user }) => {
48+
// The notification toast is displayed above the search section
49+
await app.closeNotificationToast();
50+
51+
// focus the user menu to avoid to have hover decoration
52+
await page.getByRole("button", { name: "User menu" }).focus();
53+
});
54+
55+
test.describe("Section rendering", () => {
56+
test.beforeEach(async ({ app, user }) => {
57+
// Create regular rooms
58+
for (let i = 0; i < 3; i++) {
59+
await app.client.createRoom({ name: `room${i}` });
60+
}
61+
});
62+
63+
test("should render sections with correct rooms in each", { tag: "@screenshot" }, async ({ page, app }) => {
64+
// Create a favourite room
65+
const favouriteId = await app.client.createRoom({ name: "favourite room" });
66+
await app.client.evaluate(async (client, roomId) => {
67+
await client.setRoomTag(roomId, "m.favourite");
68+
}, favouriteId);
69+
70+
// Create a low priority room
71+
const lowPrioId = await app.client.createRoom({ name: "low prio room" });
72+
await app.client.evaluate(async (client, roomId) => {
73+
await client.setRoomTag(roomId, "m.lowpriority");
74+
}, lowPrioId);
75+
76+
const roomList = getRoomList(page);
77+
78+
// All three section headers should be visible
79+
await expect(getSectionHeader(page, "Favourites")).toBeVisible();
80+
await expect(getSectionHeader(page, "Chats")).toBeVisible();
81+
await expect(getSectionHeader(page, "Low Priority")).toBeVisible();
82+
83+
// Ensure all rooms are visible
84+
await expect(roomList.getByRole("row", { name: "Open room favourite room" })).toBeVisible();
85+
await expect(roomList.getByRole("row", { name: "Open room low prio room" })).toBeVisible();
86+
await expect(roomList.getByRole("row", { name: "Open room room0" })).toBeVisible();
87+
88+
await expect(roomList).toMatchScreenshot("room-list-sections.png");
89+
});
90+
91+
test("should only show non-empty sections", async ({ page, app }) => {
92+
// No low priority rooms created, only regular and favourite rooms
93+
const favouriteId = await app.client.createRoom({ name: "favourite room" });
94+
await app.client.evaluate(async (client, roomId) => {
95+
await client.setRoomTag(roomId, "m.favourite");
96+
}, favouriteId);
97+
98+
// Chats and Favourites sections should still be visible
99+
await expect(getSectionHeader(page, "Chats")).toBeVisible();
100+
await expect(getSectionHeader(page, "Favourites")).toBeVisible();
101+
// Low Priority sections should not be visible
102+
await expect(getSectionHeader(page, "Low Priority")).not.toBeVisible();
103+
});
104+
105+
test("should render a flat list when there is only rooms in Chats section", async ({ page, app }) => {
106+
// All sections should not be visible
107+
await expect(getSectionHeader(page, "Chats")).not.toBeVisible();
108+
await expect(getSectionHeader(page, "Favourites")).not.toBeVisible();
109+
await expect(getSectionHeader(page, "Low Priority")).not.toBeVisible();
110+
// It should be a flat list (using listbox a11y role)
111+
await expect(page.getByRole("listbox", { name: "Room list", exact: true })).toBeVisible();
112+
await expect(getRoomList(page).getByRole("option", { name: "Open room room0" })).toBeVisible();
113+
});
114+
});
115+
116+
test.describe("Section collapse and expand", () => {
117+
[
118+
{ section: "Favourites", roomName: "favourite room", tag: "m.favourite" },
119+
{ section: "Low Priority", roomName: "low prio room", tag: "m.lowpriority" },
120+
].forEach(({ section, roomName, tag }) => {
121+
test(`should collapse and expand the ${section} section`, async ({ page, app }) => {
122+
const roomId = await app.client.createRoom({ name: roomName });
123+
if (tag) {
124+
await app.client.evaluate(
125+
async (client, { roomId, tag }) => {
126+
await client.setRoomTag(roomId, tag);
127+
},
128+
{ roomId, tag },
129+
);
130+
}
131+
132+
const roomList = getRoomList(page);
133+
const sectionHeader = getSectionHeader(page, section);
134+
135+
// The room should be visible
136+
await expect(roomList.getByRole("row", { name: `Open room ${roomName}` })).toBeVisible();
137+
138+
// Collapse the section
139+
await sectionHeader.click();
140+
141+
// The room should no longer be visible
142+
await expect(roomList.getByRole("row", { name: `Open room ${roomName}` })).not.toBeVisible();
143+
144+
// The section header should still be visible
145+
await expect(sectionHeader).toBeVisible();
146+
147+
// Expand the section again
148+
await sectionHeader.click();
149+
150+
// The room should be visible again
151+
await expect(roomList.getByRole("row", { name: `Open room ${roomName}` })).toBeVisible();
152+
});
153+
});
154+
155+
test("should render collapsed section", { tag: "@screenshot" }, async ({ page, app }) => {
156+
const favouriteId = await app.client.createRoom({ name: "favourite room" });
157+
await app.client.evaluate(async (client, roomId) => {
158+
await client.setRoomTag(roomId, "m.favourite");
159+
}, favouriteId);
160+
161+
await app.client.createRoom({ name: "regular room" });
162+
163+
const roomList = getRoomList(page);
164+
165+
// Collapse the Favourites section
166+
await getSectionHeader(page, "Favourites").click();
167+
168+
// Verify favourite room is hidden but regular room is still visible
169+
await expect(roomList.getByRole("row", { name: "Open room favourite room" })).not.toBeVisible();
170+
await expect(roomList.getByRole("row", { name: "Open room regular room" })).toBeVisible();
171+
172+
await expect(roomList).toMatchScreenshot("room-list-sections-collapsed.png");
173+
});
174+
});
175+
176+
test.describe("Rooms placement in sections", () => {
177+
test("should move a room between sections when tags change", async ({ page, app }) => {
178+
await app.client.createRoom({ name: "my room" });
179+
180+
const roomList = getRoomList(page);
181+
182+
// Flat list because there is only rooms in the Chats section
183+
let roomItem = roomList.getByRole("option", { name: "Open room my room" });
184+
await expect(roomItem).toBeVisible();
185+
186+
// Favourite the room via context menu
187+
await roomItem.click({ button: "right" });
188+
await page.getByRole("menuitemcheckbox", { name: "Favourited" }).click();
189+
190+
// The Favourites section header should now be visible and the room should be under it
191+
await expect(getSectionHeader(page, "Favourites")).toBeVisible();
192+
roomItem = roomList.getByRole("row", { name: "Open room my room" });
193+
await expect(roomItem).toBeVisible();
194+
195+
// Unfavourite the room
196+
await roomItem.hover();
197+
await roomItem.getByRole("button", { name: "More Options" }).click();
198+
await page.getByRole("menuitemcheckbox", { name: "Favourited" }).click();
199+
200+
// Mark the room as low priority via context menu
201+
roomItem = roomList.getByRole("option", { name: "Open room my room" });
202+
await roomItem.click({ button: "right" });
203+
await page.getByRole("menuitemcheckbox", { name: "Low priority" }).click();
204+
205+
// The Low Priority section header should now be visible and the room should be under it
206+
await expect(getSectionHeader(page, "Low Priority")).toBeVisible();
207+
roomItem = roomList.getByRole("row", { name: "Open room my room" });
208+
await expect(roomItem).toBeVisible();
209+
});
210+
});
211+
212+
test.describe("Sections and filters interaction", () => {
213+
test("should not show Favourite and Low Priority filters when sections are enabled", async ({ page, app }) => {
214+
const primaryFilters = getPrimaryFilters(page);
215+
216+
// Expand the filter list to see all filters
217+
const expandButton = primaryFilters.getByRole("button", { name: "Expand filter list" });
218+
await expandButton.click();
219+
220+
// Favourite and Low Priority filters should NOT be visible since sections handle them
221+
await expect(primaryFilters.getByRole("option", { name: "Favourite" })).not.toBeVisible();
222+
223+
// Other filters should still be present
224+
await expect(primaryFilters.getByRole("option", { name: "People" })).toBeVisible();
225+
await expect(primaryFilters.getByRole("option", { name: "Rooms" })).toBeVisible();
226+
await expect(primaryFilters.getByRole("option", { name: "Unread" })).toBeVisible();
227+
});
228+
229+
test("should maintain sections when a filter is applied", async ({ page, app, bot }) => {
230+
// Create a favourite room with unread messages
231+
const favouriteId = await app.client.createRoom({ name: "fav with unread" });
232+
await app.client.evaluate(async (client, roomId) => {
233+
await client.setRoomTag(roomId, "m.favourite");
234+
}, favouriteId);
235+
await app.client.inviteUser(favouriteId, bot.credentials.userId);
236+
await bot.joinRoom(favouriteId);
237+
await bot.sendMessage(favouriteId, "Hello from favourite!");
238+
239+
// Create a regular room with unread messages
240+
const regularId = await app.client.createRoom({ name: "regular with unread" });
241+
await app.client.inviteUser(regularId, bot.credentials.userId);
242+
await bot.joinRoom(regularId);
243+
await bot.sendMessage(regularId, "Hello from regular!");
244+
245+
// Create a room without unread
246+
await app.client.createRoom({ name: "no unread room" });
247+
248+
const roomList = getRoomList(page);
249+
const primaryFilters = getPrimaryFilters(page);
250+
251+
// Apply the Unread filter
252+
await primaryFilters.getByRole("option", { name: "Unread" }).click();
253+
254+
// Only rooms with unreads should be visible
255+
await expect(roomList.getByRole("row", { name: "fav with unread" })).toBeVisible();
256+
await expect(roomList.getByRole("row", { name: "regular with unread" })).toBeVisible();
257+
await expect(roomList.getByRole("row", { name: "no unread room" })).not.toBeVisible();
258+
});
259+
});
260+
});
7.12 KB
Loading
15 KB
Loading

apps/web/src/i18n/strings/en_EN.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1567,6 +1567,7 @@
15671567
"render_reaction_images_description": "Sometimes referred to as \"custom emojis\".",
15681568
"report_to_moderators": "Report to moderators",
15691569
"report_to_moderators_description": "In rooms that support moderation, the “Report” button will let you report abuse to room moderators.",
1570+
"room_list_sections": "Room list sections",
15701571
"share_history_on_invite": "Share encrypted history with new members",
15711572
"share_history_on_invite_description": "When inviting a user to an encrypted room that has history visibility set to \"shared\", share encrypted history with that user, and accept encrypted history when you are invited to such a room.",
15721573
"share_history_on_invite_warning": "This feature is EXPERIMENTAL and not all security precautions are implemented. Do not enable on production accounts.",
@@ -2164,6 +2165,11 @@
21642165
"one": "Currently removing messages in %(count)s room",
21652166
"other": "Currently removing messages in %(count)s rooms"
21662167
},
2168+
"section": {
2169+
"chats": "Chats",
2170+
"favourites": "Favourites",
2171+
"low_priority": "Low Priority"
2172+
},
21672173
"show_less": "Show less",
21682174
"show_n_more": {
21692175
"one": "Show %(count)s more",

apps/web/src/settings/Settings.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,7 @@ export interface Settings {
223223
"feature_dynamic_room_predecessors": IFeature;
224224
"feature_render_reaction_images": IFeature;
225225
"feature_new_room_list": IFeature;
226+
"feature_room_list_sections": IFeature;
226227
"feature_ask_to_join": IFeature;
227228
"feature_notifications": IFeature;
228229
"feature_msc4362_encrypted_state_events": IFeature;
@@ -695,6 +696,15 @@ export const SETTINGS: Settings = {
695696
default: true,
696697
controller: new ReloadOnChangeController(),
697698
},
699+
"feature_room_list_sections": {
700+
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG_PRIORITISED,
701+
labsGroup: LabGroup.Ui,
702+
displayName: _td("labs|room_list_sections"),
703+
description: _td("labs|under_active_development"),
704+
isFeature: true,
705+
default: false,
706+
controller: new ReloadOnChangeController(),
707+
},
698708
/**
699709
* With the transition to Compound we are moving to a base font size
700710
* of 16px. We're taking the opportunity to move away from the `baseFontSize`

0 commit comments

Comments
 (0)