Skip to content

Room List Store: Save preferred sorting algorithm and use that on app launch #29493

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Mar 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/settings/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -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>;
Expand Down Expand Up @@ -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,
Expand Down
49 changes: 36 additions & 13 deletions src/stores/room-list-v3/RoomListStoreV3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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
Expand Down
9 changes: 8 additions & 1 deletion src/stores/room-list-v3/skip-list/RoomSkipList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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) {
Expand Down
23 changes: 23 additions & 0 deletions src/stores/room-list-v3/skip-list/sorters/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
}
20 changes: 18 additions & 2 deletions test/unit-tests/stores/room-list-v3/RoomListStoreV3-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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);
Expand All @@ -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", () => {
Expand Down
Loading