mirror of
https://github.com/vector-im/element-web.git
synced 2025-11-30 06:51:40 +01:00
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
This commit is contained in:
parent
c31f5521ec
commit
9fb52e984c
@ -12,6 +12,7 @@ import type { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPaylo
|
|||||||
import dispatcher from "../../../dispatcher/dispatcher";
|
import dispatcher from "../../../dispatcher/dispatcher";
|
||||||
import { Action } from "../../../dispatcher/actions";
|
import { Action } from "../../../dispatcher/actions";
|
||||||
import { type PrimaryFilter, type SecondaryFilters, useFilteredRooms } from "./useFilteredRooms";
|
import { type PrimaryFilter, type SecondaryFilters, useFilteredRooms } from "./useFilteredRooms";
|
||||||
|
import { type SortOption, useSorter } from "./useSorter";
|
||||||
|
|
||||||
export interface RoomListViewState {
|
export interface RoomListViewState {
|
||||||
/**
|
/**
|
||||||
@ -39,6 +40,16 @@ export interface RoomListViewState {
|
|||||||
* The currently active secondary filter.
|
* The currently active secondary filter.
|
||||||
*/
|
*/
|
||||||
activeSecondaryFilter: SecondaryFilters;
|
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 {
|
export function useRoomListViewModel(): RoomListViewState {
|
||||||
const { primaryFilters, rooms, activateSecondaryFilter, activeSecondaryFilter } = useFilteredRooms();
|
const { primaryFilters, rooms, activateSecondaryFilter, activeSecondaryFilter } = useFilteredRooms();
|
||||||
|
const { activeSortOption, sort } = useSorter();
|
||||||
|
|
||||||
const openRoom = useCallback((roomId: string): void => {
|
const openRoom = useCallback((roomId: string): void => {
|
||||||
dispatcher.dispatch<ViewRoomPayload>({
|
dispatcher.dispatch<ViewRoomPayload>({
|
||||||
@ -62,5 +74,7 @@ export function useRoomListViewModel(): RoomListViewState {
|
|||||||
primaryFilters,
|
primaryFilters,
|
||||||
activateSecondaryFilter,
|
activateSecondaryFilter,
|
||||||
activeSecondaryFilter,
|
activeSecondaryFilter,
|
||||||
|
activeSortOption,
|
||||||
|
sort,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
62
src/components/viewmodels/roomlist/useSorter.ts
Normal file
62
src/components/viewmodels/roomlist/useSorter.ts
Normal file
@ -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!],
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -16,6 +16,9 @@ import dispatcher from "../../../../../src/dispatcher/dispatcher";
|
|||||||
import { Action } from "../../../../../src/dispatcher/actions";
|
import { Action } from "../../../../../src/dispatcher/actions";
|
||||||
import { FilterKey } from "../../../../../src/stores/room-list-v3/skip-list/filters";
|
import { FilterKey } from "../../../../../src/stores/room-list-v3/skip-list/filters";
|
||||||
import { SecondaryFilters } from "../../../../../src/components/viewmodels/roomlist/useFilteredRooms";
|
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", () => {
|
describe("RoomListViewModel", () => {
|
||||||
function mockAndCreateRooms() {
|
function mockAndCreateRooms() {
|
||||||
@ -26,6 +29,10 @@ describe("RoomListViewModel", () => {
|
|||||||
return { rooms, fn };
|
return { rooms, fn };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
it("should return a list of rooms", async () => {
|
it("should return a list of rooms", async () => {
|
||||||
const { rooms } = mockAndCreateRooms();
|
const { rooms } = mockAndCreateRooms();
|
||||||
const { result: vm } = renderHook(() => useRoomListViewModel());
|
const { result: vm } = renderHook(() => useRoomListViewModel());
|
||||||
@ -203,5 +210,29 @@ describe("RoomListViewModel", () => {
|
|||||||
expect(vm.current.primaryFilters.find((f) => f.name === primaryFilterName)).toBeUndefined();
|
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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import { type RoomListViewState } from "../../../../../../src/components/viewmod
|
|||||||
import { RoomList } from "../../../../../../src/components/views/rooms/RoomListPanel/RoomList";
|
import { RoomList } from "../../../../../../src/components/views/rooms/RoomListPanel/RoomList";
|
||||||
import DMRoomMap from "../../../../../../src/utils/DMRoomMap";
|
import DMRoomMap from "../../../../../../src/utils/DMRoomMap";
|
||||||
import { SecondaryFilters } from "../../../../../../src/components/viewmodels/roomlist/useFilteredRooms";
|
import { SecondaryFilters } from "../../../../../../src/components/viewmodels/roomlist/useFilteredRooms";
|
||||||
|
import { SortOption } from "../../../../../../src/components/viewmodels/roomlist/useSorter";
|
||||||
|
|
||||||
describe("<RoomList />", () => {
|
describe("<RoomList />", () => {
|
||||||
let matrixClient: MatrixClient;
|
let matrixClient: MatrixClient;
|
||||||
@ -34,6 +35,8 @@ describe("<RoomList />", () => {
|
|||||||
primaryFilters: [],
|
primaryFilters: [],
|
||||||
activateSecondaryFilter: () => {},
|
activateSecondaryFilter: () => {},
|
||||||
activeSecondaryFilter: SecondaryFilters.AllActivity,
|
activeSecondaryFilter: SecondaryFilters.AllActivity,
|
||||||
|
sort: jest.fn(),
|
||||||
|
activeSortOption: SortOption.Activity,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Needed to render a room list cell
|
// Needed to render a room list cell
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import userEvent from "@testing-library/user-event";
|
|||||||
import { type RoomListViewState } from "../../../../../../src/components/viewmodels/roomlist/RoomListViewModel";
|
import { type RoomListViewState } from "../../../../../../src/components/viewmodels/roomlist/RoomListViewModel";
|
||||||
import { SecondaryFilters } from "../../../../../../src/components/viewmodels/roomlist/useFilteredRooms";
|
import { SecondaryFilters } from "../../../../../../src/components/viewmodels/roomlist/useFilteredRooms";
|
||||||
import { RoomListPrimaryFilters } from "../../../../../../src/components/views/rooms/RoomListPanel/RoomListPrimaryFilters";
|
import { RoomListPrimaryFilters } from "../../../../../../src/components/views/rooms/RoomListPanel/RoomListPrimaryFilters";
|
||||||
|
import { SortOption } from "../../../../../../src/components/viewmodels/roomlist/useSorter";
|
||||||
|
|
||||||
describe("<RoomListPrimaryFilters />", () => {
|
describe("<RoomListPrimaryFilters />", () => {
|
||||||
let vm: RoomListViewState;
|
let vm: RoomListViewState;
|
||||||
@ -26,6 +27,8 @@ describe("<RoomListPrimaryFilters />", () => {
|
|||||||
],
|
],
|
||||||
activateSecondaryFilter: () => {},
|
activateSecondaryFilter: () => {},
|
||||||
activeSecondaryFilter: SecondaryFilters.AllActivity,
|
activeSecondaryFilter: SecondaryFilters.AllActivity,
|
||||||
|
sort: jest.fn(),
|
||||||
|
activeSortOption: SortOption.Activity,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user