Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ import { type RoomNotificationState } from "../../stores/notifications/RoomNotif
interface RoomListSectionHeaderViewModelProps {
tag: string;
title: string;
/**
* The ID of the current space.
*/
spaceId: string;
onToggleExpanded: (isExpanded: boolean) => void;
}

Expand All @@ -31,12 +35,19 @@ export class RoomListSectionHeaderViewModel
*/
private roomNotificationStates = new Set<RoomNotificationState>();

/**
* Tracks the expanded/collapsed state per space.
* Key is spaceId. Defaults to expanded if not set.
*/
private readonly expandedBySpace = new Map<string, boolean>();

public constructor(props: RoomListSectionHeaderViewModelProps) {
super(props, { id: props.tag, title: props.title, isExpanded: true, isUnread: false });
}

public onClick = (): void => {
const isExpanded = !this.snapshot.current.isExpanded;
this.expandedBySpace.set(this.props.spaceId, isExpanded);
this.snapshot.merge({ isExpanded });
this.props.onToggleExpanded(isExpanded);
};
Expand All @@ -48,6 +59,25 @@ export class RoomListSectionHeaderViewModel
return this.snapshot.current.isExpanded;
}

/**
* Set whether the section is expanded for the current space.
* This will not trigger the onToggleExpanded callback.
*/
public set isExpanded(value: boolean) {
this.expandedBySpace.set(this.props.spaceId, value);
this.snapshot.merge({ isExpanded: value });
}

/**
* Switch to a different space, restoring the expanded state for that space.
* Defaults to expanded if no state has been saved for the space.
*/
public setSpace(spaceId: string): void {
this.props.spaceId = spaceId;
const isExpanded = this.expandedBySpace.get(this.props.spaceId) ?? true;
this.snapshot.merge({ isExpanded });
}

/**
* Update the rooms tracked by this section header for unread state computation.
* Only subscribes to new rooms and unsubscribes from rooms no longer in the section.
Expand Down
12 changes: 12 additions & 0 deletions apps/web/src/viewmodels/room-list/RoomListViewModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,7 @@ export class RoomListViewModel
const viewModel = new RoomListSectionHeaderViewModel({
tag,
title,
spaceId: this.roomsResult.spaceId,
onToggleExpanded: () => this.updateRoomListData(),
});
this.roomSectionHeaderViewModels.set(tag, viewModel);
Expand Down Expand Up @@ -367,6 +368,11 @@ export class RoomListViewModel

this.updateRoomsMap(this.roomsResult);

// Restore the expanded/collapsed state for the new space
for (const viewModel of this.roomSectionHeaderViewModels.values()) {
viewModel.setSpace(newSpaceId);
}

// Space changed - get the last selected room for the new space to prevent flicker
const lastSelectedRoom = SpaceStore.instance.getLastSelectedRoomIdForSpace(newSpaceId);

Expand Down Expand Up @@ -501,6 +507,12 @@ export class RoomListViewModel
this.roomsResult,
(tag) => this.roomSectionHeaderViewModels.get(tag)?.isExpanded ?? true,
);
// If it's a flat list, we need to make sure the single section is expanded and has all rooms, otherwise the room list will be empty
if (isFlatList) {
const chatSections = this.roomSectionHeaderViewModels.get(CHATS_TAG);
if (chatSections) chatSections.isExpanded = true;
chatSections?.setRooms(this.roomsResult.sections.flatMap((section) => section.rooms));
}
this.sections = sections;

// Calculate the active room index from the computed sections (which exclude collapsed sections' rooms)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ describe("RoomListSectionHeaderViewModel", () => {
const vm = new RoomListSectionHeaderViewModel({
tag: "m.favourite",
title: "Favourites",
spaceId: "!space:server",
onToggleExpanded,
});

Expand All @@ -43,6 +44,7 @@ describe("RoomListSectionHeaderViewModel", () => {
const vm = new RoomListSectionHeaderViewModel({
tag: "m.favourite",
title: "Favourites",
spaceId: "!space:server",
onToggleExpanded,
});
expect(vm.isExpanded).toBe(true);
Expand All @@ -58,6 +60,33 @@ describe("RoomListSectionHeaderViewModel", () => {
expect(onToggleExpanded).toHaveBeenCalledWith(true);
});

it("should track expanded state per space", () => {
const vm = new RoomListSectionHeaderViewModel({
tag: "m.favourite",
title: "Favourites",
spaceId: "!space:server",
onToggleExpanded,
});

// Default space: collapse
vm.onClick();
expect(vm.isExpanded).toBe(false);

// Switch to a different space: should default to expanded
vm.setSpace("!space2:server");
expect(vm.isExpanded).toBe(true);

// Collapse in the new space
vm.onClick();
expect(vm.isExpanded).toBe(false);
vm.onClick();
expect(vm.isExpanded).toBe(true);

// Switch to the other space: should still be collapsed
vm.setSpace("!space:server");
expect(vm.isExpanded).toBe(false);
});

describe("unread status", () => {
let room: Room;
let notificationState: RoomNotificationState;
Expand All @@ -72,6 +101,7 @@ describe("RoomListSectionHeaderViewModel", () => {
const vm = new RoomListSectionHeaderViewModel({
tag: "m.favourite",
title: "Favourites",
spaceId: "!space:server",
onToggleExpanded,
});
vm.setRooms([room]);
Expand All @@ -85,6 +115,7 @@ describe("RoomListSectionHeaderViewModel", () => {
const vm = new RoomListSectionHeaderViewModel({
tag: "m.favourite",
title: "Favourites",
spaceId: "!space:server",
onToggleExpanded,
});
vm.setRooms([room]);
Expand All @@ -107,6 +138,7 @@ describe("RoomListSectionHeaderViewModel", () => {
const vm = new RoomListSectionHeaderViewModel({
tag: "m.favourite",
title: "Favourites",
spaceId: "!space:server",
onToggleExpanded,
});
vm.setRooms([room]);
Expand All @@ -127,6 +159,7 @@ describe("RoomListSectionHeaderViewModel", () => {
const vm = new RoomListSectionHeaderViewModel({
tag: "m.favourite",
title: "Favourites",
spaceId: "!space:server",
onToggleExpanded,
});
vm.setRooms([room]);
Expand All @@ -145,6 +178,7 @@ describe("RoomListSectionHeaderViewModel", () => {
const vm = new RoomListSectionHeaderViewModel({
tag: "m.favourite",
title: "Favourites",
spaceId: "!space:server",
onToggleExpanded,
});
vm.setRooms([room]);
Expand Down
38 changes: 30 additions & 8 deletions apps/web/test/viewmodels/room-list/RoomListViewModel-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -806,10 +806,10 @@ describe("RoomListViewModel", () => {
expect(favSection!.roomIds).toEqual([]);
});

it("should preserve section collapse state across space changes", () => {
it("should track section collapse state per space", () => {
viewModel = new RoomListViewModel({ client: matrixClient });

// Collapse favourites
// Collapse favourites in the home space
const favHeader = viewModel.getSectionHeaderViewModel(DefaultTagID.Favourite);
favHeader.onClick();

Expand All @@ -828,15 +828,37 @@ describe("RoomListViewModel", () => {

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);
let snapshot = viewModel.getSnapshot();
// Favourites should be expanded in the new space (per-space state)
let favSection = snapshot.sections.find((s) => s.id === DefaultTagID.Favourite);
expect(favSection).toBeDefined();
expect(favSection!.roomIds).toEqual([]);
expect(favSection!.roomIds).toEqual(["!spacefav:server"]);

// Other sections should remain expanded
const chatsSection = snapshot.sections.find((s) => s.id === CHATS_TAG);
// Other sections should also be expanded
let chatsSection = snapshot.sections.find((s) => s.id === CHATS_TAG);
expect(chatsSection!.roomIds).toEqual(["!spacereg:server"]);

// Switch back to home space
jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({
spaceId: "home",
sections: [
{ tag: DefaultTagID.Favourite, rooms: [favRoom1, favRoom2] },
{ tag: CHATS_TAG, rooms: [regularRoom1] },
{ tag: DefaultTagID.LowPriority, rooms: [] },
],
});

RoomListStoreV3.instance.emit(RoomListStoreV3Event.ListsUpdate);

snapshot = viewModel.getSnapshot();
// Favourites should still be collapsed in the home space
favSection = snapshot.sections.find((s) => s.id === DefaultTagID.Favourite);
expect(favSection).toBeDefined();
expect(favSection!.roomIds).toEqual([]);

// Chats should be expanded
chatsSection = snapshot.sections.find((s) => s.id === CHATS_TAG);
expect(chatsSection!.roomIds).toEqual(["!reg1:server"]);
});

it("should apply filters across all sections", () => {
Expand Down
Loading