Room list: add custom section creation (#33155)

* feat: add creation section dialog

* feat: add in skip list a method to change filters

* feat: add helper to creation section

* feat: add custom sections data to Settings

* feat: add custom section to room list store v3

* feat: update header and room list item vms

* feat: add toast to room list vm

* feat: add new translation

* chore: move util functions of room list specs

* test: add custom section playwright tests

* chore: call loadCustomSections in RoomListStoreV3 ctor
This commit is contained in:
Florian Duros
2026-04-17 12:02:42 +00:00
committed by GitHub
parent 73d4b63ada
commit 6b67b24254
27 changed files with 985 additions and 97 deletions
@@ -201,7 +201,7 @@ export class RoomListHeaderViewModel
};
public createSection = (): void => {
// To be implemented when custom section creation is added in vms
RoomListStoreV3.instance.createSection();
};
}
/**
@@ -275,6 +275,10 @@ function computeHeaderSpaceState(
);
const canAccessSpaceSettings = Boolean(activeSpace && shouldShowSpaceSettings(activeSpace));
const isSectionFeatureEnabled = SettingsStore.getValue("feature_room_list_sections");
const useComposeIcon = !isSectionFeatureEnabled;
const canCreateSection = isSectionFeatureEnabled;
return {
title,
canCreateRoom,
@@ -283,8 +287,7 @@ function computeHeaderSpaceState(
displaySpaceMenu,
canInviteInSpace,
canAccessSpaceSettings,
// To be implemented when custom section creation is added in vms
canCreateSection: false,
useComposeIcon: true,
canCreateSection,
useComposeIcon,
};
}
@@ -37,6 +37,7 @@ import { Action } from "../../dispatcher/actions";
import type { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload";
import PosthogTrackers from "../../PosthogTrackers";
import { type Call, CallEvent } from "../../models/Call";
import RoomListStoreV3 from "../../stores/room-list-v3/RoomListStoreV3";
interface RoomItemProps {
room: Room;
@@ -276,6 +277,8 @@ export class RoomListItemViewModel
const callType =
call?.callType === CallType.Voice ? "voice" : call?.callType === CallType.Video ? "video" : undefined;
const canMoveToSection = SettingsStore.getValue("feature_room_list_sections");
return {
id: room.roomId,
room,
@@ -303,8 +306,7 @@ export class RoomListItemViewModel
canMarkAsRead,
canMarkAsUnread,
roomNotifState,
// To be implemented when custom section creation is added in vms
canMoveToSection: false,
canMoveToSection,
};
}
@@ -385,6 +387,6 @@ export class RoomListItemViewModel
};
public onCreateSection = (): void => {
// To be implemented when custom section creation is added in vms
RoomListStoreV3.instance.createSection();
};
}
@@ -91,6 +91,11 @@ export class RoomListViewModel
// Don't clear section vm because we want to keep the expand/collapse state even during space changes.
private readonly roomSectionHeaderViewModels = new Map<string, RoomListSectionHeaderViewModel>();
/**
* Reference to the currently displayed toast, used to automatically close the toast after a timeout.
*/
private toastRef?: number;
public constructor(props: RoomListViewModelProps) {
const activeSpace = SpaceStore.instance.activeSpaceRoom;
@@ -144,6 +149,13 @@ export class RoomListViewModel
this.onListsLoaded,
);
// Subscribe to section creation
this.disposables.trackListener(
RoomListStoreV3.instance,
RoomListStoreV3Event.SectionCreated as any,
this.onSectionCreated,
);
// Subscribe to active room changes to update selected room
const dispatcherRef = dispatcher.register(this.onDispatch);
this.disposables.track(() => {
@@ -264,7 +276,8 @@ export class RoomListViewModel
public getSectionHeaderViewModel(tag: string): RoomListSectionHeaderViewModel {
if (this.roomSectionHeaderViewModels.has(tag)) return this.roomSectionHeaderViewModels.get(tag)!;
const title = TAG_TO_TITLE_MAP[tag] || tag;
const customSections = SettingsStore.getValue("RoomList.CustomSectionData");
const title = TAG_TO_TITLE_MAP[tag] || customSections[tag]?.name || tag;
const viewModel = new RoomListSectionHeaderViewModel({
tag,
title,
@@ -573,7 +586,19 @@ export class RoomListViewModel
}
};
public onSectionCreated = (): void => {
clearTimeout(this.toastRef);
this.snapshot.merge({
toast: "section_created",
});
// Automatically close the toast after 15 seconds
this.toastRef = setTimeout(() => {
this.closeToast();
}, 15 * 1000);
};
public closeToast: () => void = () => {
clearTimeout(this.toastRef);
this.snapshot.merge({
toast: undefined,
});
@@ -590,9 +615,11 @@ function computeSections(
roomsResult: RoomsResult,
isSectionExpanded: (tag: string) => boolean,
): { sections: Section[]; isFlatList: boolean } {
const customSections = SettingsStore.getValue("RoomList.CustomSectionData");
const sections = roomsResult.sections
// Only include sections that have rooms
.filter((section) => section.rooms.length > 0)
// Only include sections that have rooms or are custom sections (which may be empty but should still be shown)
.filter((section) => section.rooms.length > 0 || customSections[section.tag])
// Remove roomIds for sections that are currently collapsed according to their section header view model
.map((section) => ({
...section,