feat: add custom section to room list store v3

This commit is contained in:
Florian Duros 2026-04-15 16:03:10 +02:00
parent b39210aad5
commit 97035fda32
No known key found for this signature in database
GPG Key ID: A5BBB4041B493F15
2 changed files with 121 additions and 1 deletions

View File

@ -40,6 +40,7 @@ import { DefaultTagID } from "./skip-list/tag";
import { ExcludeTagsFilter } from "./skip-list/filters/ExcludeTagsFilter";
import { TagFilter } from "./skip-list/filters/TagFilter";
import { filterBoolean } from "../../utils/arrays";
import { createSection } from "./section";
/**
* These are the filters passed to the room skip list.
@ -59,6 +60,8 @@ export enum RoomListStoreV3Event {
ListsUpdate = "lists_update",
// The event which is called when the room list is loaded.
ListsLoaded = "lists_loaded",
/** Fired when a new section is created in the room list. */
SectionCreated = "section_created",
}
// The result object for returning rooms from the store
@ -89,6 +92,8 @@ export const CHATS_TAG = "chats";
export const LISTS_UPDATE_EVENT = RoomListStoreV3Event.ListsUpdate;
export const LISTS_LOADED_EVENT = RoomListStoreV3Event.ListsLoaded;
export const SECTION_CREATED_EVENT = RoomListStoreV3Event.SectionCreated;
/**
* This store allows for fast retrieval of the room list in a sorted and filtered manner.
* This is the third such implementation hence the "V3".
@ -108,7 +113,7 @@ export class RoomListStoreV3Class extends AsyncStoreWithClient<EmptyObject> {
/**
* Defines the display order of sections.
*/
private readonly sortedTags: string[] = [DefaultTagID.Favourite, CHATS_TAG, DefaultTagID.LowPriority];
private sortedTags: string[] = [DefaultTagID.Favourite, CHATS_TAG, DefaultTagID.LowPriority];
private readonly msc3946ProcessDynamicPredecessor: boolean;
@ -125,6 +130,7 @@ export class RoomListStoreV3Class extends AsyncStoreWithClient<EmptyObject> {
this.onActiveSpaceChanged();
});
SpaceStore.instance.on(UPDATE_HOME_BEHAVIOUR, () => this.onActiveSpaceChanged());
SettingsStore.watchSetting("RoomList.OrderedCustomSections", null, () => this.onOrderedCustomSectionsChange());
}
/**
@ -196,6 +202,8 @@ export class RoomListStoreV3Class extends AsyncStoreWithClient<EmptyObject> {
protected async onReady(): Promise<any> {
if (this.roomSkipList?.initialized || !this.matrixClient) return;
this.loadCustomSections();
const sorter = this.getPreferredSorter(this.matrixClient.getSafeUserId());
this.roomSkipList = new RoomSkipList(sorter, this.getSkipListFilters());
@ -463,6 +471,37 @@ export class RoomListStoreV3Class extends AsyncStoreWithClient<EmptyObject> {
};
});
}
/**
* Handle changes to the order of custom sections.
* Reloads the custom sections, updates the skip list filters to reflect the new order and emits an update.
* Emit {@link LISTS_UPDATE_EVENT}.
*/
private onOrderedCustomSectionsChange(): void {
this.loadCustomSections();
if (!this.roomSkipList) return;
this.roomSkipList.useNewFilters(this.getSkipListFilters());
this.scheduleEmit();
}
/**
* Create a new section.
* Emits {@link SECTION_CREATED_EVENT} and {@link LISTS_UPDATE_EVENT} if the section was successfully created.
*/
public async createSection(): Promise<void> {
const sectionIsCreated = await createSection();
if (!sectionIsCreated) return;
this.emit(SECTION_CREATED_EVENT);
this.scheduleEmit();
}
/**
* Load the custom sections from the settings store and update the sorted tags.
*/
private loadCustomSections(): void {
const orderedCustomSections = SettingsStore.getValue("RoomList.OrderedCustomSections");
this.sortedTags = [DefaultTagID.Favourite, ...orderedCustomSections, CHATS_TAG, DefaultTagID.LowPriority];
}
}
export default class RoomListStoreV3 {

View File

@ -14,9 +14,11 @@ import type { RoomNotificationState } from "../../../../src/stores/notifications
import {
CHATS_TAG,
LISTS_UPDATE_EVENT,
SECTION_CREATED_EVENT,
RoomListStoreV3Class,
type Section,
} from "../../../../src/stores/room-list-v3/RoomListStoreV3";
import * as sectionModule from "../../../../src/stores/room-list-v3/section";
import { AsyncStoreWithClient } from "../../../../src/stores/AsyncStoreWithClient";
import { RecencySorter } from "../../../../src/stores/room-list-v3/skip-list/sorters/RecencySorter";
import { mkEvent, mkMessage, mkSpace, mkStubRoom, stubClient, upsertRoomStateEvents } from "../../../test-utils";
@ -830,6 +832,7 @@ describe("RoomListStoreV3", () => {
function enableSections(): void {
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting: string) => {
if (setting === "feature_room_list_sections") return true;
if (setting === "RoomList.OrderedCustomSections") return [];
return false;
});
}
@ -1007,6 +1010,84 @@ describe("RoomListStoreV3", () => {
const favSection = findSection(sections, DefaultTagID.Favourite)!;
expect(favSection.rooms).toContain(rooms[3]);
});
describe("createSection", () => {
it("emits SECTION_CREATED_EVENT and LISTS_UPDATE_EVENT when section is created", async () => {
enableSections();
getClientAndRooms();
jest.spyOn(sectionModule, "createSection").mockResolvedValue(true);
const store = new RoomListStoreV3Class(dispatcher);
await store.start();
const sectionCreatedListener = jest.fn();
const listsUpdateListener = jest.fn();
store.on(SECTION_CREATED_EVENT, sectionCreatedListener);
store.on(LISTS_UPDATE_EVENT, listsUpdateListener);
await store.createSection();
expect(sectionCreatedListener).toHaveBeenCalled();
expect(listsUpdateListener).toHaveBeenCalled();
});
it("does not emit when section creation is cancelled", async () => {
enableSections();
getClientAndRooms();
jest.spyOn(sectionModule, "createSection").mockResolvedValue(false);
const store = new RoomListStoreV3Class(dispatcher);
await store.start();
const sectionCreatedListener = jest.fn();
store.on(SECTION_CREATED_EVENT, sectionCreatedListener);
await store.createSection();
expect(sectionCreatedListener).not.toHaveBeenCalled();
});
});
it("updates sections when RoomList.OrderedCustomSections setting changes", async () => {
enableSections();
const { rooms } = getClientAndRooms();
let settingsWatcher: (settingName: string) => void = () => {};
jest.spyOn(SettingsStore, "watchSetting").mockImplementation((settingName, _roomId, callback) => {
if (settingName === "RoomList.OrderedCustomSections") settingsWatcher = callback as () => void;
return "watcher-id";
});
const customTag = "element.io.section.custom";
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting: string) => {
if (setting === "feature_room_list_sections") return true;
if (setting === "RoomList.OrderedCustomSections") return [];
return false;
});
const store = new RoomListStoreV3Class(dispatcher);
await store.start();
// Initial state: 3 sections (Favourite, Chats, LowPriority)
expect(store.getSortedRoomsInActiveSpace().sections).toHaveLength(3);
// 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 === "feature_room_list_sections") return true;
if (setting === "RoomList.OrderedCustomSections") return [customTag];
return false;
});
// Trigger the settings watcher
settingsWatcher("RoomList.OrderedCustomSections");
// Now there should be 4 sections (Favourite, custom, Chats, LowPriority)
expect(store.getSortedRoomsInActiveSpace().sections).toHaveLength(4);
const customSection = findSection(store.getSortedRoomsInActiveSpace().sections, customTag)!;
expect(customSection.rooms).toContain(rooms[0]);
});
});
describe("Muted rooms", () => {