Skip to content

Commit 726481e

Browse files
committed
fix: incorrect selected room when expanding/collasping a section
1 parent bff189e commit 726481e

2 files changed

Lines changed: 55 additions & 19 deletions

File tree

apps/web/src/viewmodels/room-list/RoomListViewModel.ts

Lines changed: 31 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,10 @@ export class RoomListViewModel
7676
// State tracking
7777
private activeFilter: FilterEnum | undefined = undefined;
7878
private roomsResult: RoomsResult;
79+
/**
80+
* List of sections to display in the room list, derived from roomsResult and section header view model expansion state.
81+
*/
82+
private sections: Section[] = [];
7983
private lastActiveRoomPosition: StickyRoomPosition | undefined = undefined;
8084

8185
// Child view model management
@@ -116,11 +120,12 @@ export class RoomListViewModel
116120
filterKeys: undefined,
117121
},
118122
isFlatList,
119-
sections,
123+
sections: toRoomListSection(sections),
120124
canCreateRoom,
121125
});
122126

123127
this.roomsResult = roomsResult;
128+
this.sections = sections;
124129

125130
// Build initial roomsMap from roomsResult
126131
this.updateRoomsMap(roomsResult);
@@ -301,7 +306,7 @@ export class RoomListViewModel
301306
if (!currentRoomId) return;
302307

303308
const { delta, unread } = payload;
304-
const rooms = this.roomsResult.sections.flatMap((section) => section.rooms);
309+
const rooms = this.sections.flatMap((section) => section.rooms);
305310

306311
const filteredRooms = unread
307312
? // Filter the rooms to only include unread ones and the active room
@@ -393,9 +398,7 @@ export class RoomListViewModel
393398
return undefined;
394399
}
395400

396-
const index = this.roomsResult.sections
397-
.flatMap((section) => section.rooms)
398-
.findIndex((room) => room.roomId === roomId);
401+
const index = this.sections.flatMap((section) => section.rooms).findIndex((room) => room.roomId === roomId);
399402
return index >= 0 ? index : undefined;
400403
}
401404

@@ -485,18 +488,18 @@ export class RoomListViewModel
485488
// Rebuild roomsMap with the reordered rooms
486489
this.updateRoomsMap(this.roomsResult);
487490

488-
// Calculate the active room index after applying sticky logic
489-
const activeRoomIndex = this.getActiveRoomIndex(roomId);
490-
491491
// Track the current active room position for future sticky calculations
492492
this.lastActiveRoomPosition = roomId ? this.findRoomPosition(this.roomsResult.sections, roomId) : undefined;
493493

494494
// Build the complete state atomically to ensure consistency
495-
// roomIds and roomListState must always be in sync
496495
const { sections, isFlatList } = computeSections(
497496
this.roomsResult,
498497
(tag) => this.roomSectionHeaderViewModels.get(tag)?.isExpanded ?? true,
499498
);
499+
this.sections = sections;
500+
501+
// Calculate the active room index from the computed sections (which exclude collapsed sections' rooms)
502+
const activeRoomIndex = this.getActiveRoomIndex(roomId);
500503

501504
// Update filter keys - only update if they have actually changed to prevent unnecessary re-renders of the room list
502505
const previousFilterKeys = this.snapshot.current.roomListState.filterKeys;
@@ -511,13 +514,16 @@ export class RoomListViewModel
511514
const isRoomListEmpty = this.roomsResult.sections.every((section) => section.rooms.length === 0);
512515
const isLoadingRooms = RoomListStoreV3.instance.isLoadingRooms;
513516

517+
const viewSections = toRoomListSection(this.sections);
518+
const previousSections = this.snapshot.current.sections;
519+
514520
// Single atomic snapshot update
515521
this.snapshot.merge({
516522
isLoadingRooms,
517523
isRoomListEmpty,
518524
activeFilterId,
519525
roomListState,
520-
sections,
526+
sections: keepIfSame(previousSections, viewSections),
521527
isFlatList,
522528
});
523529
}
@@ -545,25 +551,31 @@ export class RoomListViewModel
545551
* Compute the sections to display in the room list based on the rooms result and section expansion state.
546552
* @param roomsResult - The current rooms result containing sections and rooms
547553
* @param isSectionExpanded - A function that takes a section tag and returns whether that section is currently expanded
548-
* @returns An object containing the computed sections with room IDs (empty if section is collapsed) and a boolean indicating if the list should be displayed as a flat list (only one section with all rooms)
554+
* @returns An object containing the computed sections (with rooms removed for collapsed sections) and a boolean indicating if this is a flat list (only one section with all rooms)
549555
*/
550556
function computeSections(
551557
roomsResult: RoomsResult,
552558
isSectionExpanded: (tag: string) => boolean,
553-
): { sections: RoomListSection[]; isFlatList: boolean } {
559+
): { sections: Section[]; isFlatList: boolean } {
554560
const sections = roomsResult.sections
555-
.map(({ tag, rooms }) => ({
556-
id: tag,
557-
roomIds: rooms.map((room) => room.roomId),
558-
}))
559561
// Only include sections that have rooms
560-
.filter((section) => section.roomIds.length > 0)
562+
.filter((section) => section.rooms.length > 0)
561563
// Remove roomIds for sections that are currently collapsed according to their section header view model
562564
.map((section) => ({
563565
...section,
564-
roomIds: isSectionExpanded(section.id) ? section.roomIds : [],
566+
rooms: isSectionExpanded(section.tag) ? section.rooms : [],
565567
}));
566-
const isFlatList = sections.length === 1 && sections[0].id === CHATS_TAG;
568+
const isFlatList = sections.length === 1 && sections[0].tag === CHATS_TAG;
567569

568570
return { sections, isFlatList };
569571
}
572+
573+
/**
574+
* Convert from the internal Section type used in the view model to the RoomListSection type used in the snapshot.
575+
*/
576+
function toRoomListSection(sections: Section[]): RoomListSection[] {
577+
return sections.map(({ tag, rooms }) => ({
578+
id: tag,
579+
roomIds: rooms.map((room) => room.roomId),
580+
}));
581+
}

apps/web/test/viewmodels/room-list/RoomListViewModel-test.tsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import { type MatrixClient, type Room } from "matrix-js-sdk/src/matrix";
99
import { mocked } from "jest-mock";
10+
import { waitFor } from "jest-matrix-react";
1011

1112
import { createTestClient, flushPromises, mkStubRoom, stubClient } from "../../test-utils";
1213
import RoomListStoreV3, { CHATS_TAG, RoomListStoreV3Event } from "../../../src/stores/room-list-v3/RoomListStoreV3";
@@ -732,6 +733,29 @@ describe("RoomListViewModel", () => {
732733
expect(chatsSection!.roomIds).toEqual(["!reg1:server", "!reg2:server"]);
733734
});
734735

736+
it("should compute activeRoomIndex relative to visible rooms when a section is collapsed", async () => {
737+
viewModel = new RoomListViewModel({ client: matrixClient });
738+
739+
// Collapse the favourite section (which has 2 rooms: fav1, fav2)
740+
const favHeader = viewModel.getSectionHeaderViewModel(DefaultTagID.Favourite);
741+
favHeader.onClick();
742+
expect(favHeader.isExpanded).toBe(false);
743+
744+
// Select regularRoom1, which is the first room in the chats section
745+
jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockReturnValue("!reg1:server");
746+
dispatcher.dispatch({
747+
action: Action.ActiveRoomChanged,
748+
newRoomId: "!reg1:server",
749+
});
750+
751+
await waitFor(() => {
752+
const snapshot = viewModel.getSnapshot();
753+
// The favourite section is collapsed so its 2 rooms are not visible.
754+
// regularRoom1 should be at index 0 in the visible list, not index 2.
755+
expect(snapshot.roomListState.activeRoomIndex).toBe(0);
756+
});
757+
});
758+
735759
it("should restore room IDs when a section is re-expanded", () => {
736760
viewModel = new RoomListViewModel({ client: matrixClient });
737761

0 commit comments

Comments
 (0)