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 Json, type JsonValue } from "../@types/json.ts";
|
||||||
import { type RecentEmojiData } from "../emojipicker/recent.ts";
|
import { type RecentEmojiData } from "../emojipicker/recent.ts";
|
||||||
import { type Assignable } from "../@types/common.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();
|
export const defaultWatchManager = new WatchManager();
|
||||||
|
|
||||||
@@ -311,6 +312,7 @@ export interface Settings {
|
|||||||
"lowBandwidth": IBaseSetting<boolean>;
|
"lowBandwidth": IBaseSetting<boolean>;
|
||||||
"fallbackICEServerAllowed": IBaseSetting<boolean | null>;
|
"fallbackICEServerAllowed": IBaseSetting<boolean | null>;
|
||||||
"showImages": IBaseSetting<boolean>;
|
"showImages": IBaseSetting<boolean>;
|
||||||
|
"RoomList.preferredSorting": IBaseSetting<SortingAlgorithm>;
|
||||||
"RightPanel.phasesGlobal": IBaseSetting<IRightPanelForRoomStored | null>;
|
"RightPanel.phasesGlobal": IBaseSetting<IRightPanelForRoomStored | null>;
|
||||||
"RightPanel.phases": IBaseSetting<IRightPanelForRoomStored | null>;
|
"RightPanel.phases": IBaseSetting<IRightPanelForRoomStored | null>;
|
||||||
"enableEventIndexing": IBaseSetting<boolean>;
|
"enableEventIndexing": IBaseSetting<boolean>;
|
||||||
@@ -1114,6 +1116,10 @@ export const SETTINGS: Settings = {
|
|||||||
displayName: _td("settings|image_thumbnails"),
|
displayName: _td("settings|image_thumbnails"),
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
|
"RoomList.preferredSorting": {
|
||||||
|
supportedLevels: [SettingLevel.DEVICE],
|
||||||
|
default: SortingAlgorithm.Recency,
|
||||||
|
},
|
||||||
"RightPanel.phasesGlobal": {
|
"RightPanel.phasesGlobal": {
|
||||||
supportedLevels: [SettingLevel.DEVICE],
|
supportedLevels: [SettingLevel.DEVICE],
|
||||||
default: null,
|
default: null,
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ import { RoomsFilter } from "./skip-list/filters/RoomsFilter";
|
|||||||
import { InvitesFilter } from "./skip-list/filters/InvitesFilter";
|
import { InvitesFilter } from "./skip-list/filters/InvitesFilter";
|
||||||
import { MentionsFilter } from "./skip-list/filters/MentionsFilter";
|
import { MentionsFilter } from "./skip-list/filters/MentionsFilter";
|
||||||
import { LowPriorityFilter } from "./skip-list/filters/LowPriorityFilter";
|
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.
|
* 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 {
|
public resort(algorithm: SortingAlgorithm): void {
|
||||||
if (this.roomSkipList) {
|
if (!this.roomSkipList) throw new Error("Cannot resort room list before skip list is created.");
|
||||||
const sorter = new AlphabeticSorter();
|
if (!this.matrixClient) throw new Error("Cannot resort room list without matrix client.");
|
||||||
this.roomSkipList.useNewSorter(sorter, this.getRooms());
|
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 {
|
public get activeSortAlgorithm(): SortingAlgorithm | undefined {
|
||||||
if (this.roomSkipList && this.matrixClient) {
|
return this.roomSkipList?.activeSortAlgorithm;
|
||||||
const sorter = new RecencySorter(this.matrixClient?.getSafeUserId() ?? "");
|
|
||||||
this.roomSkipList.useNewSorter(sorter, this.getRooms());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async onReady(): Promise<any> {
|
protected async onReady(): Promise<any> {
|
||||||
if (this.roomSkipList?.initialized || !this.matrixClient) return;
|
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);
|
this.roomSkipList = new RoomSkipList(sorter, FILTERS);
|
||||||
await SpaceStore.instance.storeReadyPromise;
|
await SpaceStore.instance.storeReadyPromise;
|
||||||
const rooms = this.getRooms();
|
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.
|
* Add a room to the skiplist and emit an update.
|
||||||
* @param room The room to add to the skiplist
|
* @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 { 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 type { Filter, FilterKey } from "./filters";
|
||||||
import { RoomNode } from "./RoomNode";
|
import { RoomNode } from "./RoomNode";
|
||||||
import { shouldPromote } from "./utils";
|
import { shouldPromote } from "./utils";
|
||||||
@@ -204,4 +204,11 @@ export class RoomSkipList implements Iterable<Room> {
|
|||||||
public get size(): number {
|
public get size(): number {
|
||||||
return this.levels[0].size;
|
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 { Room } from "matrix-js-sdk/src/matrix";
|
||||||
import type { Sorter } from ".";
|
import { type Sorter, SortingAlgorithm } from ".";
|
||||||
|
|
||||||
export class AlphabeticSorter implements Sorter {
|
export class AlphabeticSorter implements Sorter {
|
||||||
private readonly collator = new Intl.Collator();
|
private readonly collator = new Intl.Collator();
|
||||||
@@ -20,4 +20,8 @@ export class AlphabeticSorter implements Sorter {
|
|||||||
public comparator(roomA: Room, roomB: Room): number {
|
public comparator(roomA: Room, roomB: Room): number {
|
||||||
return this.collator.compare(roomA.name, roomB.name);
|
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 { 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";
|
import { getLastTs } from "../../../room-list/algorithms/tag-sorting/RecentAlgorithm";
|
||||||
|
|
||||||
export class RecencySorter implements Sorter {
|
export class RecencySorter implements Sorter {
|
||||||
@@ -23,6 +23,10 @@ export class RecencySorter implements Sorter {
|
|||||||
return roomBLastTs - roomALastTs;
|
return roomBLastTs - roomALastTs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get type(): SortingAlgorithm.Recency {
|
||||||
|
return SortingAlgorithm.Recency;
|
||||||
|
}
|
||||||
|
|
||||||
private getTs(room: Room, cache?: { [roomId: string]: number }): number {
|
private getTs(room: Room, cache?: { [roomId: string]: number }): number {
|
||||||
const ts = cache?.[room.roomId] ?? getLastTs(room, this.myUserId);
|
const ts = cache?.[room.roomId] ?? getLastTs(room, this.myUserId);
|
||||||
if (cache) {
|
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";
|
import type { Room } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
export interface Sorter {
|
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[];
|
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;
|
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 { FilterKey } from "../../../../src/stores/room-list-v3/skip-list/filters";
|
||||||
import { RoomNotificationStateStore } from "../../../../src/stores/notifications/RoomNotificationStateStore";
|
import { RoomNotificationStateStore } from "../../../../src/stores/notifications/RoomNotificationStateStore";
|
||||||
import DMRoomMap from "../../../../src/utils/DMRoomMap";
|
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", () => {
|
describe("RoomListStoreV3", () => {
|
||||||
async function getRoomListStore() {
|
async function getRoomListStore() {
|
||||||
@@ -53,6 +55,10 @@ describe("RoomListStoreV3", () => {
|
|||||||
}) as () => DMRoomMap);
|
}) as () => DMRoomMap);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
it("Provides an unsorted list of rooms", async () => {
|
it("Provides an unsorted list of rooms", async () => {
|
||||||
const { store, rooms } = await getRoomListStore();
|
const { store, rooms } = await getRoomListStore();
|
||||||
expect(store.getRooms()).toEqual(rooms);
|
expect(store.getRooms()).toEqual(rooms);
|
||||||
@@ -69,14 +75,24 @@ describe("RoomListStoreV3", () => {
|
|||||||
const { store, rooms, client } = await getRoomListStore();
|
const { store, rooms, client } = await getRoomListStore();
|
||||||
|
|
||||||
// List is sorted by recency, sort by alphabetical now
|
// List is sorted by recency, sort by alphabetical now
|
||||||
store.useAlphabeticSorting();
|
store.resort(SortingAlgorithm.Alphabetic);
|
||||||
let sortedRooms = new AlphabeticSorter().sort(rooms);
|
let sortedRooms = new AlphabeticSorter().sort(rooms);
|
||||||
expect(store.getSortedRooms()).toEqual(sortedRooms);
|
expect(store.getSortedRooms()).toEqual(sortedRooms);
|
||||||
|
expect(store.activeSortAlgorithm).toEqual(SortingAlgorithm.Alphabetic);
|
||||||
|
|
||||||
// Go back to recency sorting
|
// Go back to recency sorting
|
||||||
store.useRecencySorting();
|
store.resort(SortingAlgorithm.Recency);
|
||||||
sortedRooms = new RecencySorter(client.getSafeUserId()).sort(rooms);
|
sortedRooms = new RecencySorter(client.getSafeUserId()).sort(rooms);
|
||||||
expect(store.getSortedRooms()).toEqual(sortedRooms);
|
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", () => {
|
describe("Updates", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user