Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
0233ae0
feat: add sections to RLSV3
florianduros Mar 12, 2026
ef3e2ec
feat: add sections in vms
florianduros Mar 12, 2026
4eaf924
feat: add room list section labs flag
florianduros Mar 19, 2026
5a53aaa
fix: wrong margin for room list item when in sections
florianduros Mar 19, 2026
a89ccc7
feat: hide favourites and low priority filters
florianduros Mar 19, 2026
6269d61
fix: crash when changing filter
florianduros Mar 20, 2026
7d20f10
feat: support sticky room in sections
florianduros Mar 20, 2026
690c501
test: update SC snapshot
florianduros Mar 23, 2026
7b9de65
test: update SC screenshot
florianduros Mar 23, 2026
39eabe1
test: update RLS tests
florianduros Mar 23, 2026
41e7b21
test: add tests to RoomListSectionHeaderViewModel
florianduros Mar 23, 2026
7c6c367
test: fix existing test in RoomListViewModel
florianduros Mar 23, 2026
2fcd24a
test: add sections tests for RoomListViewModel
florianduros Mar 23, 2026
bff189e
test: add e2e tests for sections
florianduros Mar 24, 2026
726481e
fix: incorrect selected room when expanding/collasping a section
florianduros Mar 24, 2026
ccbfbec
Merge branch 'develop' into florianduros/default-sections
florianduros Mar 30, 2026
a85bb86
fix: typo in `roomSkipList`
florianduros Mar 30, 2026
aa76629
feat: use one skip list with all filters instead of one list by tag
florianduros Mar 30, 2026
9448318
chore: put back comment about `roomIndexInSection`
florianduros Mar 30, 2026
d76160f
chore: add missing `readonly`
florianduros Mar 30, 2026
489f257
chore: add doc about possible undefined value for room item vm
florianduros Mar 30, 2026
3fcf85f
Merge branch 'develop' into florianduros/default-sections
florianduros Mar 31, 2026
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
@@ -0,0 +1,260 @@
/*
* Copyright 2025 New Vector 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 Locator, type Page } from "@playwright/test";

import { expect, test } from "../../../element-web-test";

test.describe("Room list sections", () => {
test.use({
displayName: "Alice",
labsFlags: ["feature_new_room_list", "feature_room_list_sections"],
botCreateOpts: {
displayName: "BotBob",
autoAcceptInvites: true,
},
});

/**
* Get the room list
* @param page
*/
function getRoomList(page: Page): Locator {

Check warning on line 26 in apps/web/playwright/e2e/left-panel/room-list-panel/room-list-sections.spec.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Move function 'getRoomList' to the outer scope.

See more on https://sonarcloud.io/project/issues?id=element-web&issues=AZ0-XfPlS9MLcJZZ_peV&open=AZ0-XfPlS9MLcJZZ_peV&pullRequest=32785
return page.getByTestId("room-list");
}

/**
* Get the primary filters
* @param page
*/
function getPrimaryFilters(page: Page): Locator {

Check warning on line 34 in apps/web/playwright/e2e/left-panel/room-list-panel/room-list-sections.spec.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Move function 'getPrimaryFilters' to the outer scope.

See more on https://sonarcloud.io/project/issues?id=element-web&issues=AZ0-XfPlS9MLcJZZ_peW&open=AZ0-XfPlS9MLcJZZ_peW&pullRequest=32785
return page.getByTestId("primary-filters");
}

/**
* Get a section header toggle button by section name
* @param page
* @param sectionName The display name of the section (e.g. "Favourites", "Chats", "Low Priority")
*/
function getSectionHeader(page: Page, sectionName: string): Locator {
return getRoomList(page).getByRole("gridcell", { name: `Toggle ${sectionName} section` });
}

test.beforeEach(async ({ page, app, user }) => {
// The notification toast is displayed above the search section
await app.closeNotificationToast();

// focus the user menu to avoid to have hover decoration
await page.getByRole("button", { name: "User menu" }).focus();
});

test.describe("Section rendering", () => {
test.beforeEach(async ({ app, user }) => {
// Create regular rooms
for (let i = 0; i < 3; i++) {
await app.client.createRoom({ name: `room${i}` });
}
});

test("should render sections with correct rooms in each", { tag: "@screenshot" }, async ({ page, app }) => {
// Create a favourite room
const favouriteId = await app.client.createRoom({ name: "favourite room" });
await app.client.evaluate(async (client, roomId) => {
await client.setRoomTag(roomId, "m.favourite");
}, favouriteId);

// Create a low priority room
const lowPrioId = await app.client.createRoom({ name: "low prio room" });
await app.client.evaluate(async (client, roomId) => {
await client.setRoomTag(roomId, "m.lowpriority");
}, lowPrioId);

const roomList = getRoomList(page);

// All three section headers should be visible
await expect(getSectionHeader(page, "Favourites")).toBeVisible();
await expect(getSectionHeader(page, "Chats")).toBeVisible();
await expect(getSectionHeader(page, "Low Priority")).toBeVisible();

// Ensure all rooms are visible
await expect(roomList.getByRole("row", { name: "Open room favourite room" })).toBeVisible();
await expect(roomList.getByRole("row", { name: "Open room low prio room" })).toBeVisible();
await expect(roomList.getByRole("row", { name: "Open room room0" })).toBeVisible();

await expect(roomList).toMatchScreenshot("room-list-sections.png");
});

test("should only show non-empty sections", async ({ page, app }) => {
// No low priority rooms created, only regular and favourite rooms
const favouriteId = await app.client.createRoom({ name: "favourite room" });
await app.client.evaluate(async (client, roomId) => {
await client.setRoomTag(roomId, "m.favourite");
}, favouriteId);

// Chats and Favourites sections should still be visible
await expect(getSectionHeader(page, "Chats")).toBeVisible();
await expect(getSectionHeader(page, "Favourites")).toBeVisible();
// Low Priority sections should not be visible
await expect(getSectionHeader(page, "Low Priority")).not.toBeVisible();
});

test("should render a flat list when there is only rooms in Chats section", async ({ page, app }) => {
// All sections should not be visible
await expect(getSectionHeader(page, "Chats")).not.toBeVisible();
await expect(getSectionHeader(page, "Favourites")).not.toBeVisible();
await expect(getSectionHeader(page, "Low Priority")).not.toBeVisible();
// It should be a flat list (using listbox a11y role)
await expect(page.getByRole("listbox", { name: "Room list", exact: true })).toBeVisible();
await expect(getRoomList(page).getByRole("option", { name: "Open room room0" })).toBeVisible();
});
});

test.describe("Section collapse and expand", () => {
[
{ section: "Favourites", roomName: "favourite room", tag: "m.favourite" },
{ section: "Low Priority", roomName: "low prio room", tag: "m.lowpriority" },
].forEach(({ section, roomName, tag }) => {
test(`should collapse and expand the ${section} section`, async ({ page, app }) => {
const roomId = await app.client.createRoom({ name: roomName });
if (tag) {
await app.client.evaluate(
async (client, { roomId, tag }) => {
await client.setRoomTag(roomId, tag);
},
{ roomId, tag },
);
}

const roomList = getRoomList(page);
const sectionHeader = getSectionHeader(page, section);

// The room should be visible
await expect(roomList.getByRole("row", { name: `Open room ${roomName}` })).toBeVisible();

// Collapse the section
await sectionHeader.click();

// The room should no longer be visible
await expect(roomList.getByRole("row", { name: `Open room ${roomName}` })).not.toBeVisible();

// The section header should still be visible
await expect(sectionHeader).toBeVisible();

// Expand the section again
await sectionHeader.click();

// The room should be visible again
await expect(roomList.getByRole("row", { name: `Open room ${roomName}` })).toBeVisible();
});
});

test("should render collapsed section", { tag: "@screenshot" }, async ({ page, app }) => {
const favouriteId = await app.client.createRoom({ name: "favourite room" });
await app.client.evaluate(async (client, roomId) => {
await client.setRoomTag(roomId, "m.favourite");
}, favouriteId);

await app.client.createRoom({ name: "regular room" });

const roomList = getRoomList(page);

// Collapse the Favourites section
await getSectionHeader(page, "Favourites").click();

// Verify favourite room is hidden but regular room is still visible
await expect(roomList.getByRole("row", { name: "Open room favourite room" })).not.toBeVisible();
await expect(roomList.getByRole("row", { name: "Open room regular room" })).toBeVisible();

await expect(roomList).toMatchScreenshot("room-list-sections-collapsed.png");
});
});

test.describe("Rooms placement in sections", () => {
test("should move a room between sections when tags change", async ({ page, app }) => {
await app.client.createRoom({ name: "my room" });

const roomList = getRoomList(page);

// Flat list because there is only rooms in the Chats section
let roomItem = roomList.getByRole("option", { name: "Open room my room" });
await expect(roomItem).toBeVisible();

// Favourite the room via context menu
await roomItem.click({ button: "right" });
await page.getByRole("menuitemcheckbox", { name: "Favourited" }).click();

// The Favourites section header should now be visible and the room should be under it
await expect(getSectionHeader(page, "Favourites")).toBeVisible();
roomItem = roomList.getByRole("row", { name: "Open room my room" });
await expect(roomItem).toBeVisible();

// Unfavourite the room
await roomItem.hover();
await roomItem.getByRole("button", { name: "More Options" }).click();
await page.getByRole("menuitemcheckbox", { name: "Favourited" }).click();

// Mark the room as low priority via context menu
roomItem = roomList.getByRole("option", { name: "Open room my room" });
await roomItem.click({ button: "right" });
await page.getByRole("menuitemcheckbox", { name: "Low priority" }).click();

// The Low Priority section header should now be visible and the room should be under it
await expect(getSectionHeader(page, "Low Priority")).toBeVisible();
roomItem = roomList.getByRole("row", { name: "Open room my room" });
await expect(roomItem).toBeVisible();
});
});

test.describe("Sections and filters interaction", () => {
test("should not show Favourite and Low Priority filters when sections are enabled", async ({ page, app }) => {
const primaryFilters = getPrimaryFilters(page);

// Expand the filter list to see all filters
const expandButton = primaryFilters.getByRole("button", { name: "Expand filter list" });
await expandButton.click();

// Favourite and Low Priority filters should NOT be visible since sections handle them
await expect(primaryFilters.getByRole("option", { name: "Favourite" })).not.toBeVisible();

// Other filters should still be present
await expect(primaryFilters.getByRole("option", { name: "People" })).toBeVisible();
await expect(primaryFilters.getByRole("option", { name: "Rooms" })).toBeVisible();
await expect(primaryFilters.getByRole("option", { name: "Unread" })).toBeVisible();
});

test("should maintain sections when a filter is applied", async ({ page, app, bot }) => {
// Create a favourite room with unread messages
const favouriteId = await app.client.createRoom({ name: "fav with unread" });
await app.client.evaluate(async (client, roomId) => {
await client.setRoomTag(roomId, "m.favourite");
}, favouriteId);
await app.client.inviteUser(favouriteId, bot.credentials.userId);
await bot.joinRoom(favouriteId);
await bot.sendMessage(favouriteId, "Hello from favourite!");

// Create a regular room with unread messages
const regularId = await app.client.createRoom({ name: "regular with unread" });
await app.client.inviteUser(regularId, bot.credentials.userId);
await bot.joinRoom(regularId);
await bot.sendMessage(regularId, "Hello from regular!");

// Create a room without unread
await app.client.createRoom({ name: "no unread room" });

const roomList = getRoomList(page);
const primaryFilters = getPrimaryFilters(page);

// Apply the Unread filter
await primaryFilters.getByRole("option", { name: "Unread" }).click();

// Only rooms with unreads should be visible
await expect(roomList.getByRole("row", { name: "fav with unread" })).toBeVisible();
await expect(roomList.getByRole("row", { name: "regular with unread" })).toBeVisible();
await expect(roomList.getByRole("row", { name: "no unread room" })).not.toBeVisible();
});
});
});
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions apps/web/src/i18n/strings/en_EN.json
Original file line number Diff line number Diff line change
Expand Up @@ -1567,6 +1567,7 @@
"render_reaction_images_description": "Sometimes referred to as \"custom emojis\".",
"report_to_moderators": "Report to moderators",
"report_to_moderators_description": "In rooms that support moderation, the “Report” button will let you report abuse to room moderators.",
"room_list_sections": "Room list sections",
"share_history_on_invite": "Share encrypted history with new members",
"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.",
"share_history_on_invite_warning": "This feature is EXPERIMENTAL and not all security precautions are implemented. Do not enable on production accounts.",
Expand Down Expand Up @@ -2164,6 +2165,11 @@
"one": "Currently removing messages in %(count)s room",
"other": "Currently removing messages in %(count)s rooms"
},
"section": {
"chats": "Chats",
"favourites": "Favourites",
"low_priority": "Low Priority"
},
"show_less": "Show less",
"show_n_more": {
"one": "Show %(count)s more",
Expand Down
10 changes: 10 additions & 0 deletions apps/web/src/settings/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ export interface Settings {
"feature_dynamic_room_predecessors": IFeature;
"feature_render_reaction_images": IFeature;
"feature_new_room_list": IFeature;
"feature_room_list_sections": IFeature;
"feature_ask_to_join": IFeature;
"feature_notifications": IFeature;
"feature_msc4362_encrypted_state_events": IFeature;
Expand Down Expand Up @@ -695,6 +696,15 @@ export const SETTINGS: Settings = {
default: true,
controller: new ReloadOnChangeController(),
},
"feature_room_list_sections": {
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG_PRIORITISED,
labsGroup: LabGroup.Ui,
displayName: _td("labs|room_list_sections"),
description: _td("labs|under_active_development"),
isFeature: true,
default: false,
controller: new ReloadOnChangeController(),
},
/**
* With the transition to Compound we are moving to a base font size
* of 16px. We're taking the opportunity to move away from the `baseFontSize`
Expand Down
Loading
Loading