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();