Skip to content

Commit dfebcfe

Browse files
committed
Introduce section sorting to the roomlist
1 parent e225c23 commit dfebcfe

File tree

10 files changed

+111
-18
lines changed

10 files changed

+111
-18
lines changed

src/components/viewmodels/roomlist/RoomListHeaderViewModel.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,11 +121,12 @@ export interface RoomListHeaderViewState {
121121
/**
122122
* Change the sort order of the room-list.
123123
*/
124-
sort: (option: SortOption) => void;
124+
sort: (option: SortOption, grouped: boolean) => void;
125125
/**
126126
* The currently active sort option.
127127
*/
128128
activeSortOption: SortOption;
129+
grouped: boolean;
129130
}
130131

131132
/**
@@ -147,7 +148,7 @@ export function useRoomListHeaderViewModel(): RoomListHeaderViewState {
147148

148149
/* Actions */
149150

150-
const { activeSortOption, sort } = useSorter();
151+
const { activeSortOption, grouped, sort } = useSorter();
151152

152153
const createChatRoom = useCallback((e: Event) => {
153154
defaultDispatcher.fire(Action.CreateChat);
@@ -219,6 +220,7 @@ export function useRoomListHeaderViewModel(): RoomListHeaderViewState {
219220
openSpacePreferences,
220221
openSpaceSettings,
221222
activeSortOption,
223+
grouped,
222224
sort,
223225
};
224226
}

src/components/viewmodels/roomlist/useSorter.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import { useState } from "react";
99
import RoomListStoreV3 from "../../../stores/room-list-v3/RoomListStoreV3";
1010
import { SortingAlgorithm } from "../../../stores/room-list-v3/skip-list/sorters";
1111
import SettingsStore from "../../../settings/SettingsStore";
12+
import { FilterKey } from "../../../stores/room-list-v3/skip-list/filters";
13+
import { arrayHasDiff } from "../../../utils/arrays.ts";
1214

1315
/**
1416
* Sorting options made available to the view.
@@ -35,10 +37,14 @@ const sortOptionToSortingAlgorithm = {
3537
};
3638

3739
interface SortState {
38-
sort: (option: SortOption) => void;
40+
sort: (option: SortOption, grouped: boolean) => void;
3941
activeSortOption: SortOption;
42+
grouped: boolean;
4043
}
4144

45+
const defaultSections = [FilterKey.FavouriteFilter, FilterKey.PeopleFilter, null];
46+
const noSections = [null];
47+
4248
/**
4349
* This hook does two things:
4450
* - Provides a way to track the currently active sort option.
@@ -48,15 +54,19 @@ export function useSorter(): SortState {
4854
const [activeSortingAlgorithm, setActiveSortingAlgorithm] = useState(() =>
4955
SettingsStore.getValue("RoomList.preferredSorting"),
5056
);
57+
const [activeSections, setActiveSections] = useState(() => SettingsStore.getValue("RoomList.sections"));
5158

52-
const sort = (option: SortOption): void => {
59+
const sort = (option: SortOption, grouped: boolean): void => {
5360
const sortingAlgorithm = sortOptionToSortingAlgorithm[option];
54-
RoomListStoreV3.instance.resort(sortingAlgorithm);
61+
const sections = grouped ? defaultSections : noSections;
62+
RoomListStoreV3.instance.resort(sortingAlgorithm, sections);
5563
setActiveSortingAlgorithm(sortingAlgorithm);
64+
setActiveSections(sections);
5665
};
5766

5867
return {
5968
sort,
6069
activeSortOption: sortingAlgorithmToSortingOption[activeSortingAlgorithm!],
70+
grouped: arrayHasDiff(noSections, activeSections),
6171
};
6272
}

src/components/views/rooms/RoomListPanel/RoomListOptionsMenu.tsx

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

8-
import { IconButton, Menu, MenuTitle, Tooltip, RadioMenuItem } from "@vector-im/compound-web";
8+
import { IconButton, Menu, MenuTitle, Tooltip, RadioMenuItem, CheckboxMenuItem } from "@vector-im/compound-web";
99
import React, { type Ref, type JSX, useState, useCallback } from "react";
1010
import OverflowHorizontalIcon from "@vector-im/compound-design-tokens/assets/web/icons/overflow-horizontal";
1111

@@ -36,11 +36,15 @@ export function RoomListOptionsMenu({ vm }: Props): JSX.Element {
3636
const [open, setOpen] = useState(false);
3737

3838
const onActivitySelected = useCallback(() => {
39-
vm.sort(SortOption.Activity);
39+
vm.sort(SortOption.Activity, vm.grouped);
4040
}, [vm]);
4141

4242
const onAtoZSelected = useCallback(() => {
43-
vm.sort(SortOption.AToZ);
43+
vm.sort(SortOption.AToZ, vm.grouped);
44+
}, [vm]);
45+
46+
const onGroupedSelected = useCallback(() => {
47+
vm.sort(vm.activeSortOption, !vm.grouped);
4448
}, [vm]);
4549

4650
return (
@@ -63,6 +67,7 @@ export function RoomListOptionsMenu({ vm }: Props): JSX.Element {
6367
checked={vm.activeSortOption === SortOption.AToZ}
6468
onSelect={onAtoZSelected}
6569
/>
70+
<CheckboxMenuItem label={_t("room_list|grouped")} checked={vm.grouped} onSelect={onGroupedSelected} />
6671
</Menu>
6772
);
6873
}

src/i18n/strings/en_EN.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2152,6 +2152,7 @@
21522152
"rooms": "Rooms",
21532153
"unread": "Unreads"
21542154
},
2155+
"grouped": "Show favourites first",
21552156
"home_menu_label": "Home options",
21562157
"join_public_room_label": "Join public room",
21572158
"joining_rooms_status": {

src/settings/Settings.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ import { SortingAlgorithm } from "../stores/room-list-v3/skip-list/sorters/index
5050
import MediaPreviewConfigController from "./controllers/MediaPreviewConfigController.ts";
5151
import InviteRulesConfigController from "./controllers/InviteRulesConfigController.ts";
5252
import { type ComputedInviteConfig } from "../@types/invite-rules.ts";
53+
import { FilterKey } from "../stores/room-list-v3/skip-list/filters";
5354

5455
export const defaultWatchManager = new WatchManager();
5556

@@ -324,6 +325,7 @@ export interface Settings {
324325
"lowBandwidth": IBaseSetting<boolean>;
325326
"fallbackICEServerAllowed": IBaseSetting<boolean | null>;
326327
"RoomList.preferredSorting": IBaseSetting<SortingAlgorithm>;
328+
"RoomList.sections": IBaseSetting<(FilterKey | null)[]>;
327329
"RoomList.showMessagePreview": IBaseSetting<boolean>;
328330
"RightPanel.phasesGlobal": IBaseSetting<IRightPanelForRoomStored | null>;
329331
"RightPanel.phases": IBaseSetting<IRightPanelForRoomStored | null>;
@@ -1199,6 +1201,10 @@ export const SETTINGS: Settings = {
11991201
supportedLevels: [SettingLevel.DEVICE],
12001202
default: SortingAlgorithm.Recency,
12011203
},
1204+
"RoomList.sections": {
1205+
supportedLevels: [SettingLevel.DEVICE],
1206+
default: [FilterKey.FavouriteFilter, FilterKey.PeopleFilter, null],
1207+
},
12021208
"RoomList.showMessagePreview": {
12031209
supportedLevels: [SettingLevel.DEVICE],
12041210
default: false,

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

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import { SettingLevel } from "../../settings/SettingLevel";
3535
import { MARKED_UNREAD_TYPE_STABLE, MARKED_UNREAD_TYPE_UNSTABLE } from "../../utils/notifications";
3636
import { getChangedOverrideRoomMutePushRules } from "../room-list/utils/roomMute";
3737
import { Action } from "../../dispatcher/actions";
38+
import { SectionSorter } from "./skip-list/sorters/SectionSorter.ts";
3839

3940
/**
4041
* These are the filters passed to the room skip list.
@@ -131,24 +132,27 @@ export class RoomListStoreV3Class extends AsyncStoreWithClient<EmptyObject> {
131132
/**
132133
* Resort the list of rooms using a different algorithm.
133134
* @param algorithm The sorting algorithm to use.
135+
* @param sections Which sections to group rooms into
134136
*/
135-
public resort(algorithm: SortingAlgorithm): void {
137+
public resort(algorithm: SortingAlgorithm, sections: (FilterKey | null)[]): void {
136138
if (!this.roomSkipList) throw new Error("Cannot resort room list before skip list is created.");
137139
if (!this.matrixClient) throw new Error("Cannot resort room list without matrix client.");
138-
if (this.roomSkipList.activeSortAlgorithm === algorithm) return;
139140
const sorter =
140141
algorithm === SortingAlgorithm.Alphabetic
141142
? new AlphabeticSorter()
142143
: new RecencySorter(this.matrixClient.getSafeUserId());
143-
this.roomSkipList.useNewSorter(sorter, this.getRooms());
144+
const sectionSorter = new SectionSorter(sorter, sections);
145+
if (this.roomSkipList.activeSortAlgorithm === sectionSorter.type) return;
146+
this.roomSkipList.useNewSorter(sectionSorter, this.getRooms());
144147
this.emit(LISTS_UPDATE_EVENT);
145148
SettingsStore.setValue("RoomList.preferredSorting", null, SettingLevel.DEVICE, algorithm);
149+
SettingsStore.setValue("RoomList.sections", null, SettingLevel.DEVICE, sections);
146150
}
147151

148152
/**
149153
* Currently active sorting algorithm if the store is ready or undefined otherwise.
150154
*/
151-
public get activeSortAlgorithm(): SortingAlgorithm | undefined {
155+
public get activeSortAlgorithm(): string | undefined {
152156
return this.roomSkipList?.activeSortAlgorithm;
153157
}
154158

@@ -321,11 +325,12 @@ export class RoomListStoreV3Class extends AsyncStoreWithClient<EmptyObject> {
321325
*/
322326
private getPreferredSorter(myUserId: string): Sorter {
323327
const preferred = SettingsStore.getValue("RoomList.preferredSorting");
328+
const sections = SettingsStore.getValue("RoomList.sections");
324329
switch (preferred) {
325330
case SortingAlgorithm.Alphabetic:
326-
return new AlphabeticSorter();
331+
return new SectionSorter(new AlphabeticSorter(), sections);
327332
case SortingAlgorithm.Recency:
328-
return new RecencySorter(myUserId);
333+
return new SectionSorter(new RecencySorter(myUserId), sections);
329334
default:
330335
throw new Error(`Got unknown sort preference from RoomList.preferredSorting setting`);
331336
}

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

Lines changed: 2 additions & 2 deletions
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, SortingAlgorithm } from "./sorters";
9+
import type { Sorter } from "./sorters";
1010
import type { Filter, FilterKey } from "./filters";
1111
import { RoomNode } from "./RoomNode";
1212
import { shouldPromote } from "./utils";
@@ -227,7 +227,7 @@ export class RoomSkipList implements Iterable<Room> {
227227
/**
228228
* The currently active sorting algorithm.
229229
*/
230-
public get activeSortAlgorithm(): SortingAlgorithm {
230+
public get activeSortAlgorithm(): string {
231231
return this.sorter.type;
232232
}
233233
}

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

Lines changed: 1 addition & 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
import type { Room } from "matrix-js-sdk/src/matrix";
88

9-
export const enum FilterKey {
9+
export enum FilterKey {
1010
FavouriteFilter,
1111
UnreadFilter,
1212
PeopleFilter,
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
Copyright 2025 New Vector Ltd.
3+
4+
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
5+
Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
import type { Room } from "matrix-js-sdk/src/matrix";
9+
import type { Sorter } from ".";
10+
import { type Filter, FilterKey } from "../filters";
11+
import { FavouriteFilter } from "../filters/FavouriteFilter.ts";
12+
import { PeopleFilter } from "../filters/PeopleFilter.ts";
13+
import { InvitesFilter } from "../filters/InvitesFilter.ts";
14+
import { UnreadFilter } from "../filters/UnreadFilter.ts";
15+
import { RoomsFilter } from "../filters/RoomsFilter.ts";
16+
import { MentionsFilter } from "../filters/MentionsFilter.ts";
17+
import { LowPriorityFilter } from "../filters/LowPriorityFilter.ts";
18+
19+
const filters: { [T in FilterKey]?: Filter } = {
20+
[FilterKey.FavouriteFilter]: new FavouriteFilter(),
21+
[FilterKey.UnreadFilter]: new UnreadFilter(),
22+
[FilterKey.PeopleFilter]: new PeopleFilter(),
23+
[FilterKey.RoomsFilter]: new RoomsFilter(),
24+
[FilterKey.InvitesFilter]: new InvitesFilter(),
25+
[FilterKey.MentionsFilter]: new MentionsFilter(),
26+
[FilterKey.LowPriorityFilter]: new LowPriorityFilter(),
27+
};
28+
29+
export class SectionSorter implements Sorter {
30+
public constructor(
31+
public readonly wrapped: Sorter,
32+
public readonly sections: (FilterKey | null)[],
33+
) {}
34+
35+
public sort(rooms: Room[]): Room[] {
36+
return [...rooms].sort((a, b) => {
37+
return this.comparator(a, b);
38+
});
39+
}
40+
41+
private getSectionIndex(room: Room): number {
42+
for (let index = 0; index < this.sections.length; index++) {
43+
const key = this.sections[index];
44+
if (key !== null && filters[key] && filters[key].matches(room)) {
45+
return index;
46+
}
47+
}
48+
return this.sections.indexOf(null);
49+
}
50+
51+
public comparator(roomA: Room, roomB: Room): number {
52+
const sectionA = this.getSectionIndex(roomA);
53+
const sectionB = this.getSectionIndex(roomB);
54+
if (sectionA === sectionB) {
55+
return this.wrapped.comparator(roomA, roomB);
56+
} else {
57+
return sectionA - sectionB;
58+
}
59+
}
60+
61+
public get type(): string {
62+
return ["grouping", this.wrapped.type, ...this.sections].join("|");
63+
}
64+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export interface Sorter {
2424
/**
2525
* A string that uniquely identifies this given sorter.
2626
*/
27-
type: SortingAlgorithm;
27+
type: string;
2828
}
2929

3030
/**

0 commit comments

Comments
 (0)