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:
R Midhun Suresh
2025-03-14 15:11:04 +05:30
committed by GitHub
parent be3778bef0
commit f4b03a1b06
7 changed files with 101 additions and 18 deletions

View File

@@ -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,

View File

@@ -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

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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) {

View File

@@ -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",
}

View File

@@ -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", () => {