Room list: improve custom sections in Spaces (#33523)

* feat: display empty custom sections in the space where they were created

* test: update tests

* fix: room can't be drag in to a section if a section is created before

* fix: re-render issue when section is created
This commit is contained in:
Florian Duros
2026-05-27 09:43:59 +00:00
committed by GitHub
parent 1f305f1e10
commit abe383d996
7 changed files with 174 additions and 19 deletions
@@ -486,7 +486,7 @@ export class RoomListStoreV3Class extends AsyncStoreWithClient<EmptyObject> {
* Emits {@link SECTION_CREATED_EVENT} if the section was successfully created.
*/
public async createSection(): Promise<string | undefined> {
const tag = await createSection();
const tag = await createSection(SpaceStore.instance.activeSpace);
if (!tag) return;
this.emit(SECTION_CREATED_EVENT, tag);
return tag;
+25 -4
View File
@@ -13,6 +13,8 @@ import Modal from "../../Modal";
import { CreateSectionDialog } from "../../components/views/dialogs/CreateSectionDialog";
import { RemoveSectionDialog } from "../../components/views/dialogs/RemoveSectionDialog";
import { DefaultTagID, type TagID } from "./skip-list/tag";
import { isMetaSpace, MetaSpace, type SpaceKey } from "../spaces";
import SpaceStore from "../spaces/SpaceStore";
/**
* A synthetic tag used to represent the "Chats" section, which contains
@@ -60,6 +62,8 @@ export function isSectionTag(tagId: TagID): boolean {
type CustomSection = {
tag: CustomTag;
name: string;
/** The space in which this section was created. Used to control visibility of empty sections. */
spaceId?: SpaceKey;
};
/**
@@ -83,6 +87,14 @@ export type CustomSectionsData = Record<CustomTag, CustomSection>;
*/
export type OrderedCustomSections = CustomTag[];
/**
* Returns true if the given space key corresponds to an enabled meta-space or a known top-level space room.
*/
function doesSpaceExist(spaceId: SpaceKey): boolean {
if (isMetaSpace(spaceId)) return SpaceStore.instance.enabledMetaSpaces.includes(spaceId);
return SpaceStore.instance.spacePanelSpaces.some((room) => room.roomId === spaceId);
}
/**
* Retrieves the custom sections data from the settings.
* Invalid or malformed entries are dropped and the cleaned data is persisted back to settings.
@@ -92,9 +104,17 @@ export function getCustomSectionData(): CustomSectionsData {
// Data are malformed
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return {};
// Filter out invalid entries
return Object.fromEntries(
Object.entries(raw).filter(([key, value]) => isValidCustomSection(value) && value.tag === key),
Object.entries(raw)
.filter(([key, value]) => isValidCustomSection(value) && value.tag === key)
.map(([key, value]) => [
key,
{
...value,
// Default to MetaSpace.Home for legacy sections (no spaceId) or if the stored space no longer exists
spaceId: value.spaceId && doesSpaceExist(value.spaceId) ? value.spaceId : MetaSpace.Home,
},
]),
) as CustomSectionsData;
}
@@ -113,16 +133,17 @@ export function getOrderedCustomSections(): OrderedCustomSections {
* Creates a new custom section by showing a dialog to the user to enter the section name.
* If the user confirms, it generates a unique tag for the section, saves the section data in the settings, and updates the ordered list of sections.
*
* @param spaceId The space in which the section is being created. Used to control visibility of the empty section.
* @return A promise that resolves to the new section tag if created, or undefined if cancelled.
*/
export async function createSection(): Promise<string | undefined> {
export async function createSection(spaceId: SpaceKey): Promise<string | undefined> {
const modal = Modal.createDialog(CreateSectionDialog);
const [shouldCreateSection, sectionName] = await modal.finished;
if (!shouldCreateSection || !sectionName) return undefined;
const tag: CustomTag = `${CUSTOM_SECTION_TAG_PREFIX}${window.crypto.randomUUID()}`;
const newSection: CustomSection = { tag, name: sectionName };
const newSection: CustomSection = { tag, name: sectionName, spaceId };
// Save the new section data
const sectionData = getCustomSectionData();
@@ -645,6 +645,10 @@ export class RoomListViewModel
};
public onSectionCreated = (tag: string): void => {
// Refresh roomsResult so the new section lands in the same snapshot as the scroll-to.
const filterKeys = this.activeFilter !== undefined ? [this.activeFilter] : undefined;
this.roomsResult = RoomListStoreV3.instance.getSortedRoomsInActiveSpace(filterKeys);
this.updateRoomsMap(this.roomsResult);
this.updateRoomListData(false, null, tag);
this.showToast("section_created");
};
@@ -694,9 +698,11 @@ function computeSections(
const customSections = getCustomSectionData();
const sections = roomsResult.sections
// Only include sections that have rooms or are custom sections (which may be empty but should still be shown)
// Only include sections that have rooms, or custom sections that were created in the current space.
.filter(
(section) => section.rooms.length > 0 || (isCustomSectionTag(section.tag) && customSections[section.tag]),
(section) =>
section.rooms.length > 0 ||
(isCustomSectionTag(section.tag) && customSections[section.tag]?.spaceId === roomsResult.spaceId),
)
// Remove roomIds for sections that are currently collapsed according to their section header view model
.map((section) => ({