Room list: add new settings to enable/disable the sections (#34151)

* Add new settings to enable/disable the sections

* Add `RoomList.showSections` settings to preference user settings

* Hide sections ui in room list header

- Hide the collapse/expand button and the *new section* entry in the
menu.
- Display the *start chat* button instead of the compose menu when the
only action available is to create a DM

* Hide sections ui in context menu of room item

* Listen to show section settings in room list item vm

* Listen to show sections setting in room list header vm

* Put all the rooms inside the chat section when sections are disabled

When the chat section is the only section, the room list is in "flat
list mode". Putting all the rooms inside the chat section.

The section filters (aka custom section, favourites etc) are not passed
to the skip list.

* Update preferences settings screenshot

* Add e2e test for RoomList.showSections toggle

* Hide "Chat moved" toast when a room is tagged when RoomList.showSections is disabled
This commit is contained in:
Florian Duros
2026-07-07 08:45:37 +00:00
committed by GitHub
parent 5065683658
commit 7ff6956014
25 changed files with 478 additions and 193 deletions
@@ -8,6 +8,7 @@
import { rejectToast } from "@element-hq/element-web-playwright-common";
import { expect, test } from "../../../element-web-test";
import { SettingLevel } from "../../../../src/settings/SettingLevel";
import { assertRoomInSection, dragRoomToSection, getPrimaryFilters, getRoomList, getSectionHeader } from "./utils";
test.describe("Room list sections", () => {
@@ -89,6 +90,45 @@ test.describe("Room list sections", () => {
});
});
test.describe("Show sections setting", () => {
test.beforeEach(async ({ app }) => {
// A favourite room and a regular room so that, when sections are enabled, we get
// two meaningful sections (Favourites + Chats).
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" });
});
test("toggling RoomList.showSections switches between a sectioned and a flat list", async ({ page, app }) => {
const roomList = getRoomList(page);
// Sections are enabled by default: section headers are visible and rooms render as treegrid rows.
await expect(getSectionHeader(page, "Favourites")).toBeVisible();
await expect(getSectionHeader(page, "Chats")).toBeVisible();
await expect(roomList.getByRole("row", { name: "Open room favourite room" })).toBeVisible();
// Disable sections
await app.settings.setValue("RoomList.showSections", null, SettingLevel.ACCOUNT, false);
// The list becomes flat: no section headers, rooms render as listbox options.
await expect(getSectionHeader(page, "Favourites")).not.toBeVisible();
await expect(getSectionHeader(page, "Chats")).not.toBeVisible();
await expect(page.getByRole("listbox", { name: "Room list", exact: true })).toBeVisible();
await expect(roomList.getByRole("option", { name: "Open room favourite room" })).toBeVisible();
await expect(roomList.getByRole("option", { name: "Open room regular room" })).toBeVisible();
// Re-enable sections
await app.settings.setValue("RoomList.showSections", null, SettingLevel.ACCOUNT, true);
// The sections reappear.
await expect(getSectionHeader(page, "Favourites")).toBeVisible();
await expect(getSectionHeader(page, "Chats")).toBeVisible();
await expect(roomList.getByRole("row", { name: "Open room favourite room" })).toBeVisible();
});
});
test.describe("Section collapse and expand", () => {
[
{ section: "Favourites", roomName: "favourite room", tag: "m.favourite" },
Binary file not shown.

Before

Width:  |  Height:  |  Size: 268 KiB

After

Width:  |  Height:  |  Size: 271 KiB

@@ -272,6 +272,7 @@ export default class PreferencesUserSettingsTab extends React.Component<EmptyObj
<SettingsSubsection heading={_t("settings|preferences|room_list_heading")} formWrap>
<SettingsFlag name="RoomList.showMessagePreview" level={SettingLevel.DEVICE} />
<SettingsFlag name="RoomList.showSections" level={SettingLevel.ACCOUNT} />
</SettingsSubsection>
<SettingsSubsection heading={_t("common|spaces")} formWrap>
+1
View File
@@ -2958,6 +2958,7 @@
"show_nsfw_content": "Show NSFW content",
"show_read_receipts": "Show read receipts sent by other users",
"show_redaction_placeholder": "Show a placeholder for removed messages",
"show_sections": "Show sections",
"show_stickers_button": "Show stickers button",
"show_typing_notifications": "Show typing notifications",
"showbold": "Show all activity in the room list (dots or number of unread messages)",
+6
View File
@@ -366,6 +366,7 @@ export interface Settings {
"Developer.elementCallUrl": IBaseSetting<string>;
"RoomList.CustomSectionData": IBaseSetting<CustomSectionsData>;
"RoomList.OrderedCustomSections": IBaseSetting<ReorderableSection[]>;
"RoomList.showSections": IBaseSetting<boolean>;
}
export type SettingKey = keyof Settings;
@@ -1225,6 +1226,11 @@ export const SETTINGS: Settings = {
default: false,
displayName: _td("settings|show_message_previews"),
},
"RoomList.showSections": {
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
default: true,
displayName: _td("settings|show_sections"),
},
"RightPanel.phasesGlobal": {
supportedLevels: [SettingLevel.DEVICE],
default: null,
@@ -137,6 +137,8 @@ export class RoomListStoreV3Class extends AsyncStoreWithClient<EmptyObject> {
SpaceStore.instance.on(UPDATE_HOME_BEHAVIOUR, () => this.onActiveSpaceChanged());
SettingsStore.watchSetting("RoomList.OrderedCustomSections", null, () => this.onOrderedCustomSectionsChange());
this.loadCustomSections();
SettingsStore.watchSetting("RoomList.showSections", null, () => this.scheduleEmit());
}
/**
@@ -172,8 +174,11 @@ export class RoomListStoreV3Class extends AsyncStoreWithClient<EmptyObject> {
*/
public getSortedRoomsInActiveSpace(filterKeys?: FilterKey[]): RoomsResult {
const spaceId = SpaceStore.instance.activeSpace;
const areSectionsEnabled = SettingsStore.getValue("RoomList.showSections");
const sections = this.getSections(filterKeys);
const sections = areSectionsEnabled
? this.getSections(filterKeys)
: [{ tag: CHATS_TAG, rooms: Array.from(this.roomSkipList?.getRoomsInActiveSpace(filterKeys) ?? []) }];
return {
spaceId: spaceId,
@@ -70,6 +70,13 @@ export class RoomListHeaderViewModel
);
this.disposables.track(() => SettingsStore.unwatchSetting(settingsFeatureVideoRef));
const settingsShowSectionsRef = SettingsStore.watchSetting(
"RoomList.showSections",
null,
this.onShowSectionsChange,
);
this.disposables.track(() => SettingsStore.unwatchSetting(settingsShowSectionsRef));
// Listen for space changes
this.disposables.trackListener(props.spaceStore, UPDATE_SELECTED_SPACE, this.onSpaceChange);
this.disposables.trackListener(props.spaceStore, UPDATE_HOME_BEHAVIOUR, this.onHomeBehaviourChange);
@@ -133,6 +140,15 @@ export class RoomListHeaderViewModel
});
};
/**
* Handles show sections setting change events.
*/
private readonly onShowSectionsChange = (): void => {
this.snapshot.merge({
areSectionsEnabled: SettingsStore.getValue("RoomList.showSections"),
});
};
public createChatRoom = (e: Event): void => {
defaultDispatcher.fire(Action.CreateChat);
PosthogTrackers.trackInteraction("WebRoomListHeaderPlusMenuCreateChatItem", e);
@@ -310,6 +326,7 @@ function computeHeaderSpaceState(
): Omit<RoomListHeaderViewSnapshot, "activeSortOption" | "isMessagePreviewEnabled"> {
const displaySectionReleaseAnnouncement =
ReleaseAnnouncementStore.instance.getReleaseAnnouncement() === "room_list_section";
const areSectionsEnabled = SettingsStore.getValue("RoomList.showSections");
const activeSpace = spaceStore.activeSpaceRoom;
const title = getHeaderTitle(spaceStore);
@@ -330,5 +347,6 @@ function computeHeaderSpaceState(
canInviteInSpace,
canAccessSpaceSettings,
displaySectionReleaseAnnouncement,
areSectionsEnabled,
};
}
@@ -110,6 +110,14 @@ export class RoomListItemViewModel
SettingsStore.unwatchSetting(settingsWatchRef);
});
// Subscribe to settings changes for section toggle
const settingsShowSectionsRef = SettingsStore.watchSetting(
"RoomList.showSections",
null,
this.onShowSectionsChange,
);
this.disposables.track(() => SettingsStore.unwatchSetting(settingsShowSectionsRef));
// Subscribe to call state changes
this.disposables.trackListener(CallStore.instance, CallStoreEvent.Call, this.onCallStateChanged);
// If there is an active call for this room, listen to participant changes
@@ -153,6 +161,11 @@ export class RoomListItemViewModel
void this.loadAndSetMessagePreview();
};
private readonly onShowSectionsChange = (): void => {
const areSectionsEnabled = SettingsStore.getValue("RoomList.showSections");
this.snapshot.merge({ areSectionsEnabled });
};
/**
* Handler for call participant changes. Only updates the item if the call moves between having participants and not having participants, to avoid unnecessary updates.
* @param participants The current call participants
@@ -321,6 +334,7 @@ export class RoomListItemViewModel
// Build sections list for the "Move to section" submenu
const sections: Section[] = RoomListItemViewModel.buildSections(roomTags, availableSections);
const areSectionsEnabled = SettingsStore.getValue("RoomList.showSections");
return {
id: room.roomId,
@@ -350,6 +364,7 @@ export class RoomListItemViewModel
canMarkAsUnread,
roomNotifState,
sections,
areSectionsEnabled,
};
}
@@ -42,6 +42,7 @@ import { RoomListSectionHeaderViewModel } from "./RoomListSectionHeaderViewModel
import { getCustomSectionData, isCustomSectionTag, CHATS_TAG } from "../../stores/room-list-v3/section";
import { tagRoom } from "../../utils/room/tagRoom";
import { getSectionTagForRoom } from "../../utils/room/getSectionTagForRoom";
import SettingsStore from "../../settings/SettingsStore";
/**
* Tracks the position of the active room within a specific section.
@@ -799,6 +800,10 @@ export class RoomListViewModel
};
public onRoomTagged = (): void => {
const areSectionsEnabled = SettingsStore.getValue("RoomList.showSections");
// Only show the "chat moved" toast if sections are enabled
if (!areSectionsEnabled) return;
this.showToast("chat_moved");
};
@@ -117,6 +117,39 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
</label>
</div>
</div>
<div
class="_inline-field_1o4d9_33"
>
<div
class="_inline-field-control_1o4d9_45"
>
<div
class="_container_udcm8_10"
>
<input
checked=""
class="_input_udcm8_24"
disabled=""
id="mx_SettingsFlag_QgU2PomxwKpa"
role="switch"
type="checkbox"
/>
<div
class="_ui_udcm8_34"
/>
</div>
</div>
<div
class="_inline-field-body_1o4d9_39"
>
<label
class="_label_1o4d9_60"
for="mx_SettingsFlag_QgU2PomxwKpa"
>
Show sections
</label>
</div>
</div>
</div>
</div>
</form>
@@ -150,7 +183,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
<input
class="_input_udcm8_24"
disabled=""
id="mx_SettingsFlag_QgU2PomxwKpa"
id="mx_SettingsFlag_6hpi3YEetmBG"
role="switch"
type="checkbox"
/>
@@ -164,7 +197,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
>
<label
class="_label_1o4d9_60"
for="mx_SettingsFlag_QgU2PomxwKpa"
for="mx_SettingsFlag_6hpi3YEetmBG"
>
Show all rooms in Home
</label>
@@ -228,7 +261,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
<input
class="_input_udcm8_24"
disabled=""
id="mx_SettingsFlag_6hpi3YEetmBG"
id="mx_SettingsFlag_4yVCeEefiPqp"
role="switch"
type="checkbox"
/>
@@ -242,7 +275,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
>
<label
class="_label_1o4d9_60"
for="mx_SettingsFlag_6hpi3YEetmBG"
for="mx_SettingsFlag_4yVCeEefiPqp"
>
Use Ctrl + F to search timeline
</label>
@@ -307,38 +340,6 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
</div>
</div>
</div>
<div
class="_inline-field_1o4d9_33"
>
<div
class="_inline-field-control_1o4d9_45"
>
<div
class="_container_udcm8_10"
>
<input
class="_input_udcm8_24"
disabled=""
id="mx_SettingsFlag_4yVCeEefiPqp"
role="switch"
type="checkbox"
/>
<div
class="_ui_udcm8_34"
/>
</div>
</div>
<div
class="_inline-field-body_1o4d9_39"
>
<label
class="_label_1o4d9_60"
for="mx_SettingsFlag_4yVCeEefiPqp"
>
Show timestamps in 12 hour format (e.g. 2:30pm)
</label>
</div>
</div>
<div
class="_inline-field_1o4d9_33"
>
@@ -367,7 +368,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
class="_label_1o4d9_60"
for="mx_SettingsFlag_MRMwbPDmfGtm"
>
Always show message timestamps
Show timestamps in 12 hour format (e.g. 2:30pm)
</label>
</div>
</div>
@@ -398,6 +399,38 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
<label
class="_label_1o4d9_60"
for="mx_SettingsFlag_GQvdMWe954DV"
>
Always show message timestamps
</label>
</div>
</div>
<div
class="_inline-field_1o4d9_33"
>
<div
class="_inline-field-control_1o4d9_45"
>
<div
class="_container_udcm8_10"
>
<input
class="_input_udcm8_24"
disabled=""
id="mx_SettingsFlag_IAu5CsiHRD7n"
role="switch"
type="checkbox"
/>
<div
class="_ui_udcm8_34"
/>
</div>
</div>
<div
class="_inline-field-body_1o4d9_39"
>
<label
class="_label_1o4d9_60"
for="mx_SettingsFlag_IAu5CsiHRD7n"
>
Publish timezone on public profile
</label>
@@ -452,7 +485,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
checked=""
class="_input_udcm8_24"
disabled=""
id="mx_SettingsFlag_IAu5CsiHRD7n"
id="mx_SettingsFlag_yrA2ohjWVJIP"
role="switch"
type="checkbox"
/>
@@ -466,7 +499,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
>
<label
class="_label_1o4d9_60"
for="mx_SettingsFlag_IAu5CsiHRD7n"
for="mx_SettingsFlag_yrA2ohjWVJIP"
>
Send read receipts
</label>
@@ -491,7 +524,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
checked=""
class="_input_udcm8_24"
disabled=""
id="mx_SettingsFlag_yrA2ohjWVJIP"
id="mx_SettingsFlag_auy1OmnTidX4"
role="switch"
type="checkbox"
/>
@@ -505,7 +538,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
>
<label
class="_label_1o4d9_60"
for="mx_SettingsFlag_yrA2ohjWVJIP"
for="mx_SettingsFlag_auy1OmnTidX4"
>
Send typing notifications
</label>
@@ -542,39 +575,6 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
class="_container_udcm8_10"
>
<input
class="_input_udcm8_24"
disabled=""
id="mx_SettingsFlag_auy1OmnTidX4"
role="switch"
type="checkbox"
/>
<div
class="_ui_udcm8_34"
/>
</div>
</div>
<div
class="_inline-field-body_1o4d9_39"
>
<label
class="_label_1o4d9_60"
for="mx_SettingsFlag_auy1OmnTidX4"
>
Automatically replace plain text Emoji
</label>
</div>
</div>
<div
class="_inline-field_1o4d9_33"
>
<div
class="_inline-field-control_1o4d9_45"
>
<div
class="_container_udcm8_10"
>
<input
checked=""
class="_input_udcm8_24"
disabled=""
id="mx_SettingsFlag_ePDS0OpWwAHG"
@@ -593,20 +593,8 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
class="_label_1o4d9_60"
for="mx_SettingsFlag_ePDS0OpWwAHG"
>
Enable Markdown
Automatically replace plain text Emoji
</label>
<span
class="_message_1o4d9_86 _help-message_1o4d9_92"
id="radix-react-use-id-4"
>
<span>
Start messages with
<code>
/plain
</code>
to send without markdown.
</span>
</span>
</div>
</div>
<div
@@ -638,8 +626,20 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
class="_label_1o4d9_60"
for="mx_SettingsFlag_75JNTNkNU64r"
>
Enable Emoji suggestions while typing
Enable Markdown
</label>
<span
class="_message_1o4d9_86 _help-message_1o4d9_92"
id="radix-react-use-id-4"
>
<span>
Start messages with
<code>
/plain
</code>
to send without markdown.
</span>
</span>
</div>
</div>
<div
@@ -652,6 +652,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
class="_container_udcm8_10"
>
<input
checked=""
class="_input_udcm8_24"
disabled=""
id="mx_SettingsFlag_aTLcRsQRlYy7"
@@ -670,7 +671,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
class="_label_1o4d9_60"
for="mx_SettingsFlag_aTLcRsQRlYy7"
>
Use Ctrl + Enter to send a message
Enable Emoji suggestions while typing
</label>
</div>
</div>
@@ -702,7 +703,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
class="_label_1o4d9_60"
for="mx_SettingsFlag_5nfv5bOEPN1s"
>
Surround selected text when typing special characters
Use Ctrl + Enter to send a message
</label>
</div>
</div>
@@ -716,7 +717,6 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
class="_container_udcm8_10"
>
<input
checked=""
class="_input_udcm8_24"
disabled=""
id="mx_SettingsFlag_u1JYVtOyR5kb"
@@ -735,7 +735,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
class="_label_1o4d9_60"
for="mx_SettingsFlag_u1JYVtOyR5kb"
>
Show stickers button
Surround selected text when typing special characters
</label>
</div>
</div>
@@ -767,6 +767,39 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
<label
class="_label_1o4d9_60"
for="mx_SettingsFlag_u3pEwuLn9Enn"
>
Show stickers button
</label>
</div>
</div>
<div
class="_inline-field_1o4d9_33"
>
<div
class="_inline-field-control_1o4d9_45"
>
<div
class="_container_udcm8_10"
>
<input
checked=""
class="_input_udcm8_24"
disabled=""
id="mx_SettingsFlag_YuxfFEpOsztW"
role="switch"
type="checkbox"
/>
<div
class="_ui_udcm8_34"
/>
</div>
</div>
<div
class="_inline-field-body_1o4d9_39"
>
<label
class="_label_1o4d9_60"
for="mx_SettingsFlag_YuxfFEpOsztW"
>
Insert a trailing colon after user mentions at the start of a message
</label>
@@ -805,7 +838,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
<input
class="_input_udcm8_24"
disabled=""
id="mx_SettingsFlag_YuxfFEpOsztW"
id="mx_SettingsFlag_hQkBerF1ejc4"
role="switch"
type="checkbox"
/>
@@ -819,7 +852,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
>
<label
class="_label_1o4d9_60"
for="mx_SettingsFlag_YuxfFEpOsztW"
for="mx_SettingsFlag_hQkBerF1ejc4"
>
Enable automatic language detection for syntax highlighting
</label>
@@ -837,7 +870,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
<input
class="_input_udcm8_24"
disabled=""
id="mx_SettingsFlag_hQkBerF1ejc4"
id="mx_SettingsFlag_GFes1UFzOK2n"
role="switch"
type="checkbox"
/>
@@ -851,7 +884,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
>
<label
class="_label_1o4d9_60"
for="mx_SettingsFlag_hQkBerF1ejc4"
for="mx_SettingsFlag_GFes1UFzOK2n"
>
Expand code blocks by default
</label>
@@ -870,7 +903,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
checked=""
class="_input_udcm8_24"
disabled=""
id="mx_SettingsFlag_GFes1UFzOK2n"
id="mx_SettingsFlag_vfGFMldL2r2v"
role="switch"
type="checkbox"
/>
@@ -884,7 +917,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
>
<label
class="_label_1o4d9_60"
for="mx_SettingsFlag_GFes1UFzOK2n"
for="mx_SettingsFlag_vfGFMldL2r2v"
>
Show line numbers in code blocks
</label>
@@ -932,7 +965,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
<input
checked=""
class="_input_udcm8_24"
id="mx_SettingsFlag_vfGFMldL2r2v"
id="mx_SettingsFlag_bsSwicmKUiOB"
role="switch"
type="checkbox"
/>
@@ -946,7 +979,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
>
<label
class="_label_1o4d9_60"
for="mx_SettingsFlag_vfGFMldL2r2v"
for="mx_SettingsFlag_bsSwicmKUiOB"
>
Enable previews
</label>
@@ -963,7 +996,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
>
<input
class="_input_udcm8_24"
id="mx_SettingsFlag_bsSwicmKUiOB"
id="mx_SettingsFlag_dvqsxEaZtl3A"
role="switch"
type="checkbox"
/>
@@ -977,7 +1010,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
>
<label
class="_label_1o4d9_60"
for="mx_SettingsFlag_bsSwicmKUiOB"
for="mx_SettingsFlag_dvqsxEaZtl3A"
>
Enable previews in encrypted rooms
</label>
@@ -1016,7 +1049,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
<input
class="_input_udcm8_24"
disabled=""
id="mx_SettingsFlag_dvqsxEaZtl3A"
id="mx_SettingsFlag_NIiWzqsApP1c"
role="switch"
type="checkbox"
/>
@@ -1030,7 +1063,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
>
<label
class="_label_1o4d9_60"
for="mx_SettingsFlag_dvqsxEaZtl3A"
for="mx_SettingsFlag_NIiWzqsApP1c"
>
Autoplay GIFs
</label>
@@ -1048,7 +1081,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
<input
class="_input_udcm8_24"
disabled=""
id="mx_SettingsFlag_NIiWzqsApP1c"
id="mx_SettingsFlag_q1SIAPqLMVXh"
role="switch"
type="checkbox"
/>
@@ -1062,7 +1095,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
>
<label
class="_label_1o4d9_60"
for="mx_SettingsFlag_NIiWzqsApP1c"
for="mx_SettingsFlag_q1SIAPqLMVXh"
>
Autoplay videos
</label>
@@ -1089,39 +1122,6 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
<div
class="mx_SettingsSubsection_content"
>
<div
class="_inline-field_1o4d9_33"
>
<div
class="_inline-field-control_1o4d9_45"
>
<div
class="_container_udcm8_10"
>
<input
checked=""
class="_input_udcm8_24"
disabled=""
id="mx_SettingsFlag_q1SIAPqLMVXh"
role="switch"
type="checkbox"
/>
<div
class="_ui_udcm8_34"
/>
</div>
</div>
<div
class="_inline-field-body_1o4d9_39"
>
<label
class="_label_1o4d9_60"
for="mx_SettingsFlag_q1SIAPqLMVXh"
>
Show typing notifications
</label>
</div>
</div>
<div
class="_inline-field_1o4d9_33"
>
@@ -1151,7 +1151,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
class="_label_1o4d9_60"
for="mx_SettingsFlag_dXFDGgBsKXay"
>
Show a placeholder for removed messages
Show typing notifications
</label>
</div>
</div>
@@ -1184,7 +1184,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
class="_label_1o4d9_60"
for="mx_SettingsFlag_7Az0xw4Bs4Tt"
>
Show read receipts sent by other users
Show a placeholder for removed messages
</label>
</div>
</div>
@@ -1217,7 +1217,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
class="_label_1o4d9_60"
for="mx_SettingsFlag_8jmzPIlPoBCv"
>
Show join/leave messages (invites/removes/bans unaffected)
Show read receipts sent by other users
</label>
</div>
</div>
@@ -1250,7 +1250,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
class="_label_1o4d9_60"
for="mx_SettingsFlag_enFRaTjdsFou"
>
Show display name changes
Show join/leave messages (invites/removes/bans unaffected)
</label>
</div>
</div>
@@ -1283,7 +1283,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
class="_label_1o4d9_60"
for="mx_SettingsFlag_bfwnd5rz4XNX"
>
Show chat effects (animations when receiving e.g. confetti)
Show display name changes
</label>
</div>
</div>
@@ -1316,7 +1316,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
class="_label_1o4d9_60"
for="mx_SettingsFlag_gs5uWEzYzZrS"
>
Show profile picture changes
Show chat effects (animations when receiving e.g. confetti)
</label>
</div>
</div>
@@ -1349,7 +1349,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
class="_label_1o4d9_60"
for="mx_SettingsFlag_qWg7OgID1yRR"
>
Show avatars in user, room and event mentions
Show profile picture changes
</label>
</div>
</div>
@@ -1382,7 +1382,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
class="_label_1o4d9_60"
for="mx_SettingsFlag_pOPewl7rtMbV"
>
Enable big emoji in chat
Show avatars in user, room and event mentions
</label>
</div>
</div>
@@ -1415,7 +1415,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
class="_label_1o4d9_60"
for="mx_SettingsFlag_cmt3PZSyNp3v"
>
Jump to the bottom of the timeline when you send a message
Enable big emoji in chat
</label>
</div>
</div>
@@ -1447,6 +1447,39 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
<label
class="_label_1o4d9_60"
for="mx_SettingsFlag_dJJz3lHUv9XX"
>
Jump to the bottom of the timeline when you send a message
</label>
</div>
</div>
<div
class="_inline-field_1o4d9_33"
>
<div
class="_inline-field-control_1o4d9_45"
>
<div
class="_container_udcm8_10"
>
<input
checked=""
class="_input_udcm8_24"
disabled=""
id="mx_SettingsFlag_SBSSOZDRlzlA"
role="switch"
type="checkbox"
/>
<div
class="_ui_udcm8_34"
/>
</div>
</div>
<div
class="_inline-field-body_1o4d9_39"
>
<label
class="_label_1o4d9_60"
for="mx_SettingsFlag_SBSSOZDRlzlA"
>
Show current profile picture and name for users in message history
</label>
@@ -1696,7 +1729,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
<input
class="_input_udcm8_24"
disabled=""
id="mx_SettingsFlag_SBSSOZDRlzlA"
id="mx_SettingsFlag_FLEpLCb0jpp6"
role="switch"
type="checkbox"
/>
@@ -1710,7 +1743,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
>
<label
class="_label_1o4d9_60"
for="mx_SettingsFlag_SBSSOZDRlzlA"
for="mx_SettingsFlag_FLEpLCb0jpp6"
>
Show NSFW content
</label>
@@ -1750,7 +1783,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
checked=""
class="_input_udcm8_24"
disabled=""
id="mx_SettingsFlag_FLEpLCb0jpp6"
id="mx_SettingsFlag_NQFWldEwbV3q"
role="switch"
type="checkbox"
/>
@@ -1764,7 +1797,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
>
<label
class="_label_1o4d9_60"
for="mx_SettingsFlag_FLEpLCb0jpp6"
for="mx_SettingsFlag_NQFWldEwbV3q"
>
Prompt before sending invites to potentially invalid matrix IDs
</label>
@@ -929,6 +929,7 @@ describe("RoomListStoreV3", () => {
describe("Sections", () => {
function enableSections(): void {
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting: string) => {
if (setting === "RoomList.showSections") return true;
if (setting === "RoomList.OrderedCustomSections") return [];
if (setting === "RoomList.CustomSectionData") return {};
return false;
@@ -961,6 +962,55 @@ describe("RoomListStoreV3", () => {
expect(result.sections[2].tag).toBe(DefaultTagID.LowPriority);
});
describe("RoomList.showSections disabled", () => {
function disableSections(): void {
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting: string) => {
if (setting === "RoomList.showSections") return false;
if (setting === "RoomList.OrderedCustomSections") return [];
if (setting === "RoomList.CustomSectionData") return {};
return false;
});
}
it("returns a single Chats section containing the rooms", async () => {
disableSections();
const { rooms } = getClientAndRooms();
rooms[3].tags[DefaultTagID.Favourite] = {};
rooms[7].tags[DefaultTagID.LowPriority] = {};
const store = new RoomListStoreV3Class(dispatcher);
await store.start();
const { sections } = store.getSortedRoomsInActiveSpace();
expect(sections).toHaveLength(1);
expect(sections[0].tag).toBe(CHATS_TAG);
expect(sections[0].rooms).toContain(rooms[3]);
expect(sections[0].rooms).toContain(rooms[7]);
});
});
it("emits LISTS_UPDATE_EVENT when RoomList.showSections setting changes", async () => {
enableSections();
getClientAndRooms();
let settingsWatcher: () => void = () => {};
jest.spyOn(SettingsStore, "watchSetting").mockImplementation((settingName, _roomId, callback) => {
if (settingName === "RoomList.showSections") settingsWatcher = callback as () => void;
return "watcher-id";
});
const store = new RoomListStoreV3Class(dispatcher);
await store.start();
const listsUpdateListener = jest.fn();
store.on(LISTS_UPDATE_EVENT, listsUpdateListener);
settingsWatcher();
expect(listsUpdateListener).toHaveBeenCalled();
});
it.each([
{ tag: DefaultTagID.Favourite, label: "Favourite" },
{ tag: DefaultTagID.LowPriority, label: "LowPriority" },
@@ -1209,6 +1259,7 @@ describe("RoomListStoreV3", () => {
const customTag = "element.io.section.custom";
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting: string) => {
if (setting === "RoomList.showSections") return true;
if (setting === "RoomList.OrderedCustomSections") return [];
if (setting === "RoomList.CustomSectionData") return {};
return false;
@@ -1223,6 +1274,7 @@ describe("RoomListStoreV3", () => {
// Mark a room with the custom tag and update the settings
rooms[0].tags = { [customTag]: { order: 0 } };
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting: string) => {
if (setting === "RoomList.showSections") return true;
if (setting === "RoomList.OrderedCustomSections") return [customTag];
if (setting === "RoomList.CustomSectionData")
return { [customTag]: { tag: customTag, name: "Custom" } };
@@ -156,6 +156,36 @@ describe("RoomListHeaderViewModel", () => {
expect(vm.getSnapshot().isMessagePreviewEnabled).toBe(true);
});
it("should set areSectionsEnabled to true when RoomList.showSections is enabled", () => {
jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName: string) => {
if (settingName === "RoomList.showSections") return true;
return false;
});
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance });
expect(vm.getSnapshot().areSectionsEnabled).toBe(true);
});
it("should update areSectionsEnabled when RoomList.showSections setting changes", () => {
let watchCallback: () => void = () => {};
jest.spyOn(SettingsStore, "watchSetting").mockImplementation((settingName, _roomId, callback) => {
if (settingName === "RoomList.showSections") watchCallback = callback as () => void;
return "watcher-id";
});
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance });
expect(vm.getSnapshot().areSectionsEnabled).toBe(false);
// Enable sections
jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName: string) => {
if (settingName === "RoomList.showSections") return true;
return false;
});
watchCallback();
expect(vm.getSnapshot().areSectionsEnabled).toBe(true);
});
it("should set displaySectionReleaseAnnouncement to true when sections feature is enabled and announcement is active", () => {
jest.spyOn(ReleaseAnnouncementStore.instance, "getReleaseAnnouncement").mockReturnValue(
"room_list_section",
@@ -664,6 +664,36 @@ describe("RoomListItemViewModel", () => {
expect(viewModel.getSnapshot().sections.map((s) => s.tag)).toEqual([]);
});
it("should set areSectionsEnabled to true when RoomList.showSections is enabled", () => {
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting) => {
if (setting === "RoomList.showSections") return true;
return false;
});
viewModel = new RoomListItemViewModel({ room, client: matrixClient });
expect(viewModel.getSnapshot().areSectionsEnabled).toBe(true);
});
it("should update areSectionsEnabled when RoomList.showSections setting changes", () => {
let watchCallback: CallbackFn<"RoomList.showSections"> = () => {};
jest.spyOn(SettingsStore, "watchSetting").mockImplementation((setting, _room, callback) => {
if (setting === "RoomList.showSections") watchCallback = callback;
return "watcher-id";
});
viewModel = new RoomListItemViewModel({ room, client: matrixClient });
expect(viewModel.getSnapshot().areSectionsEnabled).toBe(false);
// Enable sections
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting) => {
if (setting === "RoomList.showSections") return true;
return false;
});
watchCallback("RoomList.showSections", null, null as any, null, null);
expect(viewModel.getSnapshot().areSectionsEnabled).toBe(true);
});
});
describe("Cleanup", () => {
@@ -142,3 +142,17 @@ export const DisplaySectionReleaseAnnouncement: Story = {
},
},
};
export const SectionsDisabled: Story = {
args: {
areSectionsEnabled: false,
},
};
export const NoComposeMenu: Story = {
args: {
canCreateRoom: false,
canCreateVideoRoom: false,
areSectionsEnabled: false,
},
};
@@ -7,7 +7,7 @@
import React, { type JSX } from "react";
import { IconButton, H1 } from "@vector-im/compound-web";
import { CollapseAllIcon, ExpandAllIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import { CollapseAllIcon, ExpandAllIcon, ChatIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import { type ViewModel, useViewModel } from "../../core/viewmodel";
import { Flex } from "../../core/utils/Flex";
@@ -59,6 +59,10 @@ export interface RoomListHeaderViewSnapshot {
* Whether message previews are enabled in the room list.
*/
isMessagePreviewEnabled: boolean;
/**
* Whether sections are enabled in the room list.
*/
areSectionsEnabled: boolean;
/**
* If "collapse", an icon to collapse all sections is shown.
* If "expand", an icon to expand all sections is shown.
@@ -145,7 +149,9 @@ interface RoomListHeaderViewProps {
*/
export function RoomListHeaderView({ vm }: Readonly<RoomListHeaderViewProps>): JSX.Element {
const { translate: _t } = useI18n();
const { title, displaySpaceMenu, collapseSections } = useViewModel(vm);
const { title, displaySpaceMenu, collapseSections, areSectionsEnabled, canCreateRoom, canCreateVideoRoom } =
useViewModel(vm);
const canOnlyStartChat = !areSectionsEnabled && !canCreateRoom && !canCreateVideoRoom;
return (
<Flex
@@ -164,7 +170,7 @@ export function RoomListHeaderView({ vm }: Readonly<RoomListHeaderViewProps>): J
</Flex>
<Flex align="center" gap="var(--cpd-space-2x)">
<OptionMenuView vm={vm} />
{collapseSections && (
{areSectionsEnabled && collapseSections && (
<IconButton
size="28px"
style={{ padding: "4px" }}
@@ -182,7 +188,18 @@ export function RoomListHeaderView({ vm }: Readonly<RoomListHeaderViewProps>): J
)}
</IconButton>
)}
<ComposeMenuView vm={vm} />
{canOnlyStartChat ? (
<IconButton
size="28px"
style={{ padding: "4px" }}
onClick={(e) => vm.createChatRoom(e.nativeEvent)}
tooltip={_t("action|start_chat")}
>
<ChatIcon color="var(--cpd-color-icon-secondary)" aria-hidden />
</IconButton>
) : (
<ComposeMenuView vm={vm} />
)}
</Flex>
</Flex>
</Flex>
@@ -17,4 +17,5 @@ export const defaultSnapshot: RoomListHeaderViewSnapshot = {
activeSortOption: "recent",
isMessagePreviewEnabled: true,
displaySectionReleaseAnnouncement: false,
areSectionsEnabled: true,
};
@@ -36,7 +36,8 @@ interface ComposeMenuViewProps {
export function ComposeMenuView({ vm }: ComposeMenuViewProps): JSX.Element {
const { translate: _t } = useI18n();
const [open, setOpen] = useState(false);
const { canCreateRoom, canCreateVideoRoom, displaySectionReleaseAnnouncement } = useViewModel(vm);
const { canCreateRoom, canCreateVideoRoom, displaySectionReleaseAnnouncement, areSectionsEnabled } =
useViewModel(vm);
// 28px button with a 20px icon
const button = (
@@ -80,7 +81,9 @@ export function ComposeMenuView({ vm }: ComposeMenuViewProps): JSX.Element {
hideChevron
/>
)}
<MenuItem Icon={SectionIcon} label={_t("action|new_section")} onSelect={vm.createSection} hideChevron />
{areSectionsEnabled && (
<MenuItem Icon={SectionIcon} label={_t("action|new_section")} onSelect={vm.createSection} hideChevron />
)}
</Menu>
);
}
@@ -131,41 +131,45 @@ export function MoreOptionContent({ vm }: MoreOptionContentProps): JSX.Element {
hideChevron={true}
/>
)}
<SubMenu
trigger={
<MenuItem
Icon={ArrowRightIcon}
label={_t("room_list|more_options|move_to_section")}
onSelect={null}
/>
}
>
{snapshot.sections.map((section) => (
<MenuItem
key={section.tag}
label={section.name}
labelProps={{ className: styles.sectionLabel }}
onSelect={() => vm.onToggleSection(section.tag)}
onClick={(evt) => evt.stopPropagation()}
hideChevron={true}
aria-checked={section.isSelected}
{snapshot.areSectionsEnabled && (
<>
<SubMenu
trigger={
<MenuItem
Icon={ArrowRightIcon}
label={_t("room_list|more_options|move_to_section")}
onSelect={null}
/>
}
>
{section.isSelected && (
<CheckIcon color="var(--cpd-color-icon-tertiary)" width="24px" height="24px" />
)}
</MenuItem>
))}
{hasSections && <Separator />}
<MenuItem label={_t("action|new_section")} onSelect={vm.onCreateSection} hideChevron={true} />
</SubMenu>
{isInSection && (
<MenuItem
Icon={MinusIcon}
label={_t("room_list|more_options|remove_from_section")}
onSelect={vm.onRemoveFromSection}
onClick={(evt) => evt.stopPropagation()}
hideChevron={true}
/>
{snapshot.sections.map((section) => (
<MenuItem
key={section.tag}
label={section.name}
labelProps={{ className: styles.sectionLabel }}
onSelect={() => vm.onToggleSection(section.tag)}
onClick={(evt) => evt.stopPropagation()}
hideChevron={true}
aria-checked={section.isSelected}
>
{section.isSelected && (
<CheckIcon color="var(--cpd-color-icon-tertiary)" width="24px" height="24px" />
)}
</MenuItem>
))}
{hasSections && <Separator />}
<MenuItem label={_t("action|new_section")} onSelect={vm.onCreateSection} hideChevron={true} />
</SubMenu>
{isInSection && (
<MenuItem
Icon={MinusIcon}
label={_t("room_list|more_options|remove_from_section")}
onSelect={vm.onRemoveFromSection}
onClick={(evt) => evt.stopPropagation()}
hideChevron={true}
/>
)}
</>
)}
<Separator />
<MenuItem
@@ -308,3 +308,9 @@ export const LastItem: Story = {
isSelected: true,
},
};
export const SectionDisabled: Story = {
args: {
areSectionsEnabled: false,
},
};
@@ -94,6 +94,8 @@ export interface RoomListItemViewSnapshot {
roomNotifState: RoomNotifState;
/** Available sections the room can be assigned to */
sections: Section[];
/** Whether sections are enabled in the room list */
areSectionsEnabled: boolean;
}
/**
@@ -53,4 +53,5 @@ export const defaultSnapshot: RoomListItemViewSnapshot = {
isSelected: false,
},
],
areSectionsEnabled: true,
};
@@ -106,6 +106,7 @@ export const createMockRoomSnapshot = (id: string, name: string, index: number):
canMarkAsUnread: true,
roomNotifState: RoomNotifState.AllMessages,
sections: [],
areSectionsEnabled: true,
});
export function createMockRoomItemViewModel(roomId: string, name: string, index: number): RoomListItemViewModel {