From 9fb52e984c65844996b7223daf103421f63cac03 Mon Sep 17 00:00:00 2001 From: R Midhun Suresh Date: Fri, 14 Mar 2025 20:40:34 +0530 Subject: [PATCH] RoomListViewModel: Provide a way to resort the room list and track the active sort method (#29499) * Add a hook that deals with the sorting behaviour Hook will provide a function to sort the list and also provides a state which tracks the currently active sort option. * Use hook in vm * Write test for the vm * Fix broken view tests --- .../viewmodels/roomlist/RoomListViewModel.tsx | 14 +++++ .../viewmodels/roomlist/useSorter.ts | 62 +++++++++++++++++++ .../roomlist/RoomListViewModel-test.tsx | 31 ++++++++++ .../rooms/RoomListPanel/RoomList-test.tsx | 3 + .../RoomListPrimaryFilters-test.tsx | 3 + 5 files changed, 113 insertions(+) create mode 100644 src/components/viewmodels/roomlist/useSorter.ts diff --git a/src/components/viewmodels/roomlist/RoomListViewModel.tsx b/src/components/viewmodels/roomlist/RoomListViewModel.tsx index fb827c4889..c195613d7d 100644 --- a/src/components/viewmodels/roomlist/RoomListViewModel.tsx +++ b/src/components/viewmodels/roomlist/RoomListViewModel.tsx @@ -12,6 +12,7 @@ import type { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPaylo import dispatcher from "../../../dispatcher/dispatcher"; import { Action } from "../../../dispatcher/actions"; import { type PrimaryFilter, type SecondaryFilters, useFilteredRooms } from "./useFilteredRooms"; +import { type SortOption, useSorter } from "./useSorter"; export interface RoomListViewState { /** @@ -39,6 +40,16 @@ export interface RoomListViewState { * The currently active secondary filter. */ activeSecondaryFilter: SecondaryFilters; + + /** + * Change the sort order of the room-list. + */ + sort: (option: SortOption) => void; + + /** + * The currently active sort option. + */ + activeSortOption: SortOption; } /** @@ -47,6 +58,7 @@ export interface RoomListViewState { */ export function useRoomListViewModel(): RoomListViewState { const { primaryFilters, rooms, activateSecondaryFilter, activeSecondaryFilter } = useFilteredRooms(); + const { activeSortOption, sort } = useSorter(); const openRoom = useCallback((roomId: string): void => { dispatcher.dispatch({ @@ -62,5 +74,7 @@ export function useRoomListViewModel(): RoomListViewState { primaryFilters, activateSecondaryFilter, activeSecondaryFilter, + activeSortOption, + sort, }; } diff --git a/src/components/viewmodels/roomlist/useSorter.ts b/src/components/viewmodels/roomlist/useSorter.ts new file mode 100644 index 0000000000..c7a880d430 --- /dev/null +++ b/src/components/viewmodels/roomlist/useSorter.ts @@ -0,0 +1,62 @@ +/* +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 { useState } from "react"; + +import RoomListStoreV3 from "../../../stores/room-list-v3/RoomListStoreV3"; +import { SortingAlgorithm } from "../../../stores/room-list-v3/skip-list/sorters"; +import SettingsStore from "../../../settings/SettingsStore"; + +/** + * Sorting options made available to the view. + */ +export const enum SortOption { + Activity = SortingAlgorithm.Recency, + AToZ = SortingAlgorithm.Alphabetic, +} + +/** + * {@link SortOption} holds almost the same information as + * {@link SortingAlgorithm}. This is done intentionally to + * prevent the view from having a dependence on the + * model (which is the store in this case). + */ +const sortingAlgorithmToSortingOption = { + [SortingAlgorithm.Alphabetic]: SortOption.AToZ, + [SortingAlgorithm.Recency]: SortOption.Activity, +}; + +const sortOptionToSortingAlgorithm = { + [SortOption.AToZ]: SortingAlgorithm.Alphabetic, + [SortOption.Activity]: SortingAlgorithm.Recency, +}; + +interface SortState { + sort: (option: SortOption) => void; + activeSortOption: SortOption; +} + +/** + * This hook does two things: + * - Provides a way to track the currently active sort option. + * - Provides a function to resort the room list. + */ +export function useSorter(): SortState { + const [activeSortingAlgorithm, setActiveSortingAlgorithm] = useState(() => + SettingsStore.getValue("RoomList.preferredSorting"), + ); + + const sort = (option: SortOption): void => { + const sortingAlgorithm = sortOptionToSortingAlgorithm[option]; + RoomListStoreV3.instance.resort(sortingAlgorithm); + setActiveSortingAlgorithm(sortingAlgorithm); + }; + + return { + sort, + activeSortOption: sortingAlgorithmToSortingOption[activeSortingAlgorithm!], + }; +} diff --git a/test/unit-tests/components/viewmodels/roomlist/RoomListViewModel-test.tsx b/test/unit-tests/components/viewmodels/roomlist/RoomListViewModel-test.tsx index 055feb84e6..e1e2f6ac57 100644 --- a/test/unit-tests/components/viewmodels/roomlist/RoomListViewModel-test.tsx +++ b/test/unit-tests/components/viewmodels/roomlist/RoomListViewModel-test.tsx @@ -16,6 +16,9 @@ import dispatcher from "../../../../../src/dispatcher/dispatcher"; import { Action } from "../../../../../src/dispatcher/actions"; import { FilterKey } from "../../../../../src/stores/room-list-v3/skip-list/filters"; import { SecondaryFilters } from "../../../../../src/components/viewmodels/roomlist/useFilteredRooms"; +import { SortingAlgorithm } from "../../../../../src/stores/room-list-v3/skip-list/sorters"; +import { SortOption } from "../../../../../src/components/viewmodels/roomlist/useSorter"; +import SettingsStore from "../../../../../src/settings/SettingsStore"; describe("RoomListViewModel", () => { function mockAndCreateRooms() { @@ -26,6 +29,10 @@ describe("RoomListViewModel", () => { return { rooms, fn }; } + afterEach(() => { + jest.restoreAllMocks(); + }); + it("should return a list of rooms", async () => { const { rooms } = mockAndCreateRooms(); const { result: vm } = renderHook(() => useRoomListViewModel()); @@ -203,5 +210,29 @@ describe("RoomListViewModel", () => { expect(vm.current.primaryFilters.find((f) => f.name === primaryFilterName)).toBeUndefined(); }); }); + + it("should change sort order", () => { + mockAndCreateRooms(); + const { result: vm } = renderHook(() => useRoomListViewModel()); + + const resort = jest.spyOn(RoomListStoreV3.instance, "resort").mockImplementation(() => {}); + + // Change the sort option + act(() => { + vm.current.sort(SortOption.AToZ); + }); + + // Resort method in RLS must have been called + expect(resort).toHaveBeenCalledWith(SortingAlgorithm.Alphabetic); + }); + + it("should set activeSortOption based on value from settings", () => { + // Let's say that the user's preferred sorting is alphabetic + jest.spyOn(SettingsStore, "getValue").mockImplementation(() => SortingAlgorithm.Alphabetic); + + mockAndCreateRooms(); + const { result: vm } = renderHook(() => useRoomListViewModel()); + expect(vm.current.activeSortOption).toEqual(SortOption.AToZ); + }); }); }); 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 e720798f04..cd6ed29e27 100644 --- a/test/unit-tests/components/views/rooms/RoomListPanel/RoomList-test.tsx +++ b/test/unit-tests/components/views/rooms/RoomListPanel/RoomList-test.tsx @@ -15,6 +15,7 @@ import { type RoomListViewState } from "../../../../../../src/components/viewmod import { RoomList } from "../../../../../../src/components/views/rooms/RoomListPanel/RoomList"; import DMRoomMap from "../../../../../../src/utils/DMRoomMap"; import { SecondaryFilters } from "../../../../../../src/components/viewmodels/roomlist/useFilteredRooms"; +import { SortOption } from "../../../../../../src/components/viewmodels/roomlist/useSorter"; describe("", () => { let matrixClient: MatrixClient; @@ -34,6 +35,8 @@ describe("", () => { primaryFilters: [], activateSecondaryFilter: () => {}, activeSecondaryFilter: SecondaryFilters.AllActivity, + sort: jest.fn(), + activeSortOption: SortOption.Activity, }; // 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 3b1b29a5ff..3bb1968371 100644 --- a/test/unit-tests/components/views/rooms/RoomListPanel/RoomListPrimaryFilters-test.tsx +++ b/test/unit-tests/components/views/rooms/RoomListPanel/RoomListPrimaryFilters-test.tsx @@ -12,6 +12,7 @@ import userEvent from "@testing-library/user-event"; import { type RoomListViewState } from "../../../../../../src/components/viewmodels/roomlist/RoomListViewModel"; import { SecondaryFilters } from "../../../../../../src/components/viewmodels/roomlist/useFilteredRooms"; import { RoomListPrimaryFilters } from "../../../../../../src/components/views/rooms/RoomListPanel/RoomListPrimaryFilters"; +import { SortOption } from "../../../../../../src/components/viewmodels/roomlist/useSorter"; describe("", () => { let vm: RoomListViewState; @@ -26,6 +27,8 @@ describe("", () => { ], activateSecondaryFilter: () => {}, activeSecondaryFilter: SecondaryFilters.AllActivity, + sort: jest.fn(), + activeSortOption: SortOption.Activity, }; });