Room list: add default sections (#32785)
* feat: add sections to RLSV3 * feat: add sections in vms * feat: add room list section labs flag * fix: wrong margin for room list item when in sections * feat: hide favourites and low priority filters * fix: crash when changing filter * feat: support sticky room in sections * test: update SC snapshot * test: update SC screenshot * test: update RLS tests * test: add tests to RoomListSectionHeaderViewModel * test: fix existing test in RoomListViewModel * test: add sections tests for RoomListViewModel * test: add e2e tests for sections * fix: incorrect selected room when expanding/collasping a section * fix: typo in `roomSkipList` * feat: use one skip list with all filters instead of one list by tag * chore: put back comment about `roomIndexInSection` * chore: add missing `readonly` * chore: add doc about possible undefined value for room item vm
This commit is contained in:
@@ -11,7 +11,12 @@ import { mocked } from "jest-mock";
|
||||
|
||||
import type { MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import type { RoomNotificationState } from "../../../../src/stores/notifications/RoomNotificationState";
|
||||
import { LISTS_UPDATE_EVENT, RoomListStoreV3Class } from "../../../../src/stores/room-list-v3/RoomListStoreV3";
|
||||
import {
|
||||
CHATS_TAG,
|
||||
LISTS_UPDATE_EVENT,
|
||||
RoomListStoreV3Class,
|
||||
type Section,
|
||||
} from "../../../../src/stores/room-list-v3/RoomListStoreV3";
|
||||
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";
|
||||
@@ -21,7 +26,7 @@ import dispatcher from "../../../../src/dispatcher/dispatcher";
|
||||
import SpaceStore from "../../../../src/stores/spaces/SpaceStore";
|
||||
import { MetaSpace, UPDATE_SELECTED_SPACE } from "../../../../src/stores/spaces";
|
||||
import { DefaultTagID } from "../../../../src/stores/room-list-v3/skip-list/tag";
|
||||
import { FilterKey } from "../../../../src/stores/room-list-v3/skip-list/filters";
|
||||
import { FilterEnum } from "../../../../src/stores/room-list-v3/skip-list/filters";
|
||||
import { RoomNotificationStateStore } from "../../../../src/stores/notifications/RoomNotificationStateStore";
|
||||
import DMRoomMap from "../../../../src/utils/DMRoomMap";
|
||||
import { SortingAlgorithm } from "../../../../src/stores/room-list-v3/skip-list/sorters";
|
||||
@@ -502,7 +507,10 @@ describe("RoomListStoreV3", () => {
|
||||
store.on(LISTS_UPDATE_EVENT, fn);
|
||||
|
||||
// The rooms which belong to the space should not be shown
|
||||
const result = store.getSortedRoomsInActiveSpace().rooms.map((r) => r.roomId);
|
||||
const result = store
|
||||
.getSortedRoomsInActiveSpace()
|
||||
.sections.flatMap((s) => s.rooms)
|
||||
.map((r) => r.roomId);
|
||||
for (const id of roomIds) {
|
||||
expect(result).not.toContain(id);
|
||||
}
|
||||
@@ -511,7 +519,10 @@ describe("RoomListStoreV3", () => {
|
||||
jest.spyOn(SpaceStore.instance, "activeSpace", "get").mockImplementation(() => spaceRoom.roomId);
|
||||
SpaceStore.instance.emit(UPDATE_SELECTED_SPACE);
|
||||
expect(fn).toHaveBeenCalled();
|
||||
const result2 = store.getSortedRoomsInActiveSpace().rooms.map((r) => r.roomId);
|
||||
const result2 = store
|
||||
.getSortedRoomsInActiveSpace()
|
||||
.sections.flatMap((s) => s.rooms)
|
||||
.map((r) => r.roomId);
|
||||
for (const id of roomIds) {
|
||||
expect(result2).toContain(id);
|
||||
}
|
||||
@@ -534,7 +545,9 @@ describe("RoomListStoreV3", () => {
|
||||
await store.start();
|
||||
|
||||
// Sorted, filtered rooms should be 8, 27 and 75
|
||||
const result = store.getSortedRoomsInActiveSpace([FilterKey.FavouriteFilter]).rooms;
|
||||
const result = store
|
||||
.getSortedRoomsInActiveSpace([FilterEnum.FavouriteFilter])
|
||||
.sections.flatMap((s) => s.rooms);
|
||||
expect(result).toHaveLength(3);
|
||||
for (const i of [8, 27, 75]) {
|
||||
expect(result).toContain(rooms[i]);
|
||||
@@ -569,7 +582,9 @@ describe("RoomListStoreV3", () => {
|
||||
expect(fn).toHaveBeenCalled();
|
||||
|
||||
// Sorted, filtered rooms should be 27 and 75
|
||||
const result = store.getSortedRoomsInActiveSpace([FilterKey.FavouriteFilter]).rooms;
|
||||
const result = store
|
||||
.getSortedRoomsInActiveSpace([FilterEnum.FavouriteFilter])
|
||||
.sections.flatMap((s) => s.rooms);
|
||||
expect(result).toHaveLength(2);
|
||||
for (const i of [8, 75]) {
|
||||
expect(result).toContain(rooms[i]);
|
||||
@@ -594,7 +609,9 @@ describe("RoomListStoreV3", () => {
|
||||
await store.start();
|
||||
|
||||
// Should only give us rooms at index 8 and 27
|
||||
const result = store.getSortedRoomsInActiveSpace([FilterKey.UnreadFilter]).rooms;
|
||||
const result = store
|
||||
.getSortedRoomsInActiveSpace([FilterEnum.UnreadFilter])
|
||||
.sections.flatMap((s) => s.rooms);
|
||||
expect(result).toHaveLength(2);
|
||||
for (const i of [8, 27]) {
|
||||
expect(result).toContain(rooms[i]);
|
||||
@@ -611,7 +628,9 @@ describe("RoomListStoreV3", () => {
|
||||
await store.start();
|
||||
|
||||
// Since there's no unread yet, we expect zero results
|
||||
let result = store.getSortedRoomsInActiveSpace([FilterKey.UnreadFilter]).rooms;
|
||||
let result = store
|
||||
.getSortedRoomsInActiveSpace([FilterEnum.UnreadFilter])
|
||||
.sections.flatMap((s) => s.rooms);
|
||||
expect(result).toHaveLength(0);
|
||||
|
||||
// Mock so that room at index 8 is marked as unread
|
||||
@@ -626,7 +645,7 @@ describe("RoomListStoreV3", () => {
|
||||
);
|
||||
|
||||
// Now we expect room at index 8 to show as unread
|
||||
result = store.getSortedRoomsInActiveSpace([FilterKey.UnreadFilter]).rooms;
|
||||
result = store.getSortedRoomsInActiveSpace([FilterEnum.UnreadFilter]).sections.flatMap((s) => s.rooms);
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result).toContain(rooms[8]);
|
||||
});
|
||||
@@ -649,14 +668,18 @@ describe("RoomListStoreV3", () => {
|
||||
await store.start();
|
||||
|
||||
// Should only give us rooms at index 8 and 27
|
||||
const peopleRooms = store.getSortedRoomsInActiveSpace([FilterKey.PeopleFilter]).rooms;
|
||||
const peopleRooms = store
|
||||
.getSortedRoomsInActiveSpace([FilterEnum.PeopleFilter])
|
||||
.sections.flatMap((s) => s.rooms);
|
||||
expect(peopleRooms).toHaveLength(2);
|
||||
for (const i of [8, 27]) {
|
||||
expect(peopleRooms).toContain(rooms[i]);
|
||||
}
|
||||
|
||||
// Rest are normal rooms
|
||||
const nonDms = store.getSortedRoomsInActiveSpace([FilterKey.RoomsFilter]).rooms;
|
||||
const nonDms = store
|
||||
.getSortedRoomsInActiveSpace([FilterEnum.RoomsFilter])
|
||||
.sections.flatMap((s) => s.rooms);
|
||||
expect(nonDms).toHaveLength(3);
|
||||
for (const i of [6, 13, 75]) {
|
||||
expect(nonDms).toContain(rooms[i]);
|
||||
@@ -680,7 +703,9 @@ describe("RoomListStoreV3", () => {
|
||||
const store = new RoomListStoreV3Class(dispatcher);
|
||||
await store.start();
|
||||
|
||||
const result = store.getSortedRoomsInActiveSpace([FilterKey.InvitesFilter]).rooms;
|
||||
const result = store
|
||||
.getSortedRoomsInActiveSpace([FilterEnum.InvitesFilter])
|
||||
.sections.flatMap((s) => s.rooms);
|
||||
expect(result).toHaveLength(5);
|
||||
for (const room of invitedRooms) {
|
||||
expect(result).toContain(room);
|
||||
@@ -705,7 +730,9 @@ describe("RoomListStoreV3", () => {
|
||||
await store.start();
|
||||
|
||||
// Should only give us rooms at index 8 and 27
|
||||
const result = store.getSortedRoomsInActiveSpace([FilterKey.MentionsFilter]).rooms;
|
||||
const result = store
|
||||
.getSortedRoomsInActiveSpace([FilterEnum.MentionsFilter])
|
||||
.sections.flatMap((s) => s.rooms);
|
||||
expect(result).toHaveLength(2);
|
||||
for (const i of [8, 27]) {
|
||||
expect(result).toContain(rooms[i]);
|
||||
@@ -727,7 +754,9 @@ describe("RoomListStoreV3", () => {
|
||||
await store.start();
|
||||
|
||||
// Sorted, filtered rooms should be 8, 27 and 75
|
||||
const result = store.getSortedRoomsInActiveSpace([FilterKey.LowPriorityFilter]).rooms;
|
||||
const result = store
|
||||
.getSortedRoomsInActiveSpace([FilterEnum.LowPriorityFilter])
|
||||
.sections.flatMap((s) => s.rooms);
|
||||
expect(result).toHaveLength(3);
|
||||
for (const i of [8, 27, 75]) {
|
||||
expect(result).toContain(rooms[i]);
|
||||
@@ -755,10 +784,9 @@ describe("RoomListStoreV3", () => {
|
||||
await store.start();
|
||||
|
||||
// Should give us only room at 8 since that's the only room which matches both filters
|
||||
const result = store.getSortedRoomsInActiveSpace([
|
||||
FilterKey.UnreadFilter,
|
||||
FilterKey.FavouriteFilter,
|
||||
]).rooms;
|
||||
const result = store
|
||||
.getSortedRoomsInActiveSpace([FilterEnum.UnreadFilter, FilterEnum.FavouriteFilter])
|
||||
.sections.flatMap((s) => s.rooms);
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result).toContain(rooms[8]);
|
||||
});
|
||||
@@ -777,7 +805,9 @@ describe("RoomListStoreV3", () => {
|
||||
},
|
||||
true,
|
||||
);
|
||||
expect(store.getSortedRoomsInActiveSpace([FilterKey.InvitesFilter]).rooms).not.toContain(room);
|
||||
expect(
|
||||
store.getSortedRoomsInActiveSpace([FilterEnum.InvitesFilter]).sections.flatMap((s) => s.rooms),
|
||||
).not.toContain(room);
|
||||
|
||||
room.getMyMembership = jest.fn().mockReturnValue(KnownMembership.Invite);
|
||||
dispatcher.dispatch(
|
||||
@@ -789,11 +819,196 @@ describe("RoomListStoreV3", () => {
|
||||
},
|
||||
true,
|
||||
);
|
||||
expect(store.getSortedRoomsInActiveSpace([FilterKey.InvitesFilter]).rooms).toContain(room);
|
||||
expect(
|
||||
store.getSortedRoomsInActiveSpace([FilterEnum.InvitesFilter]).sections.flatMap((s) => s.rooms),
|
||||
).toContain(room);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Sections", () => {
|
||||
function enableSections(): void {
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting: string) => {
|
||||
if (setting === "feature_room_list_sections") return true;
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
function findSection(sections: Section[], tag: string): Section | undefined {
|
||||
return sections.find((s) => s.tag === tag);
|
||||
}
|
||||
|
||||
function getClientAndRooms() {
|
||||
const client = stubClient();
|
||||
const rooms = getMockedRooms(client);
|
||||
client.getVisibleRooms = jest.fn().mockReturnValue(rooms);
|
||||
jest.spyOn(AsyncStoreWithClient.prototype, "matrixClient", "get").mockReturnValue(client);
|
||||
return { client, rooms };
|
||||
}
|
||||
|
||||
it("returns a single chats section when sections feature is disabled", async () => {
|
||||
const { rooms } = getClientAndRooms();
|
||||
// Mark some rooms as favourite so we can verify they are NOT split out
|
||||
[0, 1, 2].forEach((i) => {
|
||||
rooms[i].tags[DefaultTagID.Favourite] = {};
|
||||
});
|
||||
|
||||
const store = new RoomListStoreV3Class(dispatcher);
|
||||
await store.start();
|
||||
|
||||
const result = store.getSortedRoomsInActiveSpace();
|
||||
expect(result.sections).toHaveLength(1);
|
||||
expect(result.sections[0].tag).toBe(CHATS_TAG);
|
||||
// All rooms, including favourites, are in the single section
|
||||
for (const i of [0, 1, 2]) {
|
||||
expect(result.sections[0].rooms).toContain(rooms[i]);
|
||||
}
|
||||
});
|
||||
|
||||
it("returns three sections in the correct order when enabled", async () => {
|
||||
enableSections();
|
||||
getClientAndRooms();
|
||||
|
||||
const store = new RoomListStoreV3Class(dispatcher);
|
||||
await store.start();
|
||||
|
||||
const result = store.getSortedRoomsInActiveSpace();
|
||||
expect(result.sections).toHaveLength(3);
|
||||
expect(result.sections[0].tag).toBe(DefaultTagID.Favourite);
|
||||
expect(result.sections[1].tag).toBe(CHATS_TAG);
|
||||
expect(result.sections[2].tag).toBe(DefaultTagID.LowPriority);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ tag: DefaultTagID.Favourite, label: "Favourite" },
|
||||
{ tag: DefaultTagID.LowPriority, label: "LowPriority" },
|
||||
])("places tagged rooms only in the $label section", async ({ tag }) => {
|
||||
enableSections();
|
||||
const { rooms } = getClientAndRooms();
|
||||
|
||||
// Mark rooms 3, 7 with the given tag
|
||||
[3, 7].forEach((i) => {
|
||||
rooms[i].tags[tag] = {};
|
||||
});
|
||||
|
||||
const store = new RoomListStoreV3Class(dispatcher);
|
||||
await store.start();
|
||||
|
||||
const { sections } = store.getSortedRoomsInActiveSpace();
|
||||
const targetSection = findSection(sections, tag)!;
|
||||
const chatsSection = findSection(sections, CHATS_TAG)!;
|
||||
|
||||
for (const i of [3, 7]) {
|
||||
expect(targetSection.rooms).toContain(rooms[i]);
|
||||
expect(chatsSection.rooms).not.toContain(rooms[i]);
|
||||
}
|
||||
});
|
||||
|
||||
it("places regular rooms only in the Chats section", async () => {
|
||||
enableSections();
|
||||
const { rooms } = getClientAndRooms();
|
||||
|
||||
// Mark some rooms as favourite / low priority so the rest are regular
|
||||
rooms[0].tags[DefaultTagID.Favourite] = {};
|
||||
rooms[1].tags[DefaultTagID.LowPriority] = {};
|
||||
|
||||
const store = new RoomListStoreV3Class(dispatcher);
|
||||
await store.start();
|
||||
|
||||
const { sections } = store.getSortedRoomsInActiveSpace();
|
||||
const favSection = findSection(sections, DefaultTagID.Favourite)!;
|
||||
const chatsSection = findSection(sections, CHATS_TAG)!;
|
||||
const lowPrioritySection = findSection(sections, DefaultTagID.LowPriority)!;
|
||||
|
||||
// A regular room (index 5) should be in chats only
|
||||
expect(chatsSection.rooms).toContain(rooms[5]);
|
||||
expect(favSection.rooms).not.toContain(rooms[5]);
|
||||
expect(lowPrioritySection.rooms).not.toContain(rooms[5]);
|
||||
});
|
||||
|
||||
it("all rooms are accounted for across all sections", async () => {
|
||||
enableSections();
|
||||
const { rooms } = getClientAndRooms();
|
||||
|
||||
[2, 5].forEach((i) => {
|
||||
rooms[i].tags[DefaultTagID.Favourite] = {};
|
||||
});
|
||||
[11].forEach((i) => {
|
||||
rooms[i].tags[DefaultTagID.LowPriority] = {};
|
||||
});
|
||||
|
||||
const store = new RoomListStoreV3Class(dispatcher);
|
||||
await store.start();
|
||||
|
||||
const { sections } = store.getSortedRoomsInActiveSpace();
|
||||
const totalRooms = sections.flatMap((s) => s.rooms).length;
|
||||
// All 100 rooms should be distributed across the three sections
|
||||
expect(totalRooms).toBe(rooms.length);
|
||||
});
|
||||
|
||||
it("applies additional filter keys within each section", async () => {
|
||||
enableSections();
|
||||
const { rooms } = getClientAndRooms();
|
||||
|
||||
// Rooms 3 and 7 are favourites; room 7 is also unread
|
||||
[3, 7].forEach((i) => {
|
||||
rooms[i].tags[DefaultTagID.Favourite] = {};
|
||||
});
|
||||
jest.spyOn(RoomNotificationStateStore.instance, "getRoomState").mockImplementation((room) => {
|
||||
const state = {
|
||||
hasUnreadCount: room === rooms[7],
|
||||
} as unknown as RoomNotificationState;
|
||||
return state;
|
||||
});
|
||||
|
||||
const store = new RoomListStoreV3Class(dispatcher);
|
||||
await store.start();
|
||||
|
||||
const { sections } = store.getSortedRoomsInActiveSpace([FilterEnum.UnreadFilter]);
|
||||
const favSection = findSection(sections, DefaultTagID.Favourite)!;
|
||||
|
||||
// Only room 7 is both favourite AND unread
|
||||
expect(favSection.rooms).toHaveLength(1);
|
||||
expect(favSection.rooms).toContain(rooms[7]);
|
||||
});
|
||||
|
||||
it("sections respect space filtering", async () => {
|
||||
enableSections();
|
||||
const { rooms } = getClientAndRooms();
|
||||
|
||||
// Room 3 is a favourite room in the space
|
||||
rooms[3].tags[DefaultTagID.Favourite] = {};
|
||||
|
||||
const spaceRoomId = "!space1:matrix.org";
|
||||
const inSpaceIds = [3, 10, 20].map((i) => rooms[i].roomId);
|
||||
jest.spyOn(SpaceStore.instance, "isRoomInSpace").mockImplementation((space, id) => {
|
||||
if (space === spaceRoomId && inSpaceIds.includes(id)) return true;
|
||||
return false;
|
||||
});
|
||||
jest.spyOn(SpaceStore.instance, "activeSpace", "get").mockImplementation(() => spaceRoomId);
|
||||
|
||||
const store = new RoomListStoreV3Class(dispatcher);
|
||||
await store.start();
|
||||
|
||||
const { sections, spaceId } = store.getSortedRoomsInActiveSpace();
|
||||
expect(spaceId).toBe(spaceRoomId);
|
||||
|
||||
const allRooms = sections.flatMap((s) => s.rooms);
|
||||
const allRoomIds = allRooms.map((r) => r.roomId);
|
||||
|
||||
// Only rooms in the space should appear
|
||||
for (const id of inSpaceIds) {
|
||||
expect(allRoomIds).toContain(id);
|
||||
}
|
||||
// Rooms not in the space should not appear
|
||||
expect(allRoomIds).not.toContain(rooms[50].roomId);
|
||||
|
||||
// Room 3 should be in the Favourite section specifically
|
||||
const favSection = findSection(sections, DefaultTagID.Favourite)!;
|
||||
expect(favSection.rooms).toContain(rooms[3]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Muted rooms", () => {
|
||||
async function getRoomListStoreWithMutedRooms() {
|
||||
const client = stubClient();
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { RoomListSectionHeaderViewModel } from "../../../src/viewmodels/room-list/RoomListSectionHeaderViewModel";
|
||||
|
||||
describe("RoomListSectionHeaderViewModel", () => {
|
||||
let onToggleExpanded: jest.Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
onToggleExpanded = jest.fn();
|
||||
});
|
||||
|
||||
it("should initialize snapshot from props", () => {
|
||||
const vm = new RoomListSectionHeaderViewModel({
|
||||
tag: "m.favourite",
|
||||
title: "Favourites",
|
||||
onToggleExpanded,
|
||||
});
|
||||
|
||||
const snapshot = vm.getSnapshot();
|
||||
expect(snapshot.id).toBe("m.favourite");
|
||||
expect(snapshot.title).toBe("Favourites");
|
||||
expect(snapshot.isExpanded).toBe(true);
|
||||
});
|
||||
|
||||
it("should toggle expanded state on click", () => {
|
||||
const vm = new RoomListSectionHeaderViewModel({
|
||||
tag: "m.favourite",
|
||||
title: "Favourites",
|
||||
onToggleExpanded,
|
||||
});
|
||||
expect(vm.isExpanded).toBe(true);
|
||||
|
||||
vm.onClick();
|
||||
expect(vm.isExpanded).toBe(false);
|
||||
expect(vm.getSnapshot().isExpanded).toBe(false);
|
||||
expect(onToggleExpanded).toHaveBeenCalledWith(false);
|
||||
|
||||
vm.onClick();
|
||||
expect(vm.isExpanded).toBe(true);
|
||||
expect(vm.getSnapshot().isExpanded).toBe(true);
|
||||
expect(onToggleExpanded).toHaveBeenCalledWith(true);
|
||||
});
|
||||
});
|
||||
@@ -7,17 +7,20 @@
|
||||
|
||||
import { type MatrixClient, type Room } from "matrix-js-sdk/src/matrix";
|
||||
import { mocked } from "jest-mock";
|
||||
import { waitFor } from "jest-matrix-react";
|
||||
|
||||
import { createTestClient, flushPromises, mkStubRoom, stubClient } from "../../test-utils";
|
||||
import RoomListStoreV3, { RoomListStoreV3Event } from "../../../src/stores/room-list-v3/RoomListStoreV3";
|
||||
import RoomListStoreV3, { CHATS_TAG, RoomListStoreV3Event } from "../../../src/stores/room-list-v3/RoomListStoreV3";
|
||||
import SpaceStore from "../../../src/stores/spaces/SpaceStore";
|
||||
import { FilterKey } from "../../../src/stores/room-list-v3/skip-list/filters";
|
||||
import { FilterEnum } from "../../../src/stores/room-list-v3/skip-list/filters";
|
||||
import dispatcher from "../../../src/dispatcher/dispatcher";
|
||||
import { Action } from "../../../src/dispatcher/actions";
|
||||
import { SdkContextClass } from "../../../src/contexts/SDKContext";
|
||||
import DMRoomMap from "../../../src/utils/DMRoomMap";
|
||||
import { RoomListViewModel } from "../../../src/viewmodels/room-list/RoomListViewModel";
|
||||
import { hasCreateRoomRights } from "../../../src/viewmodels/room-list/utils";
|
||||
import { DefaultTagID } from "../../../src/stores/room-list-v3/skip-list/tag";
|
||||
import SettingsStore from "../../../src/settings/SettingsStore";
|
||||
|
||||
jest.mock("../../../src/viewmodels/room-list/utils", () => ({
|
||||
hasCreateRoomRights: jest.fn().mockReturnValue(false),
|
||||
@@ -46,7 +49,7 @@ describe("RoomListViewModel", () => {
|
||||
|
||||
jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({
|
||||
spaceId: "home",
|
||||
rooms: [room1, room2, room3],
|
||||
sections: [{ tag: CHATS_TAG, rooms: [room1, room2, room3] }],
|
||||
});
|
||||
|
||||
jest.spyOn(RoomListStoreV3.instance, "isLoadingRooms", "get").mockReturnValue(false);
|
||||
@@ -77,12 +80,12 @@ describe("RoomListViewModel", () => {
|
||||
it("should initialize with empty room list", () => {
|
||||
jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({
|
||||
spaceId: "home",
|
||||
rooms: [],
|
||||
sections: [{ tag: CHATS_TAG, rooms: [] }],
|
||||
});
|
||||
|
||||
viewModel = new RoomListViewModel({ client: matrixClient });
|
||||
|
||||
expect(viewModel.getSnapshot().sections[0].roomIds).toEqual([]);
|
||||
expect(viewModel.getSnapshot().sections).toEqual([]);
|
||||
expect(viewModel.getSnapshot().isRoomListEmpty).toBe(true);
|
||||
});
|
||||
|
||||
@@ -101,7 +104,7 @@ describe("RoomListViewModel", () => {
|
||||
const newRoom = mkStubRoom("!room4:server", "Room 4", matrixClient);
|
||||
jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({
|
||||
spaceId: "home",
|
||||
rooms: [room1, room2, room3, newRoom],
|
||||
sections: [{ tag: CHATS_TAG, rooms: [room1, room2, room3, newRoom] }],
|
||||
});
|
||||
|
||||
RoomListStoreV3.instance.emit(RoomListStoreV3Event.ListsUpdate);
|
||||
@@ -136,7 +139,7 @@ describe("RoomListViewModel", () => {
|
||||
RoomListStoreV3.instance.emit(RoomListStoreV3Event.ListsUpdate);
|
||||
|
||||
// View model should be still valid
|
||||
expect(room1VM.isDisposed).toBe(false);
|
||||
expect(room1VM!.isDisposed).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -148,7 +151,7 @@ describe("RoomListViewModel", () => {
|
||||
|
||||
jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({
|
||||
spaceId: "!space:server",
|
||||
rooms: spaceRoomList,
|
||||
sections: [{ tag: CHATS_TAG, rooms: spaceRoomList }],
|
||||
});
|
||||
|
||||
jest.spyOn(SpaceStore.instance, "getLastSelectedRoomIdForSpace").mockReturnValue("!room1:server");
|
||||
@@ -163,8 +166,8 @@ describe("RoomListViewModel", () => {
|
||||
viewModel = new RoomListViewModel({ client: matrixClient });
|
||||
|
||||
// Get view models for visible rooms
|
||||
const vm1 = viewModel.getRoomItemViewModel("!room1:server");
|
||||
const vm2 = viewModel.getRoomItemViewModel("!room2:server");
|
||||
const vm1 = viewModel.getRoomItemViewModel("!room1:server")!;
|
||||
const vm2 = viewModel.getRoomItemViewModel("!room2:server")!;
|
||||
|
||||
const disposeSpy1 = jest.spyOn(vm1, "dispose");
|
||||
const disposeSpy2 = jest.spyOn(vm2, "dispose");
|
||||
@@ -172,7 +175,7 @@ describe("RoomListViewModel", () => {
|
||||
// Change space
|
||||
jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({
|
||||
spaceId: "!space:server",
|
||||
rooms: [room3],
|
||||
sections: [{ tag: CHATS_TAG, rooms: [room3] }],
|
||||
});
|
||||
|
||||
RoomListStoreV3.instance.emit(RoomListStoreV3Event.ListsUpdate);
|
||||
@@ -188,7 +191,7 @@ describe("RoomListViewModel", () => {
|
||||
|
||||
jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({
|
||||
spaceId: "!space:server",
|
||||
rooms: [newSpaceRoom],
|
||||
sections: [{ tag: CHATS_TAG, rooms: [newSpaceRoom] }],
|
||||
});
|
||||
jest.spyOn(SpaceStore.instance, "getLastSelectedRoomIdForSpace").mockReturnValue(null);
|
||||
|
||||
@@ -197,7 +200,7 @@ describe("RoomListViewModel", () => {
|
||||
// New space room should be accessible
|
||||
expect(() => viewModel.getRoomItemViewModel("!spaceroom:server")).not.toThrow();
|
||||
// Old rooms from the home space should not be accessible
|
||||
expect(() => viewModel.getRoomItemViewModel("!room1:server")).toThrow();
|
||||
expect(viewModel.getRoomItemViewModel("!room1:server")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -252,7 +255,7 @@ describe("RoomListViewModel", () => {
|
||||
// Simulate room list update that would move room2 to front
|
||||
jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({
|
||||
spaceId: "home",
|
||||
rooms: [room2, room1, room3], // room2 moved to front
|
||||
sections: [{ tag: CHATS_TAG, rooms: [room2, room1, room3] }], // room2 moved to front
|
||||
});
|
||||
|
||||
RoomListStoreV3.instance.emit(RoomListStoreV3Event.ListsUpdate);
|
||||
@@ -295,8 +298,8 @@ describe("RoomListViewModel", () => {
|
||||
|
||||
jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({
|
||||
spaceId: "home",
|
||||
rooms: [room1],
|
||||
filterKeys: [FilterKey.UnreadFilter],
|
||||
sections: [{ tag: CHATS_TAG, rooms: [room1] }],
|
||||
filterKeys: [FilterEnum.UnreadFilter],
|
||||
});
|
||||
|
||||
viewModel.onToggleFilter("unread");
|
||||
@@ -311,8 +314,8 @@ describe("RoomListViewModel", () => {
|
||||
// Turn filter on
|
||||
jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({
|
||||
spaceId: "home",
|
||||
rooms: [room1],
|
||||
filterKeys: [FilterKey.UnreadFilter],
|
||||
sections: [{ tag: CHATS_TAG, rooms: [room1] }],
|
||||
filterKeys: [FilterEnum.UnreadFilter],
|
||||
});
|
||||
viewModel.onToggleFilter("unread");
|
||||
|
||||
@@ -321,7 +324,7 @@ describe("RoomListViewModel", () => {
|
||||
// Turn filter off
|
||||
jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({
|
||||
spaceId: "home",
|
||||
rooms: [room1, room2, room3],
|
||||
sections: [{ tag: CHATS_TAG, rooms: [room1, room2, room3] }],
|
||||
});
|
||||
viewModel.onToggleFilter("unread");
|
||||
|
||||
@@ -341,7 +344,7 @@ describe("RoomListViewModel", () => {
|
||||
const itemViewModel = viewModel.getRoomItemViewModel("!room1:server");
|
||||
|
||||
expect(itemViewModel).toBeDefined();
|
||||
expect(itemViewModel.getSnapshot().room).toBe(room1);
|
||||
expect(itemViewModel!.getSnapshot().room).toBe(room1);
|
||||
});
|
||||
|
||||
it("should reuse existing room item view model", () => {
|
||||
@@ -353,12 +356,10 @@ describe("RoomListViewModel", () => {
|
||||
expect(itemViewModel1).toBe(itemViewModel2);
|
||||
});
|
||||
|
||||
it("should throw error when requesting view model for non-existent room", () => {
|
||||
it("should return undefined for non-existent room", () => {
|
||||
viewModel = new RoomListViewModel({ client: matrixClient });
|
||||
|
||||
expect(() => {
|
||||
viewModel.getRoomItemViewModel("!nonexistent:server");
|
||||
}).toThrow();
|
||||
expect(viewModel.getRoomItemViewModel("!nonexistent:server")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should not throw when requesting view model for a room removed from the list but still in roomsMap", () => {
|
||||
@@ -367,7 +368,7 @@ describe("RoomListViewModel", () => {
|
||||
// Normal list update removes room2 from the list
|
||||
jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({
|
||||
spaceId: "home",
|
||||
rooms: [room1, room3],
|
||||
sections: [{ tag: CHATS_TAG, rooms: [room1, room3] }],
|
||||
});
|
||||
|
||||
RoomListStoreV3.instance.emit(RoomListStoreV3Event.ListsUpdate);
|
||||
@@ -375,7 +376,7 @@ describe("RoomListViewModel", () => {
|
||||
expect(() => viewModel.getRoomItemViewModel("!room2:server")).not.toThrow();
|
||||
});
|
||||
|
||||
it("should throw when requesting view model for a room from old space after space change", () => {
|
||||
it("should return undefined for a room from old space after space change", () => {
|
||||
viewModel = new RoomListViewModel({ client: matrixClient });
|
||||
|
||||
const spaceRoom = mkStubRoom("!newroom:server", "New Room", matrixClient);
|
||||
@@ -383,15 +384,13 @@ describe("RoomListViewModel", () => {
|
||||
// Space change: new space only has spaceRoom
|
||||
jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({
|
||||
spaceId: "!space:server",
|
||||
rooms: [spaceRoom],
|
||||
sections: [{ tag: CHATS_TAG, rooms: [spaceRoom] }],
|
||||
});
|
||||
jest.spyOn(SpaceStore.instance, "getLastSelectedRoomIdForSpace").mockReturnValue(null);
|
||||
|
||||
RoomListStoreV3.instance.emit(RoomListStoreV3Event.ListsUpdate);
|
||||
|
||||
expect(() => viewModel.getRoomItemViewModel("!room1:server")).toThrow(
|
||||
"Room !room1:server not found in roomsMap",
|
||||
);
|
||||
expect(viewModel.getRoomItemViewModel("!room1:server")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should recover when roomsMap is stale but roomsResult has the room", () => {
|
||||
@@ -407,9 +406,9 @@ describe("RoomListViewModel", () => {
|
||||
it("should dispose view models for rooms no longer visible", () => {
|
||||
viewModel = new RoomListViewModel({ client: matrixClient });
|
||||
|
||||
const vm1 = viewModel.getRoomItemViewModel("!room1:server");
|
||||
const vm2 = viewModel.getRoomItemViewModel("!room2:server");
|
||||
const vm3 = viewModel.getRoomItemViewModel("!room3:server");
|
||||
const vm1 = viewModel.getRoomItemViewModel("!room1:server")!;
|
||||
const vm2 = viewModel.getRoomItemViewModel("!room2:server")!;
|
||||
const vm3 = viewModel.getRoomItemViewModel("!room3:server")!;
|
||||
|
||||
const disposeSpy1 = jest.spyOn(vm1, "dispose");
|
||||
const disposeSpy3 = jest.spyOn(vm3, "dispose");
|
||||
@@ -593,8 +592,8 @@ describe("RoomListViewModel", () => {
|
||||
it("should dispose all room item view models on dispose", () => {
|
||||
viewModel = new RoomListViewModel({ client: matrixClient });
|
||||
|
||||
const vm1 = viewModel.getRoomItemViewModel("!room1:server");
|
||||
const vm2 = viewModel.getRoomItemViewModel("!room2:server");
|
||||
const vm1 = viewModel.getRoomItemViewModel("!room1:server")!;
|
||||
const vm2 = viewModel.getRoomItemViewModel("!room2:server")!;
|
||||
|
||||
const disposeSpy1 = jest.spyOn(vm1, "dispose");
|
||||
const disposeSpy2 = jest.spyOn(vm2, "dispose");
|
||||
@@ -604,5 +603,297 @@ describe("RoomListViewModel", () => {
|
||||
expect(disposeSpy1).toHaveBeenCalled();
|
||||
expect(disposeSpy2).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe("Sections (feature_room_list_sections)", () => {
|
||||
let favRoom1: Room;
|
||||
let favRoom2: Room;
|
||||
let lowPriorityRoom: Room;
|
||||
let regularRoom1: Room;
|
||||
let regularRoom2: Room;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting: string) => {
|
||||
if (setting === "feature_room_list_sections") return true;
|
||||
return false;
|
||||
});
|
||||
|
||||
favRoom1 = mkStubRoom("!fav1:server", "Fav 1", matrixClient);
|
||||
favRoom2 = mkStubRoom("!fav2:server", "Fav 2", matrixClient);
|
||||
lowPriorityRoom = mkStubRoom("!low1:server", "Low 1", matrixClient);
|
||||
regularRoom1 = mkStubRoom("!reg1:server", "Reg 1", matrixClient);
|
||||
regularRoom2 = mkStubRoom("!reg2:server", "Reg 2", matrixClient);
|
||||
|
||||
jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({
|
||||
spaceId: "home",
|
||||
sections: [
|
||||
{ tag: DefaultTagID.Favourite, rooms: [favRoom1, favRoom2] },
|
||||
{ tag: CHATS_TAG, rooms: [regularRoom1, regularRoom2] },
|
||||
{ tag: DefaultTagID.LowPriority, rooms: [lowPriorityRoom] },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("should initialize with multiple sections", () => {
|
||||
viewModel = new RoomListViewModel({ client: matrixClient });
|
||||
|
||||
const snapshot = viewModel.getSnapshot();
|
||||
expect(snapshot.sections).toHaveLength(3);
|
||||
expect(snapshot.sections[0].id).toBe(DefaultTagID.Favourite);
|
||||
expect(snapshot.sections[0].roomIds).toEqual(["!fav1:server", "!fav2:server"]);
|
||||
expect(snapshot.sections[1].id).toBe(CHATS_TAG);
|
||||
expect(snapshot.sections[1].roomIds).toEqual(["!reg1:server", "!reg2:server"]);
|
||||
expect(snapshot.sections[2].id).toBe(DefaultTagID.LowPriority);
|
||||
expect(snapshot.sections[2].roomIds).toEqual(["!low1:server"]);
|
||||
});
|
||||
|
||||
it("should not be a flat list when multiple sections exist", () => {
|
||||
viewModel = new RoomListViewModel({ client: matrixClient });
|
||||
|
||||
expect(viewModel.getSnapshot().isFlatList).toBe(false);
|
||||
});
|
||||
|
||||
it("should be a flat list when only chats section has rooms", () => {
|
||||
jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({
|
||||
spaceId: "home",
|
||||
sections: [
|
||||
{ tag: DefaultTagID.Favourite, rooms: [] },
|
||||
{ tag: CHATS_TAG, rooms: [regularRoom1] },
|
||||
{ tag: DefaultTagID.LowPriority, rooms: [] },
|
||||
],
|
||||
});
|
||||
|
||||
viewModel = new RoomListViewModel({ client: matrixClient });
|
||||
|
||||
expect(viewModel.getSnapshot().isFlatList).toBe(true);
|
||||
expect(viewModel.getSnapshot().sections).toHaveLength(1);
|
||||
expect(viewModel.getSnapshot().sections[0].id).toBe(CHATS_TAG);
|
||||
});
|
||||
|
||||
it("should exclude favourite and low_priority from filter list", () => {
|
||||
viewModel = new RoomListViewModel({ client: matrixClient });
|
||||
|
||||
const snapshot = viewModel.getSnapshot();
|
||||
expect(snapshot.filterIds).not.toContain("favourite");
|
||||
expect(snapshot.filterIds).not.toContain("low_priority");
|
||||
// Other filters should still be present
|
||||
expect(snapshot.filterIds).toContain("unread");
|
||||
expect(snapshot.filterIds).toContain("people");
|
||||
});
|
||||
|
||||
it("should omit empty sections from snapshot", () => {
|
||||
jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({
|
||||
spaceId: "home",
|
||||
sections: [
|
||||
{ tag: DefaultTagID.Favourite, rooms: [] },
|
||||
{ tag: CHATS_TAG, rooms: [regularRoom1] },
|
||||
{ tag: DefaultTagID.LowPriority, rooms: [] },
|
||||
],
|
||||
});
|
||||
|
||||
viewModel = new RoomListViewModel({ client: matrixClient });
|
||||
|
||||
const snapshot = viewModel.getSnapshot();
|
||||
expect(snapshot.sections).toHaveLength(1);
|
||||
expect(snapshot.sections[0].id).toBe(CHATS_TAG);
|
||||
});
|
||||
|
||||
it("should create section header view models on demand", () => {
|
||||
viewModel = new RoomListViewModel({ client: matrixClient });
|
||||
|
||||
const headerVM = viewModel.getSectionHeaderViewModel(DefaultTagID.Favourite);
|
||||
expect(headerVM).toBeDefined();
|
||||
expect(headerVM.getSnapshot().id).toBe(DefaultTagID.Favourite);
|
||||
expect(headerVM.getSnapshot().isExpanded).toBe(true);
|
||||
});
|
||||
|
||||
it("should reuse section header view models", () => {
|
||||
viewModel = new RoomListViewModel({ client: matrixClient });
|
||||
|
||||
const headerVM1 = viewModel.getSectionHeaderViewModel(DefaultTagID.Favourite);
|
||||
const headerVM2 = viewModel.getSectionHeaderViewModel(DefaultTagID.Favourite);
|
||||
expect(headerVM1).toBe(headerVM2);
|
||||
});
|
||||
|
||||
it("should hide room IDs when a section is collapsed", () => {
|
||||
viewModel = new RoomListViewModel({ client: matrixClient });
|
||||
|
||||
// Collapse the favourite section
|
||||
const favHeader = viewModel.getSectionHeaderViewModel(DefaultTagID.Favourite);
|
||||
favHeader.onClick();
|
||||
expect(favHeader.isExpanded).toBe(false);
|
||||
|
||||
const snapshot = viewModel.getSnapshot();
|
||||
const favSection = snapshot.sections.find((s) => s.id === DefaultTagID.Favourite);
|
||||
expect(favSection).toBeDefined();
|
||||
// Collapsed sections have an empty roomIds list
|
||||
expect(favSection!.roomIds).toEqual([]);
|
||||
|
||||
// Other sections remain unaffected
|
||||
const chatsSection = snapshot.sections.find((s) => s.id === CHATS_TAG);
|
||||
expect(chatsSection!.roomIds).toEqual(["!reg1:server", "!reg2:server"]);
|
||||
});
|
||||
|
||||
it("should compute activeRoomIndex relative to visible rooms when a section is collapsed", async () => {
|
||||
viewModel = new RoomListViewModel({ client: matrixClient });
|
||||
|
||||
// Collapse the favourite section (which has 2 rooms: fav1, fav2)
|
||||
const favHeader = viewModel.getSectionHeaderViewModel(DefaultTagID.Favourite);
|
||||
favHeader.onClick();
|
||||
expect(favHeader.isExpanded).toBe(false);
|
||||
|
||||
// Select regularRoom1, which is the first room in the chats section
|
||||
jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockReturnValue("!reg1:server");
|
||||
dispatcher.dispatch({
|
||||
action: Action.ActiveRoomChanged,
|
||||
newRoomId: "!reg1:server",
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
const snapshot = viewModel.getSnapshot();
|
||||
// The favourite section is collapsed so its 2 rooms are not visible.
|
||||
// regularRoom1 should be at index 0 in the visible list, not index 2.
|
||||
expect(snapshot.roomListState.activeRoomIndex).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
it("should restore room IDs when a section is re-expanded", () => {
|
||||
viewModel = new RoomListViewModel({ client: matrixClient });
|
||||
|
||||
const favHeader = viewModel.getSectionHeaderViewModel(DefaultTagID.Favourite);
|
||||
|
||||
// Collapse then re-expand
|
||||
favHeader.onClick();
|
||||
favHeader.onClick();
|
||||
expect(favHeader.isExpanded).toBe(true);
|
||||
|
||||
const snapshot = viewModel.getSnapshot();
|
||||
const favSection = snapshot.sections.find((s) => s.id === DefaultTagID.Favourite);
|
||||
expect(favSection!.roomIds).toEqual(["!fav1:server", "!fav2:server"]);
|
||||
});
|
||||
|
||||
it("should update sections when room list changes", () => {
|
||||
viewModel = new RoomListViewModel({ client: matrixClient });
|
||||
|
||||
const newFav = mkStubRoom("!fav3:server", "Fav 3", matrixClient);
|
||||
|
||||
jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({
|
||||
spaceId: "home",
|
||||
sections: [
|
||||
{ tag: DefaultTagID.Favourite, rooms: [favRoom1, favRoom2, newFav] },
|
||||
{ tag: CHATS_TAG, rooms: [regularRoom1, regularRoom2] },
|
||||
{ tag: DefaultTagID.LowPriority, rooms: [lowPriorityRoom] },
|
||||
],
|
||||
});
|
||||
|
||||
RoomListStoreV3.instance.emit(RoomListStoreV3Event.ListsUpdate);
|
||||
|
||||
const snapshot = viewModel.getSnapshot();
|
||||
expect(snapshot.sections[0].roomIds).toEqual(["!fav1:server", "!fav2:server", "!fav3:server"]);
|
||||
});
|
||||
|
||||
it("should preserve section collapse state across list updates", () => {
|
||||
viewModel = new RoomListViewModel({ client: matrixClient });
|
||||
|
||||
// Collapse favourites
|
||||
const favHeader = viewModel.getSectionHeaderViewModel(DefaultTagID.Favourite);
|
||||
favHeader.onClick();
|
||||
|
||||
// Trigger a list update
|
||||
RoomListStoreV3.instance.emit(RoomListStoreV3Event.ListsUpdate);
|
||||
|
||||
const snapshot = viewModel.getSnapshot();
|
||||
const favSection = snapshot.sections.find((s) => s.id === DefaultTagID.Favourite);
|
||||
expect(favSection!.roomIds).toEqual([]);
|
||||
});
|
||||
|
||||
it("should preserve section collapse state across space changes", () => {
|
||||
viewModel = new RoomListViewModel({ client: matrixClient });
|
||||
|
||||
// Collapse favourites
|
||||
const favHeader = viewModel.getSectionHeaderViewModel(DefaultTagID.Favourite);
|
||||
favHeader.onClick();
|
||||
|
||||
// Switch to a different space with its own rooms
|
||||
const spaceFav = mkStubRoom("!spacefav:server", "Space Fav", matrixClient);
|
||||
const spaceReg = mkStubRoom("!spacereg:server", "Space Reg", matrixClient);
|
||||
jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({
|
||||
spaceId: "!space:server",
|
||||
sections: [
|
||||
{ tag: DefaultTagID.Favourite, rooms: [spaceFav] },
|
||||
{ tag: CHATS_TAG, rooms: [spaceReg] },
|
||||
{ tag: DefaultTagID.LowPriority, rooms: [] },
|
||||
],
|
||||
});
|
||||
jest.spyOn(SpaceStore.instance, "getLastSelectedRoomIdForSpace").mockReturnValue(null);
|
||||
|
||||
RoomListStoreV3.instance.emit(RoomListStoreV3Event.ListsUpdate);
|
||||
|
||||
const snapshot = viewModel.getSnapshot();
|
||||
// Favourites should still be collapsed even after the space change
|
||||
const favSection = snapshot.sections.find((s) => s.id === DefaultTagID.Favourite);
|
||||
expect(favSection).toBeDefined();
|
||||
expect(favSection!.roomIds).toEqual([]);
|
||||
|
||||
// Other sections should remain expanded
|
||||
const chatsSection = snapshot.sections.find((s) => s.id === CHATS_TAG);
|
||||
expect(chatsSection!.roomIds).toEqual(["!spacereg:server"]);
|
||||
});
|
||||
|
||||
it("should apply filters across all sections", () => {
|
||||
viewModel = new RoomListViewModel({ client: matrixClient });
|
||||
|
||||
// Only favRoom1 is unread
|
||||
jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({
|
||||
spaceId: "home",
|
||||
sections: [
|
||||
{ tag: DefaultTagID.Favourite, rooms: [favRoom1] },
|
||||
{ tag: CHATS_TAG, rooms: [] },
|
||||
{ tag: DefaultTagID.LowPriority, rooms: [] },
|
||||
],
|
||||
filterKeys: [FilterEnum.UnreadFilter],
|
||||
});
|
||||
|
||||
viewModel.onToggleFilter("unread");
|
||||
|
||||
const snapshot = viewModel.getSnapshot();
|
||||
expect(snapshot.activeFilterId).toBe("unread");
|
||||
// Only the favourite section should remain (chats and low priority are empty)
|
||||
expect(snapshot.sections).toHaveLength(1);
|
||||
expect(snapshot.sections[0].id).toBe(DefaultTagID.Favourite);
|
||||
expect(snapshot.sections[0].roomIds).toEqual(["!fav1:server"]);
|
||||
});
|
||||
|
||||
it("should apply sticky room within the correct section", async () => {
|
||||
stubClient();
|
||||
viewModel = new RoomListViewModel({ client: matrixClient });
|
||||
|
||||
// Select favRoom1 (index 0 globally, index 0 in favourites section)
|
||||
jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockReturnValue("!fav1:server");
|
||||
dispatcher.dispatch({
|
||||
action: Action.ActiveRoomChanged,
|
||||
newRoomId: "!fav1:server",
|
||||
});
|
||||
await flushPromises();
|
||||
|
||||
expect(viewModel.getSnapshot().roomListState.activeRoomIndex).toBe(0);
|
||||
|
||||
// Room list update moves favRoom1 to second position within favourites
|
||||
jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({
|
||||
spaceId: "home",
|
||||
sections: [
|
||||
{ tag: DefaultTagID.Favourite, rooms: [favRoom2, favRoom1] },
|
||||
{ tag: CHATS_TAG, rooms: [regularRoom1, regularRoom2] },
|
||||
{ tag: DefaultTagID.LowPriority, rooms: [lowPriorityRoom] },
|
||||
],
|
||||
});
|
||||
|
||||
RoomListStoreV3.instance.emit(RoomListStoreV3Event.ListsUpdate);
|
||||
|
||||
// Sticky room should keep favRoom1 at index 0 within the favourites section
|
||||
const snapshot = viewModel.getSnapshot();
|
||||
expect(snapshot.sections[0].roomIds[0]).toBe("!fav1:server");
|
||||
expect(snapshot.roomListState.activeRoomIndex).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user