Skip to content

Commit f4b03a1

Browse files
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
1 parent be3778b commit f4b03a1

File tree

7 files changed

+101
-18
lines changed

7 files changed

+101
-18
lines changed

src/settings/Settings.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import { type ReleaseAnnouncementData } from "../stores/ReleaseAnnouncementStore
4444
import { type Json, type JsonValue } from "../@types/json.ts";
4545
import { type RecentEmojiData } from "../emojipicker/recent.ts";
4646
import { type Assignable } from "../@types/common.ts";
47+
import { SortingAlgorithm } from "../stores/room-list-v3/skip-list/sorters/index.ts";
4748

4849
export const defaultWatchManager = new WatchManager();
4950

@@ -311,6 +312,7 @@ export interface Settings {
311312
"lowBandwidth": IBaseSetting<boolean>;
312313
"fallbackICEServerAllowed": IBaseSetting<boolean | null>;
313314
"showImages": IBaseSetting<boolean>;
315+
"RoomList.preferredSorting": IBaseSetting<SortingAlgorithm>;
314316
"RightPanel.phasesGlobal": IBaseSetting<IRightPanelForRoomStored | null>;
315317
"RightPanel.phases": IBaseSetting<IRightPanelForRoomStored | null>;
316318
"enableEventIndexing": IBaseSetting<boolean>;
@@ -1114,6 +1116,10 @@ export const SETTINGS: Settings = {
11141116
displayName: _td("settings|image_thumbnails"),
11151117
default: true,
11161118
},
1119+
"RoomList.preferredSorting": {
1120+
supportedLevels: [SettingLevel.DEVICE],
1121+
default: SortingAlgorithm.Recency,
1122+
},
11171123
"RightPanel.phasesGlobal": {
11181124
supportedLevels: [SettingLevel.DEVICE],
11191125
default: null,

src/stores/room-list-v3/RoomListStoreV3.ts

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ import { RoomsFilter } from "./skip-list/filters/RoomsFilter";
3131
import { InvitesFilter } from "./skip-list/filters/InvitesFilter";
3232
import { MentionsFilter } from "./skip-list/filters/MentionsFilter";
3333
import { LowPriorityFilter } from "./skip-list/filters/LowPriorityFilter";
34+
import { type Sorter, SortingAlgorithm } from "./skip-list/sorters";
35+
import { SettingLevel } from "../../settings/SettingLevel";
3436

3537
/**
3638
* These are the filters passed to the room skip list.
@@ -93,28 +95,32 @@ export class RoomListStoreV3Class extends AsyncStoreWithClient<EmptyObject> {
9395
}
9496

9597
/**
96-
* Re-sort the list of rooms by alphabetic order.
98+
* Resort the list of rooms using a different algorithm.
99+
* @param algorithm The sorting algorithm to use.
97100
*/
98-
public useAlphabeticSorting(): void {
99-
if (this.roomSkipList) {
100-
const sorter = new AlphabeticSorter();
101-
this.roomSkipList.useNewSorter(sorter, this.getRooms());
102-
}
101+
public resort(algorithm: SortingAlgorithm): void {
102+
if (!this.roomSkipList) throw new Error("Cannot resort room list before skip list is created.");
103+
if (!this.matrixClient) throw new Error("Cannot resort room list without matrix client.");
104+
if (this.roomSkipList.activeSortAlgorithm === algorithm) return;
105+
const sorter =
106+
algorithm === SortingAlgorithm.Alphabetic
107+
? new AlphabeticSorter()
108+
: new RecencySorter(this.matrixClient.getSafeUserId());
109+
this.roomSkipList.useNewSorter(sorter, this.getRooms());
110+
this.emit(LISTS_UPDATE_EVENT);
111+
SettingsStore.setValue("RoomList.preferredSorting", null, SettingLevel.DEVICE, algorithm);
103112
}
104113

105114
/**
106-
* Re-sort the list of rooms by recency.
115+
* Currently active sorting algorithm if the store is ready or undefined otherwise.
107116
*/
108-
public useRecencySorting(): void {
109-
if (this.roomSkipList && this.matrixClient) {
110-
const sorter = new RecencySorter(this.matrixClient?.getSafeUserId() ?? "");
111-
this.roomSkipList.useNewSorter(sorter, this.getRooms());
112-
}
117+
public get activeSortAlgorithm(): SortingAlgorithm | undefined {
118+
return this.roomSkipList?.activeSortAlgorithm;
113119
}
114120

115121
protected async onReady(): Promise<any> {
116122
if (this.roomSkipList?.initialized || !this.matrixClient) return;
117-
const sorter = new RecencySorter(this.matrixClient.getSafeUserId());
123+
const sorter = this.getPreferredSorter(this.matrixClient.getSafeUserId());
118124
this.roomSkipList = new RoomSkipList(sorter, FILTERS);
119125
await SpaceStore.instance.storeReadyPromise;
120126
const rooms = this.getRooms();
@@ -214,6 +220,23 @@ export class RoomListStoreV3Class extends AsyncStoreWithClient<EmptyObject> {
214220
}
215221
}
216222

223+
/**
224+
* Create the correct sorter depending on the persisted user preference.
225+
* @param myUserId The user-id of our user.
226+
* @returns Sorter object that can be passed to the skip list.
227+
*/
228+
private getPreferredSorter(myUserId: string): Sorter {
229+
const preferred = SettingsStore.getValue("RoomList.preferredSorting");
230+
switch (preferred) {
231+
case SortingAlgorithm.Alphabetic:
232+
return new AlphabeticSorter();
233+
case SortingAlgorithm.Recency:
234+
return new RecencySorter(myUserId);
235+
default:
236+
throw new Error(`Got unknown sort preference from RoomList.preferredSorting setting`);
237+
}
238+
}
239+
217240
/**
218241
* Add a room to the skiplist and emit an update.
219242
* @param room The room to add to the skiplist

src/stores/room-list-v3/skip-list/RoomSkipList.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Please see LICENSE files in the repository root for full details.
66
*/
77

88
import type { Room } from "matrix-js-sdk/src/matrix";
9-
import type { Sorter } from "./sorters";
9+
import type { Sorter, SortingAlgorithm } from "./sorters";
1010
import type { Filter, FilterKey } from "./filters";
1111
import { RoomNode } from "./RoomNode";
1212
import { shouldPromote } from "./utils";
@@ -204,4 +204,11 @@ export class RoomSkipList implements Iterable<Room> {
204204
public get size(): number {
205205
return this.levels[0].size;
206206
}
207+
208+
/**
209+
* The currently active sorting algorithm.
210+
*/
211+
public get activeSortAlgorithm(): SortingAlgorithm {
212+
return this.sorter.type;
213+
}
207214
}

src/stores/room-list-v3/skip-list/sorters/AlphabeticSorter.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Please see LICENSE files in the repository root for full details.
66
*/
77

88
import type { Room } from "matrix-js-sdk/src/matrix";
9-
import type { Sorter } from ".";
9+
import { type Sorter, SortingAlgorithm } from ".";
1010

1111
export class AlphabeticSorter implements Sorter {
1212
private readonly collator = new Intl.Collator();
@@ -20,4 +20,8 @@ export class AlphabeticSorter implements Sorter {
2020
public comparator(roomA: Room, roomB: Room): number {
2121
return this.collator.compare(roomA.name, roomB.name);
2222
}
23+
24+
public get type(): SortingAlgorithm.Alphabetic {
25+
return SortingAlgorithm.Alphabetic;
26+
}
2327
}

src/stores/room-list-v3/skip-list/sorters/RecencySorter.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Please see LICENSE files in the repository root for full details.
66
*/
77

88
import type { Room } from "matrix-js-sdk/src/matrix";
9-
import type { Sorter } from ".";
9+
import { type Sorter, SortingAlgorithm } from ".";
1010
import { getLastTs } from "../../../room-list/algorithms/tag-sorting/RecentAlgorithm";
1111

1212
export class RecencySorter implements Sorter {
@@ -23,6 +23,10 @@ export class RecencySorter implements Sorter {
2323
return roomBLastTs - roomALastTs;
2424
}
2525

26+
public get type(): SortingAlgorithm.Recency {
27+
return SortingAlgorithm.Recency;
28+
}
29+
2630
private getTs(room: Room, cache?: { [roomId: string]: number }): number {
2731
const ts = cache?.[room.roomId] ?? getLastTs(room, this.myUserId);
2832
if (cache) {

src/stores/room-list-v3/skip-list/sorters/index.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,29 @@ Please see LICENSE files in the repository root for full details.
88
import type { Room } from "matrix-js-sdk/src/matrix";
99

1010
export interface Sorter {
11+
/**
12+
* Performs an initial sort of rooms and returns a new array containing
13+
* the result.
14+
* @param rooms An array of rooms.
15+
*/
1116
sort(rooms: Room[]): Room[];
17+
/**
18+
* The comparator used for sorting.
19+
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#comparefn
20+
* @param roomA Room
21+
* @param roomB Room
22+
*/
1223
comparator(roomA: Room, roomB: Room): number;
24+
/**
25+
* A string that uniquely identifies this given sorter.
26+
*/
27+
type: SortingAlgorithm;
28+
}
29+
30+
/**
31+
* All the available sorting algorithms.
32+
*/
33+
export const enum SortingAlgorithm {
34+
Recency = "Recency",
35+
Alphabetic = "Alphabetic",
1336
}

test/unit-tests/stores/room-list-v3/RoomListStoreV3-test.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ import { DefaultTagID } from "../../../../src/stores/room-list/models";
2424
import { FilterKey } from "../../../../src/stores/room-list-v3/skip-list/filters";
2525
import { RoomNotificationStateStore } from "../../../../src/stores/notifications/RoomNotificationStateStore";
2626
import DMRoomMap from "../../../../src/utils/DMRoomMap";
27+
import { SortingAlgorithm } from "../../../../src/stores/room-list-v3/skip-list/sorters";
28+
import SettingsStore from "../../../../src/settings/SettingsStore";
2729

2830
describe("RoomListStoreV3", () => {
2931
async function getRoomListStore() {
@@ -53,6 +55,10 @@ describe("RoomListStoreV3", () => {
5355
}) as () => DMRoomMap);
5456
});
5557

58+
afterEach(() => {
59+
jest.restoreAllMocks();
60+
});
61+
5662
it("Provides an unsorted list of rooms", async () => {
5763
const { store, rooms } = await getRoomListStore();
5864
expect(store.getRooms()).toEqual(rooms);
@@ -69,14 +75,24 @@ describe("RoomListStoreV3", () => {
6975
const { store, rooms, client } = await getRoomListStore();
7076

7177
// List is sorted by recency, sort by alphabetical now
72-
store.useAlphabeticSorting();
78+
store.resort(SortingAlgorithm.Alphabetic);
7379
let sortedRooms = new AlphabeticSorter().sort(rooms);
7480
expect(store.getSortedRooms()).toEqual(sortedRooms);
81+
expect(store.activeSortAlgorithm).toEqual(SortingAlgorithm.Alphabetic);
7582

7683
// Go back to recency sorting
77-
store.useRecencySorting();
84+
store.resort(SortingAlgorithm.Recency);
7885
sortedRooms = new RecencySorter(client.getSafeUserId()).sort(rooms);
7986
expect(store.getSortedRooms()).toEqual(sortedRooms);
87+
expect(store.activeSortAlgorithm).toEqual(SortingAlgorithm.Recency);
88+
});
89+
90+
it("Uses preferred sorter on startup", async () => {
91+
jest.spyOn(SettingsStore, "getValue").mockImplementation(() => {
92+
return SortingAlgorithm.Alphabetic;
93+
});
94+
const { store } = await getRoomListStore();
95+
expect(store.activeSortAlgorithm).toEqual(SortingAlgorithm.Alphabetic);
8096
});
8197

8298
describe("Updates", () => {

0 commit comments

Comments
 (0)