RoomListViewModel: Track the index of the active room in the list (#29519)

* Introduce a hook to track active room

This hook simply keeps a state which tracks the index of the active room
in the list of rooms passed through props. This index will be recomputed
if the active rooms changes or if the list itself changed.

* Use hook in the view model

* Write tests

* Fix broken tests
This commit is contained in:
R Midhun Suresh
2025-03-18 18:19:10 +05:30
committed by GitHub
parent 7de54a385e
commit 839329b52a
7 changed files with 101 additions and 0 deletions

View File

@@ -18,6 +18,7 @@ import SpaceStore from "../../../stores/spaces/SpaceStore";
import dispatcher from "../../../dispatcher/dispatcher";
import { Action } from "../../../dispatcher/actions";
import { useMatrixClientContext } from "../../../contexts/MatrixClientContext";
import { useIndexForActiveRoom } from "./useIndexForActiveRoom";
export interface RoomListViewState {
/**
@@ -83,6 +84,11 @@ export interface RoomListViewState {
* A function to turn on/off message previews.
*/
toggleMessagePreview: () => void;
/**
* The index of the active room in the room list.
*/
activeIndex: number | undefined;
}
/**
@@ -101,6 +107,7 @@ export function useRoomListViewModel(): RoomListViewState {
);
const canCreateRoom = hasCreateRoomRights(matrixClient, currentSpace);
const activeIndex = useIndexForActiveRoom(rooms);
const { activeSortOption, sort } = useSorter();
const { shouldShowMessagePreview, toggleMessagePreview } = useMessagePreviewToggle();
@@ -120,5 +127,6 @@ export function useRoomListViewModel(): RoomListViewState {
sort,
shouldShowMessagePreview,
toggleMessagePreview,
activeIndex,
};
}

View File

@@ -0,0 +1,44 @@
/*
* Copyright 2025 New Vector 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 { useCallback, useEffect, useState } from "react";
import { SdkContextClass } from "../../../contexts/SDKContext";
import { useDispatcher } from "../../../hooks/useDispatcher";
import dispatcher from "../../../dispatcher/dispatcher";
import { Action } from "../../../dispatcher/actions";
import type { Room } from "matrix-js-sdk/src/matrix";
/**
* Tracks the index of the active room in the given array of rooms.
* @param rooms list of rooms
* @returns index of the active room or undefined otherwise.
*/
export function useIndexForActiveRoom(rooms: Room[]): number | undefined {
const [index, setIndex] = useState<number | undefined>(undefined);
const calculateIndex = useCallback(
(newRoomId?: string) => {
const activeRoomId = newRoomId ?? SdkContextClass.instance.roomViewStore.getRoomId();
const index = rooms.findIndex((room) => room.roomId === activeRoomId);
setIndex(index === -1 ? undefined : index);
},
[rooms],
);
// Re-calculate the index when the active room has changed.
useDispatcher(dispatcher, (payload) => {
if (payload.action === Action.ActiveRoomChanged) calculateIndex(payload.newRoomId);
});
// Re-calculate the index when the list of rooms has changed.
useEffect(() => {
calculateIndex();
}, [calculateIndex, rooms]);
return index;
}

View File

@@ -21,6 +21,7 @@ import SettingsStore from "../../../../../src/settings/SettingsStore";
import { hasCreateRoomRights, createRoom } from "../../../../../src/components/viewmodels/roomlist/utils";
import dispatcher from "../../../../../src/dispatcher/dispatcher";
import { Action } from "../../../../../src/dispatcher/actions";
import { SdkContextClass } from "../../../../../src/contexts/SDKContext";
jest.mock("../../../../../src/components/viewmodels/roomlist/utils", () => ({
hasCreateRoomRights: jest.fn().mockReturnValue(false),
@@ -289,4 +290,48 @@ describe("RoomListViewModel", () => {
expect(spy).toHaveBeenCalledWith(Action.CreateChat);
});
});
describe("active index", () => {
it("should recalculate active index when list of rooms 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);
const { result: vm } = renderHook(() => useRoomListViewModel());
expect(vm.current.activeIndex).toEqual(0);
// 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);
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);
});
it("should recalculate active index when active room changes", () => {
const { rooms } = mockAndCreateRooms();
const { result: vm } = renderHook(() => useRoomListViewModel());
// No active room yet
expect(vm.current.activeIndex).toBeUndefined();
// Let's say that room at index 5 becomes active
const room = rooms[5];
act(() => {
dispatcher.dispatch(
{
action: Action.ActiveRoomChanged,
oldRoomId: null,
newRoomId: room.roomId,
},
true,
);
});
// We expect index 5 to be active now
expect(vm.current.activeIndex).toEqual(5);
});
});
});

View File

@@ -31,6 +31,7 @@ describe("<EmptyRoomList />", () => {
canCreateRoom: true,
shouldShowMessagePreview: false,
toggleMessagePreview: jest.fn(),
activeIndex: undefined,
};
});

View File

@@ -40,6 +40,7 @@ describe("<RoomList />", () => {
createRoom: jest.fn(),
createChatRoom: jest.fn(),
canCreateRoom: true,
activeIndex: undefined,
};
// Needed to render a room list cell

View File

@@ -34,6 +34,7 @@ describe("<RoomListPrimaryFilters />", () => {
activeSortOption: SortOption.Activity,
shouldShowMessagePreview: false,
toggleMessagePreview: jest.fn(),
activeIndex: undefined,
};
});

View File

@@ -35,6 +35,7 @@ describe("<RoomListView />", () => {
canCreateRoom: true,
toggleMessagePreview: jest.fn(),
shouldShowMessagePreview: false,
activeIndex: undefined,
};
const matrixClient = stubClient();