Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit e0e28d0

Browse files
committed
Possible framework for a proof of concept
This is the fruits of about 3 attempts to write code that works. None of those attempts are here, but how edition 4 could work is at least documented now.
1 parent 7c55904 commit e0e28d0

File tree

6 files changed

+342
-9
lines changed

6 files changed

+342
-9
lines changed

src/stores/room-list/README.md

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
# Room list sorting
2+
3+
It's so complicated it needs its own README.
4+
5+
## Algorithms involved
6+
7+
There's two main kinds of algorithms involved in the room list store: list ordering and tag sorting.
8+
Throughout the code an intentional decision has been made to call them the List Algorithm and Sorting
9+
Algorithm respectively. The list algorithm determines the behaviour of the room list whereas the sorting
10+
algorithm determines how individual tags (lists of rooms, sometimes called sublists) are ordered.
11+
12+
Behaviour of the room list takes the shape of default sorting on tags in most cases, though it can
13+
override what is happening at the tag level depending on the algorithm used (as is the case with the
14+
importance algorithm, described later).
15+
16+
Tag sorting is effectively the comparator supplied to the list algorithm. This gives the list algorithm
17+
the power to decide when and how to apply the tag sorting, if at all.
18+
19+
### Tag sorting algorithm: Alphabetical
20+
21+
When used, rooms in a given tag will be sorted alphabetically, where the alphabet is determined by a
22+
simple string comparison operation (essentially giving the browser the problem of figuring out if A
23+
comes before Z).
24+
25+
### Tag sorting algorithm: Manual
26+
27+
Manual sorting makes use of the `order` property present on all tags for a room, per the
28+
[Matrix specification](https://matrix.org/docs/spec/client_server/r0.6.0#room-tagging). Smaller values
29+
of `order` cause rooms to appear closer to the top of the list.
30+
31+
### Tag sorting algorithm: Recent
32+
33+
Rooms are ordered by the timestamp of the most recent useful message. Usefulness is yet another algorithm
34+
in the room list system which determines whether an event type is capable of bubbling up in the room list.
35+
Normally events like room messages, stickers, and room security changes will be considered useful enough
36+
to cause a shift in time.
37+
38+
Note that this is reliant on the event timestamps of the most recent message. Because Matrix is eventually
39+
consistent this means that from time to time a room might plummet or skyrocket across the tag due to the
40+
timestamp contained within the event (generated server-side by the sender's server).
41+
42+
### List ordering algorithm: Natural
43+
44+
This is the easiest of the algorithms to understand because it does essentially nothing. It imposes no
45+
behavioural changes over the tag sorting algorithm and is by far the simplest way to order a room list.
46+
Historically, it's been the only option in Riot and extremely common in most chat applications due to
47+
its relative deterministic behaviour.
48+
49+
### List ordering algorithm: Importance
50+
51+
On the other end of the spectrum, this is the most complicated algorithm which exists. There's major
52+
behavioural changes and the tag sorting algorithm is selectively applied depending on circumstances.
53+
54+
Each tag which is not manually ordered gets split into 4 sections or "categories". Manually ordered tags
55+
simply get the manual sorting algorithm applied to them with no further involvement from the importance
56+
algorithm. There are 4 categories: Red, Grey, Bold, and Idle. Each has their own definition based off
57+
relative (perceived) importance to the user:
58+
59+
* **Red**: The room has unread mentions waiting for the user.
60+
* **Grey**: The room has unread notifications waiting for the user. Notifications are simply unread
61+
messages which cause a push notification or badge count. Typically this is the default as rooms are
62+
set to 'All Messages'.
63+
* **Bold**: The room has unread messages waiting for the user. Essentially this is a grey room without
64+
a badge/notification count (or 'Mentions Only'/'Muted').
65+
* **Idle**: No relevant activity has occurred in the room since the user last read it.
66+
67+
Conveniently, each tag is ordered by those categories as presented: red rooms appear above grey, grey
68+
above idle, etc.
69+
70+
Once the algorithm has determined which rooms belong in which categories, the tag sorting algorithm
71+
is applied to each category in a sub-sub-list fashion. This should result in the red rooms (for example)
72+
being sorted alphabetically amongst each other and the grey rooms sorted amongst each other, but
73+
collectively the tag will be sorted into categories with red being at the top.
74+
75+
The algorithm also has a concept of a 'sticky' room which is the room the user is currently viewing.
76+
The sticky room will remain in position on the room list regardless of other factors going on as typically
77+
clicking on a room will cause it to change categories into 'idle'. This is done by preserving N rooms
78+
above the selected room at all times where N is the number of rooms above the selected rooms when it was
79+
selected.
80+
81+
For example, if the user has 3 red rooms and selects the middle room, they will always see exactly one
82+
room above their selection at all times. If they receive another notification and the tag ordering is set
83+
to Recent, they'll see the new notification go to the top position and the one that was previously there
84+
fall behind the sticky room.
85+
86+
The sticky room's category is technically 'idle' while being viewed and is explicitly pulled out of the
87+
tag sorting algorithm's input as it must maintain its position in the list. When the user moves to another
88+
room, the previous sticky room is recalculated to determine which category it needs to be in as the user
89+
could have been scrolled up while new messages were received.
90+
91+
Further, the sticky room is not aware of category boundaries and thus the user can see a shift in what
92+
kinds of rooms move around their selection. An example would be the user having 4 red rooms, the user
93+
selecting the third room (leaving 2 above it), and then having the rooms above it read on another device.
94+
This would result in 1 red room and 1 other kind of room above the sticky room as it will try to maintain
95+
2 rooms above the sticky room.
96+
97+
An exception for the sticky room placement is when there's suddenly not enough rooms to maintain the placement
98+
exactly. This typically happens if the user selects a room and leaves enough rooms where it cannot maintain
99+
the N required rooms above the sticky room. In this case, the sticky room will simply decrease N as needed.
100+
The N value will never increase while selection remains unchanged: adding a bunch of rooms after having
101+
put the sticky room in a position where it's had to decrease N will not increase N.
102+
103+
## Responsibilities of the store
104+
105+
The store is responsible for the ordering, upkeep, and tracking of all rooms. The component simply gets
106+
an object containing the tags it needs to worry about and the rooms within. The room list component will
107+
decide which tags need rendering (as it commonly filters out empty tags in most cases), and will deal with
108+
all kinds of filtering.
109+
110+
## Class breakdowns
111+
112+
The `RoomListStore` is the major coordinator of various `IAlgorithm` implementations, which take care
113+
of the various `ListAlgorithm` and `SortingAlgorithm` options. A `TagManager` is responsible for figuring
114+
out which tags get which rooms, as Matrix specifies them as a reverse map: tags are defined on rooms and
115+
are not defined as a collection of rooms (unlike how they are presented to the user). Various list-specific
116+
utilities are also included, though they are expected to move somewhere more general when needed. For
117+
example, the `membership` utilities could easily be moved elsewhere as needed.

src/stores/room-list/TagManager.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
Copyright 2020 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import {EventEmitter} from "events";
18+
19+
// TODO: Docs on what this is
20+
export class TagManager extends EventEmitter {
21+
constructor() {
22+
super();
23+
}
24+
25+
// TODO: Implementation.
26+
// This will need to track where rooms belong in tags, and which tags they
27+
// should be tracked within. This is algorithm independent because all the
28+
// algorithms need to know what each tag contains.
29+
//
30+
// This will likely make use of the effective membership to determine the
31+
// invite+historical sections.
32+
}

src/stores/room-list/algorithms/ChaoticAlgorithm.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export class ChaoticAlgorithm implements IAlgorithm {
3131
private rooms: Room[] = [];
3232

3333
constructor(private representativeAlgorithm: ListAlgorithm) {
34+
console.log("Constructed a ChaoticAlgorithm");
3435
}
3536

3637
getOrderedRooms(): ITagMap {

src/stores/room-list/algorithms/IAlgorithm.ts

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,6 @@ export enum ListAlgorithm {
3232
Natural = "NATURAL",
3333
}
3434

35-
export enum Category {
36-
Red = "RED",
37-
Grey = "GREY",
38-
Bold = "BOLD",
39-
Idle = "IDLE",
40-
}
41-
4235
export interface ITagSortingMap {
4336
// @ts-ignore - TypeScript really wants this to be [tagId: string] but we know better.
4437
[tagId: TagID]: SortAlgorithm;
@@ -50,7 +43,7 @@ export interface ITagMap {
5043
}
5144

5245
// TODO: Convert IAlgorithm to an abstract class?
53-
// TODO: Add locking support to avoid concurrent writes
46+
// TODO: Add locking support to avoid concurrent writes?
5447
// TODO: EventEmitter support
5548

5649
/**
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
/*
2+
Copyright 2020 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import { IAlgorithm, ITagMap, ITagSortingMap } from "./IAlgorithm";
18+
import { Room } from "matrix-js-sdk/src/models/room";
19+
import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
20+
import { DefaultTagID, TagID } from "../models";
21+
import { splitRoomsByMembership } from "../membership";
22+
23+
/**
24+
* The determined category of a room.
25+
*/
26+
export enum Category {
27+
/**
28+
* The room has unread mentions within.
29+
*/
30+
Red = "RED",
31+
/**
32+
* The room has unread notifications within. Note that these are not unread
33+
* mentions - they are simply messages which the user has asked to cause a
34+
* badge count update or push notification.
35+
*/
36+
Grey = "GREY",
37+
/**
38+
* The room has unread messages within (grey without the badge).
39+
*/
40+
Bold = "BOLD",
41+
/**
42+
* The room has no relevant unread messages within.
43+
*/
44+
Idle = "IDLE",
45+
}
46+
47+
/**
48+
* An implementation of the "importance" algorithm for room list sorting. Where
49+
* the tag sorting algorithm does not interfere, rooms will be ordered into
50+
* categories of varying importance to the user. Alphabetical sorting does not
51+
* interfere with this algorithm, however manual ordering does.
52+
*
53+
* The importance of a room is defined by the kind of notifications, if any, are
54+
* present on the room. These are classified internally as Red, Grey, Bold, and
55+
* Idle. Red rooms have mentions, grey have unread messages, bold is a less noisy
56+
* version of grey, and idle means all activity has been seen by the user.
57+
*
58+
* The algorithm works by monitoring all room changes, including new messages in
59+
* tracked rooms, to determine if it needs a new category or different placement
60+
* within the same category. For more information, see the comments contained
61+
* within the class.
62+
*/
63+
export class ImportanceAlgorithm implements IAlgorithm {
64+
65+
// HOW THIS WORKS
66+
// --------------
67+
//
68+
// This block of comments assumes you've read the README one level higher.
69+
// You should do that if you haven't already.
70+
//
71+
// Tags are fed into the algorithmic functions from the TagManager changes,
72+
// which cause subsequent updates to the room list itself. Categories within
73+
// those tags are tracked as index numbers within the array (zero = top), with
74+
// each sticky room being tracked separately. Internally, the category index
75+
// can be found from `this.indices[tag][category]` and the sticky room information
76+
// from `this.stickyRooms[tag]`.
77+
//
78+
// Room categories are constantly re-evaluated and tracked in the `this.categorized`
79+
// object. Note that this doesn't track rooms by category but instead by room ID.
80+
// The theory is that by knowing the previous position, new desired position, and
81+
// category indices we can avoid tracking multiple complicated maps in memory.
82+
//
83+
// The room list store is always provided with the `this.cached` results, which are
84+
// updated as needed and not recalculated often. For example, when a room needs to
85+
// move within a tag, the array in `this.cached` will be spliced instead of iterated.
86+
87+
private cached: ITagMap = {};
88+
private sortAlgorithms: ITagSortingMap;
89+
private rooms: Room[] = [];
90+
private indices: {
91+
// @ts-ignore - TS wants this to be a string but we know better than it
92+
[tag: TagID]: {
93+
// @ts-ignore - TS wants this to be a string but we know better than it
94+
[category: Category]: number; // integer
95+
};
96+
} = {};
97+
private stickyRooms: {
98+
// @ts-ignore - TS wants this to be a string but we know better than it
99+
[tag: TagID]: {
100+
room?: Room;
101+
nAbove: number; // integer
102+
};
103+
} = {};
104+
private categorized: {
105+
// @ts-ignore - TS wants this to be a string but we know better than it
106+
[tag: TagID]: {
107+
// TODO: Remove note
108+
// Note: Should in theory be able to only track this by room ID as we'll know
109+
// the indices of each category and can determine if a category needs changing
110+
// in the cached list. Could potentially save a bunch of time if we can figure
111+
// out where a room is supposed to be using offsets, some math, and leaving the
112+
// array generally alone.
113+
[roomId: string]: {
114+
room: Room;
115+
category: Category;
116+
};
117+
};
118+
} = {};
119+
120+
constructor() {
121+
console.log("Constructed an ImportanceAlgorithm");
122+
}
123+
124+
getOrderedRooms(): ITagMap {
125+
return this.cached;
126+
}
127+
128+
async populateTags(tagSortingMap: ITagSortingMap): Promise<any> {
129+
if (!tagSortingMap) throw new Error(`Map cannot be null or empty`);
130+
this.sortAlgorithms = tagSortingMap;
131+
this.setKnownRooms(this.rooms); // regenerate the room lists
132+
}
133+
134+
handleRoomUpdate(room): Promise<boolean> {
135+
return undefined;
136+
}
137+
138+
setKnownRooms(rooms: Room[]): Promise<any> {
139+
if (isNullOrUndefined(rooms)) throw new Error(`Array of rooms cannot be null`);
140+
if (!this.sortAlgorithms) throw new Error(`Cannot set known rooms without a tag sorting map`);
141+
142+
this.rooms = rooms;
143+
144+
const newTags = {};
145+
for (const tagId in this.sortAlgorithms) {
146+
// noinspection JSUnfilteredForInLoop
147+
newTags[tagId] = [];
148+
}
149+
150+
// If we can avoid doing work, do so.
151+
if (!rooms.length) {
152+
this.cached = newTags;
153+
return;
154+
}
155+
156+
// TODO: Remove logging
157+
const memberships = splitRoomsByMembership(rooms);
158+
console.log({memberships});
159+
160+
// Step through each room and determine which tags it should be in.
161+
// We don't care about ordering or sorting here - we're simply organizing things.
162+
for (const room of rooms) {
163+
const tags = room.tags;
164+
let inTag = false;
165+
for (const tagId in tags) {
166+
// noinspection JSUnfilteredForInLoop
167+
if (isNullOrUndefined(newTags[tagId])) {
168+
// skip the tag if we don't know about it
169+
continue;
170+
}
171+
172+
inTag = true;
173+
174+
// noinspection JSUnfilteredForInLoop
175+
newTags[tagId].push(room);
176+
}
177+
178+
// If the room wasn't pushed to a tag, push it to the untagged tag.
179+
if (!inTag) {
180+
newTags[DefaultTagID.Untagged].push(room);
181+
}
182+
}
183+
184+
// TODO: Do sorting
185+
186+
// Finally, assign the tags to our cache
187+
this.cached = newTags;
188+
}
189+
}

src/stores/room-list/algorithms/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,11 @@ limitations under the License.
1616

1717
import { IAlgorithm, ListAlgorithm } from "./IAlgorithm";
1818
import { ChaoticAlgorithm } from "./ChaoticAlgorithm";
19+
import { ImportanceAlgorithm } from "./ImportanceAlgorithm";
1920

2021
const ALGORITHM_FACTORIES: { [algorithm in ListAlgorithm]: () => IAlgorithm } = {
2122
[ListAlgorithm.Natural]: () => new ChaoticAlgorithm(ListAlgorithm.Natural),
22-
[ListAlgorithm.Importance]: () => new ChaoticAlgorithm(ListAlgorithm.Importance),
23+
[ListAlgorithm.Importance]: () => new ImportanceAlgorithm(),
2324
};
2425

2526
/**

0 commit comments

Comments
 (0)