Room list: assign room to custom section (#33238)

* feat(sc): add new toast type for room list

* feat(sc): add section entries in room list item menu

* feat(rls): expose util functions

* feat: allows to tag room with custom sections

* feat(vm): add new Chat moved toast to room list vm

* feat(vm): add section selection to room list item vm

* feat(e2e): add tests for adding room in a custom section

* test(e2e): update existing screenshots

* chore: fix lint after merge

* chore: remove outline in test
This commit is contained in:
Florian Duros
2026-04-22 21:50:54 +02:00
committed by GitHub
parent 73e1b87075
commit f4c62abbcd
28 changed files with 532 additions and 26 deletions
@@ -21,7 +21,7 @@ import { RoomNotificationState } from "../../../src/stores/notifications/RoomNot
import { RoomNotificationStateStore } from "../../../src/stores/notifications/RoomNotificationStateStore";
import { NotificationStateEvents } from "../../../src/stores/notifications/NotificationState";
import { type MessagePreview, MessagePreviewStore } from "../../../src/stores/message-preview";
import SettingsStore from "../../../src/settings/SettingsStore";
import SettingsStore, { type CallbackFn } from "../../../src/settings/SettingsStore";
import DMRoomMap from "../../../src/utils/DMRoomMap";
import { DefaultTagID } from "../../../src/stores/room-list-v3/skip-list/tag";
import dispatcher from "../../../src/dispatcher/dispatcher";
@@ -29,7 +29,8 @@ import { Action } from "../../../src/dispatcher/actions";
import { CallStore } from "../../../src/stores/CallStore";
import { CallEvent, type Call } from "../../../src/models/Call";
import { RoomListItemViewModel } from "../../../src/viewmodels/room-list/RoomListItemViewModel";
import RoomListStoreV3 from "../../../src/stores/room-list-v3/RoomListStoreV3";
import RoomListStoreV3, { CHATS_TAG } from "../../../src/stores/room-list-v3/RoomListStoreV3";
import * as tagRoomModule from "../../../src/utils/room/tagRoom";
jest.mock("../../../src/viewmodels/room-list/utils", () => ({
hasAccessToOptionsMenu: jest.fn().mockReturnValue(true),
@@ -83,6 +84,7 @@ describe("RoomListItemViewModel", () => {
jest.spyOn(MessagePreviewStore.instance, "getPreviewForRoom").mockResolvedValue(null);
jest.spyOn(CallStore.instance, "getCall").mockReturnValue(null);
jest.spyOn(RoomListStoreV3.instance, "orderedSectionTags", "get").mockReturnValue([]);
});
afterEach(() => {
@@ -213,8 +215,8 @@ describe("RoomListItemViewModel", () => {
let watchCallback: any;
jest.spyOn(SettingsStore, "getValue").mockImplementation(() => showPreview);
jest.spyOn(SettingsStore, "watchSetting").mockImplementation((_setting, _room, callback) => {
watchCallback = callback;
jest.spyOn(SettingsStore, "watchSetting").mockImplementation((setting, _room, callback) => {
if (setting === "RoomList.showMessagePreview") watchCallback = callback;
return "watcher-id";
});
jest.spyOn(MessagePreviewStore.instance, "getPreviewForRoom").mockResolvedValue({
@@ -595,6 +597,93 @@ describe("RoomListItemViewModel", () => {
viewModel.onCreateSection();
expect(createSectionSpy).toHaveBeenCalled();
});
it("should call tagRoom when onToggleSection is called", () => {
const tagRoomSpy = jest.spyOn(tagRoomModule, "tagRoom").mockImplementation(() => {});
viewModel = new RoomListItemViewModel({ room, client: matrixClient });
viewModel.onToggleSection(DefaultTagID.Favourite);
expect(tagRoomSpy).toHaveBeenCalledWith(room, DefaultTagID.Favourite);
});
});
describe("Sections", () => {
const customTag = "element.io.section.custom1";
beforeEach(() => {
jest.spyOn(RoomListStoreV3.instance, "orderedSectionTags", "get").mockReturnValue([
DefaultTagID.Favourite,
customTag,
CHATS_TAG,
DefaultTagID.LowPriority,
]);
});
it("should include sections from orderedSectionTags excluding CHATS_TAG", () => {
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting) => {
if (setting === "feature_room_list_sections") return true;
return false;
});
viewModel = new RoomListItemViewModel({ room, client: matrixClient });
const sections = viewModel.getSnapshot().sections;
expect(sections.map((s) => s.tag)).toEqual([DefaultTagID.Favourite, customTag, DefaultTagID.LowPriority]);
});
it("should mark the room current section as selected", () => {
room.tags = { [DefaultTagID.Favourite]: { order: 0 } };
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting) => {
if (setting === "feature_room_list_sections") return true;
return false;
});
viewModel = new RoomListItemViewModel({ room, client: matrixClient });
const sections = viewModel.getSnapshot().sections;
expect(sections.find((s) => s.tag === DefaultTagID.Favourite)?.isSelected).toBe(true);
expect(sections.find((s) => s.tag === DefaultTagID.LowPriority)?.isSelected).toBe(false);
});
it("should use custom section name from CustomSectionData", () => {
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting) => {
if (setting === "feature_room_list_sections") return true;
if (setting === "RoomList.CustomSectionData")
return { [customTag]: { name: "My Custom Section", tag: customTag } };
return false;
});
viewModel = new RoomListItemViewModel({ room, client: matrixClient });
const section = viewModel.getSnapshot().sections.find((s) => s.tag === customTag);
expect(section?.name).toBe("My Custom Section");
});
it("should update sections when OrderedCustomSections setting changes", () => {
let watchCallback: CallbackFn = () => {};
jest.spyOn(SettingsStore, "watchSetting").mockImplementation((setting, _room, callback) => {
if (setting === "RoomList.OrderedCustomSections") watchCallback = callback;
return "watcher-id";
});
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting) => {
if (setting === "feature_room_list_sections") return true;
return false;
});
viewModel = new RoomListItemViewModel({ room, client: matrixClient });
expect(viewModel.getSnapshot().sections).toHaveLength(3); // Favourite, custom, LowPriority
// Simulate reordering: custom section removed
jest.spyOn(RoomListStoreV3.instance, "orderedSectionTags", "get").mockReturnValue([
DefaultTagID.Favourite,
CHATS_TAG,
DefaultTagID.LowPriority,
]);
watchCallback("RoomList.OrderedCustomSections", null, null as any, null, null);
expect(viewModel.getSnapshot().sections.map((s) => s.tag)).toEqual([
DefaultTagID.Favourite,
DefaultTagID.LowPriority,
]);
});
});
describe("Cleanup", () => {
@@ -619,6 +619,12 @@ describe("RoomListViewModel", () => {
expect(viewModel.getSnapshot().toast).toBe("section_created");
});
it("should show toast when RoomTagged event fires", () => {
viewModel = new RoomListViewModel({ client: matrixClient });
RoomListStoreV3.instance.emit(RoomListStoreV3Event.RoomTagged);
expect(viewModel.getSnapshot().toast).toBe("chat_moved");
});
it("should clear toast when closeToast is called", () => {
viewModel = new RoomListViewModel({ client: matrixClient });