Room list: fix expanded/collapse state of sections (#33074)
* fix: section being empty in flat list mode When switching space (or removing a section later), if the Chat section is collpased and the room list is in flat list mode in the other space, the room list is empty. The fix forces the section to be in expanded state if in flat list mode * fix: store section expanded state by space
This commit is contained in:
@@ -19,6 +19,10 @@ import { type RoomNotificationState } from "../../stores/notifications/RoomNotif
|
|||||||
interface RoomListSectionHeaderViewModelProps {
|
interface RoomListSectionHeaderViewModelProps {
|
||||||
tag: string;
|
tag: string;
|
||||||
title: string;
|
title: string;
|
||||||
|
/**
|
||||||
|
* The ID of the current space.
|
||||||
|
*/
|
||||||
|
spaceId: string;
|
||||||
onToggleExpanded: (isExpanded: boolean) => void;
|
onToggleExpanded: (isExpanded: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,12 +35,19 @@ export class RoomListSectionHeaderViewModel
|
|||||||
*/
|
*/
|
||||||
private roomNotificationStates = new Set<RoomNotificationState>();
|
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) {
|
public constructor(props: RoomListSectionHeaderViewModelProps) {
|
||||||
super(props, { id: props.tag, title: props.title, isExpanded: true, isUnread: false });
|
super(props, { id: props.tag, title: props.title, isExpanded: true, isUnread: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
public onClick = (): void => {
|
public onClick = (): void => {
|
||||||
const isExpanded = !this.snapshot.current.isExpanded;
|
const isExpanded = !this.snapshot.current.isExpanded;
|
||||||
|
this.expandedBySpace.set(this.props.spaceId, isExpanded);
|
||||||
this.snapshot.merge({ isExpanded });
|
this.snapshot.merge({ isExpanded });
|
||||||
this.props.onToggleExpanded(isExpanded);
|
this.props.onToggleExpanded(isExpanded);
|
||||||
};
|
};
|
||||||
@@ -48,6 +59,25 @@ export class RoomListSectionHeaderViewModel
|
|||||||
return this.snapshot.current.isExpanded;
|
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.
|
* 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.
|
* Only subscribes to new rooms and unsubscribes from rooms no longer in the section.
|
||||||
|
|||||||
@@ -258,6 +258,7 @@ export class RoomListViewModel
|
|||||||
const viewModel = new RoomListSectionHeaderViewModel({
|
const viewModel = new RoomListSectionHeaderViewModel({
|
||||||
tag,
|
tag,
|
||||||
title,
|
title,
|
||||||
|
spaceId: this.roomsResult.spaceId,
|
||||||
onToggleExpanded: () => this.updateRoomListData(),
|
onToggleExpanded: () => this.updateRoomListData(),
|
||||||
});
|
});
|
||||||
this.roomSectionHeaderViewModels.set(tag, viewModel);
|
this.roomSectionHeaderViewModels.set(tag, viewModel);
|
||||||
@@ -367,6 +368,11 @@ export class RoomListViewModel
|
|||||||
|
|
||||||
this.updateRoomsMap(this.roomsResult);
|
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
|
// Space changed - get the last selected room for the new space to prevent flicker
|
||||||
const lastSelectedRoom = SpaceStore.instance.getLastSelectedRoomIdForSpace(newSpaceId);
|
const lastSelectedRoom = SpaceStore.instance.getLastSelectedRoomIdForSpace(newSpaceId);
|
||||||
|
|
||||||
@@ -501,6 +507,12 @@ export class RoomListViewModel
|
|||||||
this.roomsResult,
|
this.roomsResult,
|
||||||
(tag) => this.roomSectionHeaderViewModels.get(tag)?.isExpanded ?? true,
|
(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;
|
this.sections = sections;
|
||||||
|
|
||||||
// Calculate the active room index from the computed sections (which exclude collapsed sections' rooms)
|
// Calculate the active room index from the computed sections (which exclude collapsed sections' rooms)
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ describe("RoomListSectionHeaderViewModel", () => {
|
|||||||
const vm = new RoomListSectionHeaderViewModel({
|
const vm = new RoomListSectionHeaderViewModel({
|
||||||
tag: "m.favourite",
|
tag: "m.favourite",
|
||||||
title: "Favourites",
|
title: "Favourites",
|
||||||
|
spaceId: "!space:server",
|
||||||
onToggleExpanded,
|
onToggleExpanded,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -43,6 +44,7 @@ describe("RoomListSectionHeaderViewModel", () => {
|
|||||||
const vm = new RoomListSectionHeaderViewModel({
|
const vm = new RoomListSectionHeaderViewModel({
|
||||||
tag: "m.favourite",
|
tag: "m.favourite",
|
||||||
title: "Favourites",
|
title: "Favourites",
|
||||||
|
spaceId: "!space:server",
|
||||||
onToggleExpanded,
|
onToggleExpanded,
|
||||||
});
|
});
|
||||||
expect(vm.isExpanded).toBe(true);
|
expect(vm.isExpanded).toBe(true);
|
||||||
@@ -58,6 +60,33 @@ describe("RoomListSectionHeaderViewModel", () => {
|
|||||||
expect(onToggleExpanded).toHaveBeenCalledWith(true);
|
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", () => {
|
describe("unread status", () => {
|
||||||
let room: Room;
|
let room: Room;
|
||||||
let notificationState: RoomNotificationState;
|
let notificationState: RoomNotificationState;
|
||||||
@@ -72,6 +101,7 @@ describe("RoomListSectionHeaderViewModel", () => {
|
|||||||
const vm = new RoomListSectionHeaderViewModel({
|
const vm = new RoomListSectionHeaderViewModel({
|
||||||
tag: "m.favourite",
|
tag: "m.favourite",
|
||||||
title: "Favourites",
|
title: "Favourites",
|
||||||
|
spaceId: "!space:server",
|
||||||
onToggleExpanded,
|
onToggleExpanded,
|
||||||
});
|
});
|
||||||
vm.setRooms([room]);
|
vm.setRooms([room]);
|
||||||
@@ -85,6 +115,7 @@ describe("RoomListSectionHeaderViewModel", () => {
|
|||||||
const vm = new RoomListSectionHeaderViewModel({
|
const vm = new RoomListSectionHeaderViewModel({
|
||||||
tag: "m.favourite",
|
tag: "m.favourite",
|
||||||
title: "Favourites",
|
title: "Favourites",
|
||||||
|
spaceId: "!space:server",
|
||||||
onToggleExpanded,
|
onToggleExpanded,
|
||||||
});
|
});
|
||||||
vm.setRooms([room]);
|
vm.setRooms([room]);
|
||||||
@@ -107,6 +138,7 @@ describe("RoomListSectionHeaderViewModel", () => {
|
|||||||
const vm = new RoomListSectionHeaderViewModel({
|
const vm = new RoomListSectionHeaderViewModel({
|
||||||
tag: "m.favourite",
|
tag: "m.favourite",
|
||||||
title: "Favourites",
|
title: "Favourites",
|
||||||
|
spaceId: "!space:server",
|
||||||
onToggleExpanded,
|
onToggleExpanded,
|
||||||
});
|
});
|
||||||
vm.setRooms([room]);
|
vm.setRooms([room]);
|
||||||
@@ -127,6 +159,7 @@ describe("RoomListSectionHeaderViewModel", () => {
|
|||||||
const vm = new RoomListSectionHeaderViewModel({
|
const vm = new RoomListSectionHeaderViewModel({
|
||||||
tag: "m.favourite",
|
tag: "m.favourite",
|
||||||
title: "Favourites",
|
title: "Favourites",
|
||||||
|
spaceId: "!space:server",
|
||||||
onToggleExpanded,
|
onToggleExpanded,
|
||||||
});
|
});
|
||||||
vm.setRooms([room]);
|
vm.setRooms([room]);
|
||||||
@@ -145,6 +178,7 @@ describe("RoomListSectionHeaderViewModel", () => {
|
|||||||
const vm = new RoomListSectionHeaderViewModel({
|
const vm = new RoomListSectionHeaderViewModel({
|
||||||
tag: "m.favourite",
|
tag: "m.favourite",
|
||||||
title: "Favourites",
|
title: "Favourites",
|
||||||
|
spaceId: "!space:server",
|
||||||
onToggleExpanded,
|
onToggleExpanded,
|
||||||
});
|
});
|
||||||
vm.setRooms([room]);
|
vm.setRooms([room]);
|
||||||
|
|||||||
@@ -806,10 +806,10 @@ describe("RoomListViewModel", () => {
|
|||||||
expect(favSection!.roomIds).toEqual([]);
|
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 });
|
viewModel = new RoomListViewModel({ client: matrixClient });
|
||||||
|
|
||||||
// Collapse favourites
|
// Collapse favourites in the home space
|
||||||
const favHeader = viewModel.getSectionHeaderViewModel(DefaultTagID.Favourite);
|
const favHeader = viewModel.getSectionHeaderViewModel(DefaultTagID.Favourite);
|
||||||
favHeader.onClick();
|
favHeader.onClick();
|
||||||
|
|
||||||
@@ -828,15 +828,37 @@ describe("RoomListViewModel", () => {
|
|||||||
|
|
||||||
RoomListStoreV3.instance.emit(RoomListStoreV3Event.ListsUpdate);
|
RoomListStoreV3.instance.emit(RoomListStoreV3Event.ListsUpdate);
|
||||||
|
|
||||||
const snapshot = viewModel.getSnapshot();
|
let snapshot = viewModel.getSnapshot();
|
||||||
// Favourites should still be collapsed even after the space change
|
// Favourites should be expanded in the new space (per-space state)
|
||||||
const favSection = snapshot.sections.find((s) => s.id === DefaultTagID.Favourite);
|
let favSection = snapshot.sections.find((s) => s.id === DefaultTagID.Favourite);
|
||||||
|
expect(favSection).toBeDefined();
|
||||||
|
expect(favSection!.roomIds).toEqual(["!spacefav:server"]);
|
||||||
|
|
||||||
|
// 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).toBeDefined();
|
||||||
expect(favSection!.roomIds).toEqual([]);
|
expect(favSection!.roomIds).toEqual([]);
|
||||||
|
|
||||||
// Other sections should remain expanded
|
// Chats should be expanded
|
||||||
const chatsSection = snapshot.sections.find((s) => s.id === CHATS_TAG);
|
chatsSection = snapshot.sections.find((s) => s.id === CHATS_TAG);
|
||||||
expect(chatsSection!.roomIds).toEqual(["!spacereg:server"]);
|
expect(chatsSection!.roomIds).toEqual(["!reg1:server"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should apply filters across all sections", () => {
|
it("should apply filters across all sections", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user