RoomListViewModel: Make the active room sticky in the list (#29551)

* Add new hook for sticky room

This hook takes the filtered, sorted rooms and returns a new list of
rooms such that the active room is kept in the same index even when the
list has changes.

* Use new hook in view model

* Add tests

* Use single * in comments
This commit is contained in:
R Midhun Suresh
2025-03-21 17:41:59 +05:30
committed by GitHub
parent 0d28df0f67
commit b54122884c
4 changed files with 230 additions and 67 deletions

View File

@@ -314,34 +314,51 @@ describe("RoomListViewModel", () => {
});
});
describe("active index", () => {
it("should recalculate active index when list of rooms change", () => {
describe("Sticky room and active index", () => {
function expectActiveRoom(vm: ReturnType<typeof useRoomListViewModel>, i: number, roomId: string) {
expect(vm.activeIndex).toEqual(i);
expect(vm.rooms[i].roomId).toEqual(roomId);
}
it("active room and active index are retained on order change", () => {
const { rooms } = mockAndCreateRooms();
// Let's say that the first room is the active room initially
jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockImplementation(() => rooms[0].roomId);
// Let's say that the room at index 5 is active
const roomId = rooms[5].roomId;
jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockImplementation(() => roomId);
const { result: vm } = renderHook(() => useRoomListViewModel());
expect(vm.current.activeIndex).toEqual(0);
expect(vm.current.activeIndex).toEqual(5);
// Let's say that a new room is added and that becomes active
const newRoom = mkStubRoom("bar:matrix.org", "Bar", undefined);
jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockImplementation(() => newRoom.roomId);
rooms.push(newRoom);
// Let's say that room at index 9 moves to index 5
const room9 = rooms[9];
rooms.splice(9, 1);
rooms.splice(5, 0, room9);
act(() => RoomListStoreV3.instance.emit(LISTS_UPDATE_EVENT));
// Now the active room should be the last room which we just added
expect(vm.current.activeIndex).toEqual(rooms.length - 1);
// Active room index should still be 5
expectActiveRoom(vm.current, 5, roomId);
// Let's add 2 new rooms from index 0
const newRoom1 = mkStubRoom("bar1:matrix.org", "Bar 1", undefined);
const newRoom2 = mkStubRoom("bar2:matrix.org", "Bar 2", undefined);
rooms.unshift(newRoom1, newRoom2);
act(() => RoomListStoreV3.instance.emit(LISTS_UPDATE_EVENT));
// Active room index should still be 5
expectActiveRoom(vm.current, 5, roomId);
});
it("should recalculate active index when active room changes", () => {
it("active room and active index are updated when another room is opened", () => {
const { rooms } = mockAndCreateRooms();
const roomId = rooms[5].roomId;
jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockImplementation(() => roomId);
const { result: vm } = renderHook(() => useRoomListViewModel());
expectActiveRoom(vm.current, 5, roomId);
// No active room yet
expect(vm.current.activeIndex).toBeUndefined();
// Let's say that room at index 5 becomes active
const room = rooms[5];
// Let's say that room at index 9 becomes active
const room = rooms[9];
act(() => {
dispatcher.dispatch(
{
@@ -353,8 +370,76 @@ describe("RoomListViewModel", () => {
);
});
// We expect index 5 to be active now
expect(vm.current.activeIndex).toEqual(5);
// Active room index should change to reflect new room
expectActiveRoom(vm.current, 9, room.roomId);
});
it("active room and active index are updated when active index spills out of rooms array bounds", () => {
const { rooms } = mockAndCreateRooms();
// Let's say that the room at index 5 is active
const roomId = rooms[5].roomId;
jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockImplementation(() => roomId);
const { result: vm } = renderHook(() => useRoomListViewModel());
expectActiveRoom(vm.current, 5, roomId);
// Let's say that we remove rooms from the start of the array
for (let i = 0; i < 4; ++i) {
// We should be able to do 4 deletions before we run out of rooms
rooms.splice(0, 1);
act(() => RoomListStoreV3.instance.emit(LISTS_UPDATE_EVENT));
expectActiveRoom(vm.current, 5, roomId);
}
// If we remove one more room from the start, there's not going to be enough rooms
// to maintain the active index.
rooms.splice(0, 1);
act(() => RoomListStoreV3.instance.emit(LISTS_UPDATE_EVENT));
expectActiveRoom(vm.current, 0, roomId);
});
it("active room and active index are retained when rooms that appear after the active room are deleted", () => {
const { rooms } = mockAndCreateRooms();
// Let's say that the room at index 5 is active
const roomId = rooms[5].roomId;
jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockImplementation(() => roomId);
const { result: vm } = renderHook(() => useRoomListViewModel());
expectActiveRoom(vm.current, 5, roomId);
// Let's say that we remove rooms from the start of the array
for (let i = 0; i < 4; ++i) {
// Deleting rooms after index 5 (active) should not update the active index
rooms.splice(6, 1);
act(() => RoomListStoreV3.instance.emit(LISTS_UPDATE_EVENT));
expectActiveRoom(vm.current, 5, roomId);
}
});
it("active room index becomes undefined when active room is deleted", () => {
const { rooms } = mockAndCreateRooms();
// Let's say that the room at index 5 is active
let roomId: string | undefined = rooms[5].roomId;
jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockImplementation(() => roomId);
const { result: vm } = renderHook(() => useRoomListViewModel());
expectActiveRoom(vm.current, 5, roomId);
// Let's remove the active room (i.e room at index 5)
rooms.splice(5, 1);
roomId = undefined;
act(() => RoomListStoreV3.instance.emit(LISTS_UPDATE_EVENT));
expect(vm.current.activeIndex).toBeUndefined();
});
it("active room index is initially undefined", () => {
mockAndCreateRooms();
// Let's say that there's no active room currently
jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockImplementation(() => undefined);
const { result: vm } = renderHook(() => useRoomListViewModel());
expect(vm.current.activeIndex).toEqual(undefined);
});
});
});