Room List Store: Save preferred sorting algorithm and use that on app launch (#29493)
* Add `type` property to Sorter So that we can uniquely identify any given sorting algorithm. * Add a getter for the active sort algorithm * Define a setting to store the sorting algorithm * Add a method to resort the list of rooms - Just one method where you specify the sorting algorithm by type. - Persist the new sorting algorithm using SettingsStore. * On startup, use preferred sorter * Add tests
This commit is contained in:
@@ -44,6 +44,7 @@ import { type ReleaseAnnouncementData } from "../stores/ReleaseAnnouncementStore
|
||||
import { type Json, type JsonValue } from "../@types/json.ts";
|
||||
import { type RecentEmojiData } from "../emojipicker/recent.ts";
|
||||
import { type Assignable } from "../@types/common.ts";
|
||||
import { SortingAlgorithm } from "../stores/room-list-v3/skip-list/sorters/index.ts";
|
||||
|
||||
export const defaultWatchManager = new WatchManager();
|
||||
|
||||
@@ -311,6 +312,7 @@ export interface Settings {
|
||||
"lowBandwidth": IBaseSetting<boolean>;
|
||||
"fallbackICEServerAllowed": IBaseSetting<boolean | null>;
|
||||
"showImages": IBaseSetting<boolean>;
|
||||
"RoomList.preferredSorting": IBaseSetting<SortingAlgorithm>;
|
||||
"RightPanel.phasesGlobal": IBaseSetting<IRightPanelForRoomStored | null>;
|
||||
"RightPanel.phases": IBaseSetting<IRightPanelForRoomStored | null>;
|
||||
"enableEventIndexing": IBaseSetting<boolean>;
|
||||
@@ -1114,6 +1116,10 @@ export const SETTINGS: Settings = {
|
||||
displayName: _td("settings|image_thumbnails"),
|
||||
default: true,
|
||||
},
|
||||
"RoomList.preferredSorting": {
|
||||
supportedLevels: [SettingLevel.DEVICE],
|
||||
default: SortingAlgorithm.Recency,
|
||||
},
|
||||
"RightPanel.phasesGlobal": {
|
||||
supportedLevels: [SettingLevel.DEVICE],
|
||||
default: null,
|
||||
|
||||
@@ -31,6 +31,8 @@ import { RoomsFilter } from "./skip-list/filters/RoomsFilter";
|
||||
import { InvitesFilter } from "./skip-list/filters/InvitesFilter";
|
||||
import { MentionsFilter } from "./skip-list/filters/MentionsFilter";
|
||||
import { LowPriorityFilter } from "./skip-list/filters/LowPriorityFilter";
|
||||
import { type Sorter, SortingAlgorithm } from "./skip-list/sorters";
|
||||
import { SettingLevel } from "../../settings/SettingLevel";
|
||||
|
||||
/**
|
||||
* These are the filters passed to the room skip list.
|
||||
@@ -93,28 +95,32 @@ export class RoomListStoreV3Class extends AsyncStoreWithClient<EmptyObject> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-sort the list of rooms by alphabetic order.
|
||||
* Resort the list of rooms using a different algorithm.
|
||||
* @param algorithm The sorting algorithm to use.
|
||||
*/
|
||||
public useAlphabeticSorting(): void {
|
||||
if (this.roomSkipList) {
|
||||
const sorter = new AlphabeticSorter();
|
||||
this.roomSkipList.useNewSorter(sorter, this.getRooms());
|
||||
}
|
||||
public resort(algorithm: SortingAlgorithm): void {
|
||||
if (!this.roomSkipList) throw new Error("Cannot resort room list before skip list is created.");
|
||||
if (!this.matrixClient) throw new Error("Cannot resort room list without matrix client.");
|
||||
if (this.roomSkipList.activeSortAlgorithm === algorithm) return;
|
||||
const sorter =
|
||||
algorithm === SortingAlgorithm.Alphabetic
|
||||
? new AlphabeticSorter()
|
||||
: new RecencySorter(this.matrixClient.getSafeUserId());
|
||||
this.roomSkipList.useNewSorter(sorter, this.getRooms());
|
||||
this.emit(LISTS_UPDATE_EVENT);
|
||||
SettingsStore.setValue("RoomList.preferredSorting", null, SettingLevel.DEVICE, algorithm);
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-sort the list of rooms by recency.
|
||||
* Currently active sorting algorithm if the store is ready or undefined otherwise.
|
||||
*/
|
||||
public useRecencySorting(): void {
|
||||
if (this.roomSkipList && this.matrixClient) {
|
||||
const sorter = new RecencySorter(this.matrixClient?.getSafeUserId() ?? "");
|
||||
this.roomSkipList.useNewSorter(sorter, this.getRooms());
|
||||
}
|
||||
public get activeSortAlgorithm(): SortingAlgorithm | undefined {
|
||||
return this.roomSkipList?.activeSortAlgorithm;
|
||||
}
|
||||
|
||||
protected async onReady(): Promise<any> {
|
||||
if (this.roomSkipList?.initialized || !this.matrixClient) return;
|
||||
const sorter = new RecencySorter(this.matrixClient.getSafeUserId());
|
||||
const sorter = this.getPreferredSorter(this.matrixClient.getSafeUserId());
|
||||
this.roomSkipList = new RoomSkipList(sorter, FILTERS);
|
||||
await SpaceStore.instance.storeReadyPromise;
|
||||
const rooms = this.getRooms();
|
||||
@@ -214,6 +220,23 @@ export class RoomListStoreV3Class extends AsyncStoreWithClient<EmptyObject> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the correct sorter depending on the persisted user preference.
|
||||
* @param myUserId The user-id of our user.
|
||||
* @returns Sorter object that can be passed to the skip list.
|
||||
*/
|
||||
private getPreferredSorter(myUserId: string): Sorter {
|
||||
const preferred = SettingsStore.getValue("RoomList.preferredSorting");
|
||||
switch (preferred) {
|
||||
case SortingAlgorithm.Alphabetic:
|
||||
return new AlphabeticSorter();
|
||||
case SortingAlgorithm.Recency:
|
||||
return new RecencySorter(myUserId);
|
||||
default:
|
||||
throw new Error(`Got unknown sort preference from RoomList.preferredSorting setting`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a room to the skiplist and emit an update.
|
||||
* @param room The room to add to the skiplist
|
||||
|
||||
@@ -6,7 +6,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import type { Room } from "matrix-js-sdk/src/matrix";
|
||||
import type { Sorter } from "./sorters";
|
||||
import type { Sorter, SortingAlgorithm } from "./sorters";
|
||||
import type { Filter, FilterKey } from "./filters";
|
||||
import { RoomNode } from "./RoomNode";
|
||||
import { shouldPromote } from "./utils";
|
||||
@@ -204,4 +204,11 @@ export class RoomSkipList implements Iterable<Room> {
|
||||
public get size(): number {
|
||||
return this.levels[0].size;
|
||||
}
|
||||
|
||||
/**
|
||||
* The currently active sorting algorithm.
|
||||
*/
|
||||
public get activeSortAlgorithm(): SortingAlgorithm {
|
||||
return this.sorter.type;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import type { Room } from "matrix-js-sdk/src/matrix";
|
||||
import type { Sorter } from ".";
|
||||
import { type Sorter, SortingAlgorithm } from ".";
|
||||
|
||||
export class AlphabeticSorter implements Sorter {
|
||||
private readonly collator = new Intl.Collator();
|
||||
@@ -20,4 +20,8 @@ export class AlphabeticSorter implements Sorter {
|
||||
public comparator(roomA: Room, roomB: Room): number {
|
||||
return this.collator.compare(roomA.name, roomB.name);
|
||||
}
|
||||
|
||||
public get type(): SortingAlgorithm.Alphabetic {
|
||||
return SortingAlgorithm.Alphabetic;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import type { Room } from "matrix-js-sdk/src/matrix";
|
||||
import type { Sorter } from ".";
|
||||
import { type Sorter, SortingAlgorithm } from ".";
|
||||
import { getLastTs } from "../../../room-list/algorithms/tag-sorting/RecentAlgorithm";
|
||||
|
||||
export class RecencySorter implements Sorter {
|
||||
@@ -23,6 +23,10 @@ export class RecencySorter implements Sorter {
|
||||
return roomBLastTs - roomALastTs;
|
||||
}
|
||||
|
||||
public get type(): SortingAlgorithm.Recency {
|
||||
return SortingAlgorithm.Recency;
|
||||
}
|
||||
|
||||
private getTs(room: Room, cache?: { [roomId: string]: number }): number {
|
||||
const ts = cache?.[room.roomId] ?? getLastTs(room, this.myUserId);
|
||||
if (cache) {
|
||||
|
||||
@@ -8,6 +8,29 @@ Please see LICENSE files in the repository root for full details.
|
||||
import type { Room } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
export interface Sorter {
|
||||
/**
|
||||
* Performs an initial sort of rooms and returns a new array containing
|
||||
* the result.
|
||||
* @param rooms An array of rooms.
|
||||
*/
|
||||
sort(rooms: Room[]): Room[];
|
||||
/**
|
||||
* The comparator used for sorting.
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#comparefn
|
||||
* @param roomA Room
|
||||
* @param roomB Room
|
||||
*/
|
||||
comparator(roomA: Room, roomB: Room): number;
|
||||
/**
|
||||
* A string that uniquely identifies this given sorter.
|
||||
*/
|
||||
type: SortingAlgorithm;
|
||||
}
|
||||
|
||||
/**
|
||||
* All the available sorting algorithms.
|
||||
*/
|
||||
export const enum SortingAlgorithm {
|
||||
Recency = "Recency",
|
||||
Alphabetic = "Alphabetic",
|
||||
}
|
||||
|
||||
@@ -24,6 +24,8 @@ import { DefaultTagID } from "../../../../src/stores/room-list/models";
|
||||
import { FilterKey } from "../../../../src/stores/room-list-v3/skip-list/filters";
|
||||
import { RoomNotificationStateStore } from "../../../../src/stores/notifications/RoomNotificationStateStore";
|
||||
import DMRoomMap from "../../../../src/utils/DMRoomMap";
|
||||
import { SortingAlgorithm } from "../../../../src/stores/room-list-v3/skip-list/sorters";
|
||||
import SettingsStore from "../../../../src/settings/SettingsStore";
|
||||
|
||||
describe("RoomListStoreV3", () => {
|
||||
async function getRoomListStore() {
|
||||
@@ -53,6 +55,10 @@ describe("RoomListStoreV3", () => {
|
||||
}) as () => DMRoomMap);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("Provides an unsorted list of rooms", async () => {
|
||||
const { store, rooms } = await getRoomListStore();
|
||||
expect(store.getRooms()).toEqual(rooms);
|
||||
@@ -69,14 +75,24 @@ describe("RoomListStoreV3", () => {
|
||||
const { store, rooms, client } = await getRoomListStore();
|
||||
|
||||
// List is sorted by recency, sort by alphabetical now
|
||||
store.useAlphabeticSorting();
|
||||
store.resort(SortingAlgorithm.Alphabetic);
|
||||
let sortedRooms = new AlphabeticSorter().sort(rooms);
|
||||
expect(store.getSortedRooms()).toEqual(sortedRooms);
|
||||
expect(store.activeSortAlgorithm).toEqual(SortingAlgorithm.Alphabetic);
|
||||
|
||||
// Go back to recency sorting
|
||||
store.useRecencySorting();
|
||||
store.resort(SortingAlgorithm.Recency);
|
||||
sortedRooms = new RecencySorter(client.getSafeUserId()).sort(rooms);
|
||||
expect(store.getSortedRooms()).toEqual(sortedRooms);
|
||||
expect(store.activeSortAlgorithm).toEqual(SortingAlgorithm.Recency);
|
||||
});
|
||||
|
||||
it("Uses preferred sorter on startup", async () => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation(() => {
|
||||
return SortingAlgorithm.Alphabetic;
|
||||
});
|
||||
const { store } = await getRoomListStore();
|
||||
expect(store.activeSortAlgorithm).toEqual(SortingAlgorithm.Alphabetic);
|
||||
});
|
||||
|
||||
describe("Updates", () => {
|
||||
|
||||
Reference in New Issue
Block a user