From 839329b52acd3e94c55318c8efa64ddae987f41e Mon Sep 17 00:00:00 2001 From: R Midhun Suresh Date: Tue, 18 Mar 2025 18:19:10 +0530 Subject: [PATCH] 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 --- .../viewmodels/roomlist/RoomListViewModel.tsx | 8 ++++ .../roomlist/useIndexForActiveRoom.tsx | 44 ++++++++++++++++++ .../roomlist/RoomListViewModel-test.tsx | 45 +++++++++++++++++++ .../RoomListPanel/EmptyRoomList-test.tsx | 1 + .../rooms/RoomListPanel/RoomList-test.tsx | 1 + .../RoomListPrimaryFilters-test.tsx | 1 + .../rooms/RoomListPanel/RoomListView-test.tsx | 1 + 7 files changed, 101 insertions(+) create mode 100644 src/components/viewmodels/roomlist/useIndexForActiveRoom.tsx diff --git a/src/components/viewmodels/roomlist/RoomListViewModel.tsx b/src/components/viewmodels/roomlist/RoomListViewModel.tsx index 2143aeae78..584e436c57 100644 --- a/src/components/viewmodels/roomlist/RoomListViewModel.tsx +++ b/src/components/viewmodels/roomlist/RoomListViewModel.tsx @@ -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, }; } diff --git a/src/components/viewmodels/roomlist/useIndexForActiveRoom.tsx b/src/components/viewmodels/roomlist/useIndexForActiveRoom.tsx new file mode 100644 index 0000000000..210e0efd0f --- /dev/null +++ b/src/components/viewmodels/roomlist/useIndexForActiveRoom.tsx @@ -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(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; +} diff --git a/test/unit-tests/components/viewmodels/roomlist/RoomListViewModel-test.tsx b/test/unit-tests/components/viewmodels/roomlist/RoomListViewModel-test.tsx index 9cfb83a766..4b986f8d2d 100644 --- a/test/unit-tests/components/viewmodels/roomlist/RoomListViewModel-test.tsx +++ b/test/unit-tests/components/viewmodels/roomlist/RoomListViewModel-test.tsx @@ -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); + }); + }); }); diff --git a/test/unit-tests/components/views/rooms/RoomListPanel/EmptyRoomList-test.tsx b/test/unit-tests/components/views/rooms/RoomListPanel/EmptyRoomList-test.tsx index 5c41fb367c..b68929f74a 100644 --- a/test/unit-tests/components/views/rooms/RoomListPanel/EmptyRoomList-test.tsx +++ b/test/unit-tests/components/views/rooms/RoomListPanel/EmptyRoomList-test.tsx @@ -31,6 +31,7 @@ describe("", () => { canCreateRoom: true, shouldShowMessagePreview: false, toggleMessagePreview: jest.fn(), + activeIndex: undefined, }; }); diff --git a/test/unit-tests/components/views/rooms/RoomListPanel/RoomList-test.tsx b/test/unit-tests/components/views/rooms/RoomListPanel/RoomList-test.tsx index 3490a3c509..0d837c3a20 100644 --- a/test/unit-tests/components/views/rooms/RoomListPanel/RoomList-test.tsx +++ b/test/unit-tests/components/views/rooms/RoomListPanel/RoomList-test.tsx @@ -40,6 +40,7 @@ describe("", () => { createRoom: jest.fn(), createChatRoom: jest.fn(), canCreateRoom: true, + activeIndex: undefined, }; // Needed to render a room list cell diff --git a/test/unit-tests/components/views/rooms/RoomListPanel/RoomListPrimaryFilters-test.tsx b/test/unit-tests/components/views/rooms/RoomListPanel/RoomListPrimaryFilters-test.tsx index 301f293835..79bfbb6dc0 100644 --- a/test/unit-tests/components/views/rooms/RoomListPanel/RoomListPrimaryFilters-test.tsx +++ b/test/unit-tests/components/views/rooms/RoomListPanel/RoomListPrimaryFilters-test.tsx @@ -34,6 +34,7 @@ describe("", () => { activeSortOption: SortOption.Activity, shouldShowMessagePreview: false, toggleMessagePreview: jest.fn(), + activeIndex: undefined, }; }); diff --git a/test/unit-tests/components/views/rooms/RoomListPanel/RoomListView-test.tsx b/test/unit-tests/components/views/rooms/RoomListPanel/RoomListView-test.tsx index 015fe5404d..f56d30976a 100644 --- a/test/unit-tests/components/views/rooms/RoomListPanel/RoomListView-test.tsx +++ b/test/unit-tests/components/views/rooms/RoomListPanel/RoomListView-test.tsx @@ -35,6 +35,7 @@ describe("", () => { canCreateRoom: true, toggleMessagePreview: jest.fn(), shouldShowMessagePreview: false, + activeIndex: undefined, }; const matrixClient = stubClient();