Break up algorithms and use the new layering

Sorting and ordering has now been split apart. The ImportanceAlgorithm also finally makes use of the sorting.

So far metrics look okay at 3ms for a simple account, though this could potentially get worse due to the multiple loops involved (one for tags, one for categories, one for ordering). We might be able to feed a whole list of rooms into the thing and have it regenerate the lists on demand.
pull/21833/head
Travis Ralston 2020-04-29 16:57:06 -06:00
parent e7fffee175
commit d244eeb5d5
11 changed files with 283 additions and 41 deletions

View File

@ -19,10 +19,11 @@ import { MatrixClient } from "matrix-js-sdk/src/client";
import { ActionPayload, defaultDispatcher } from "../../dispatcher-types";
import SettingsStore from "../../settings/SettingsStore";
import { DefaultTagID, OrderedDefaultTagIDs, TagID } from "./models";
import { Algorithm, ITagMap, ITagSortingMap, ListAlgorithm, SortAlgorithm } from "./algorithms/Algorithm";
import { Algorithm } from "./algorithms/list_ordering/Algorithm";
import TagOrderStore from "../TagOrderStore";
import { getAlgorithmInstance } from "./algorithms";
import { getListAlgorithmInstance } from "./algorithms/list_ordering";
import { AsyncStore } from "../AsyncStore";
import { ITagMap, ITagSortingMap, ListAlgorithm, SortAlgorithm } from "./algorithms/models";
interface IState {
tagsEnabled?: boolean;
@ -172,7 +173,7 @@ class _RoomListStore extends AsyncStore<ActionPayload> {
}
private setAlgorithmClass() {
this.algorithm = getAlgorithmInstance(this.state.preferredAlgorithm);
this.algorithm = getListAlgorithmInstance(this.state.preferredAlgorithm);
}
private async regenerateAllLists() {

View File

@ -19,7 +19,7 @@ import { Room } from "matrix-js-sdk/src/models/room";
import SettingsStore from "../../settings/SettingsStore";
import RoomListStore from "./RoomListStore2";
import OldRoomListStore from "../RoomListStore";
import { ITagMap } from "./algorithms/Algorithm";
import { ITagMap } from "./algorithms/models";
import { UPDATE_EVENT } from "../AsyncStore";
/**

View File

@ -14,38 +14,20 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { DefaultTagID, TagID } from "../models";
import { DefaultTagID, TagID } from "../../models";
import { Room } from "matrix-js-sdk/src/models/room";
import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
import { EffectiveMembership, splitRoomsByMembership } from "../membership";
export enum SortAlgorithm {
Manual = "MANUAL",
Alphabetic = "ALPHABETIC",
Recent = "RECENT",
}
export enum ListAlgorithm {
// Orders Red > Grey > Bold > Idle
Importance = "IMPORTANCE",
// Orders however the SortAlgorithm decides
Natural = "NATURAL",
}
export interface ITagSortingMap {
// @ts-ignore - TypeScript really wants this to be [tagId: string] but we know better.
[tagId: TagID]: SortAlgorithm;
}
export interface ITagMap {
// @ts-ignore - TypeScript really wants this to be [tagId: string] but we know better.
[tagId: TagID]: Room[];
}
import { EffectiveMembership, splitRoomsByMembership } from "../../membership";
import { ITagMap, ITagSortingMap } from "../models";
// TODO: Add locking support to avoid concurrent writes?
// TODO: EventEmitter support? Might not be needed.
/**
* Represents a list ordering algorithm. This class will take care of tag
* management (which rooms go in which tags) and ask the implementation to
* deal with ordering mechanics.
*/
export abstract class Algorithm {
protected cached: ITagMap = {};
protected sortAlgorithms: ITagSortingMap;
@ -160,6 +142,7 @@ export abstract class Algorithm {
* @param {Room[]} rooms The rooms within the tag, unordered.
* @returns {Promise<Room[]>} Resolves to the ordered rooms in the tag.
*/
// TODO: Do we need this?
protected abstract regenerateTag(tagId: TagID, rooms: Room[]): Promise<Room[]>;
/**
@ -173,6 +156,6 @@ export abstract class Algorithm {
* processing.
*/
// TODO: Take a ReasonForChange to better predict the behaviour?
// TODO: Intercept here and handle tag changes automatically
// TODO: Intercept here and handle tag changes automatically? May be best to let the impl do that.
public abstract handleRoomUpdate(room: Room): Promise<boolean>;
}

View File

@ -14,8 +14,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { Algorithm, ITagMap } from "./Algorithm";
import { DefaultTagID } from "../models";
import { Algorithm } from "./Algorithm";
import { DefaultTagID } from "../../models";
import { ITagMap } from "../models";
/**
* A demonstration/temporary algorithm to verify the API surface works.

View File

@ -1,4 +1,5 @@
/*
Copyright 2018, 2019 New Vector Ltd
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
@ -14,11 +15,12 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { Algorithm, ITagMap, ITagSortingMap } from "./Algorithm";
import { Algorithm } from "./Algorithm";
import { Room } from "matrix-js-sdk/src/models/room";
import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
import { DefaultTagID, TagID } from "../models";
import { splitRoomsByMembership } from "../membership";
import { DefaultTagID, TagID } from "../../models";
import { ITagMap, SortAlgorithm } from "../models";
import { getSortingAlgorithmInstance, sortRoomsWithAlgorithm } from "../tag_sorting";
import * as Unread from '../../../../Unread';
/**
* The determined category of a room.
@ -44,6 +46,11 @@ export enum Category {
Idle = "IDLE",
}
interface ICategorizedRoomMap {
// @ts-ignore - TS wants this to be a string, but we know better
[category: Category]: Room[];
}
/**
* An implementation of the "importance" algorithm for room list sorting. Where
* the tag sorting algorithm does not interfere, rooms will be ordered into
@ -119,8 +126,72 @@ export class ImportanceAlgorithm extends Algorithm {
console.log("Constructed an ImportanceAlgorithm");
}
// noinspection JSMethodCanBeStatic
private categorizeRooms(rooms: Room[]): ICategorizedRoomMap {
const map: ICategorizedRoomMap = {
[Category.Red]: [],
[Category.Grey]: [],
[Category.Bold]: [],
[Category.Idle]: [],
};
for (const room of rooms) {
const category = this.getRoomCategory(room);
console.log(`[DEBUG] "${room.name}" (${room.roomId}) is a ${category} room`);
map[category].push(room);
}
return map;
}
// noinspection JSMethodCanBeStatic
private getRoomCategory(room: Room): Category {
// Function implementation borrowed from old RoomListStore
const mentions = room.getUnreadNotificationCount('highlight') > 0;
if (mentions) {
return Category.Red;
}
let unread = room.getUnreadNotificationCount() > 0;
if (unread) {
return Category.Grey;
}
unread = Unread.doesRoomHaveUnreadMessages(room);
if (unread) {
return Category.Bold;
}
return Category.Idle;
}
protected async generateFreshTags(updatedTagMap: ITagMap): Promise<any> {
return Promise.resolve();
for (const tagId of Object.keys(updatedTagMap)) {
const unorderedRooms = updatedTagMap[tagId];
const sortBy = this.sortAlgorithms[tagId];
if (!sortBy) throw new Error(`${tagId} does not have a sorting algorithm`);
if (sortBy === SortAlgorithm.Manual) {
// Manual tags essentially ignore the importance algorithm, so don't do anything
// special about them.
updatedTagMap[tagId] = await sortRoomsWithAlgorithm(unorderedRooms, tagId, sortBy);
} else {
// Every other sorting type affects the categories, not the whole tag.
const categorized = this.categorizeRooms(unorderedRooms);
for (const category of Object.keys(categorized)) {
const roomsToOrder = categorized[category];
categorized[category] = await sortRoomsWithAlgorithm(roomsToOrder, tagId, sortBy);
}
// TODO: Update positions of categories in cache
updatedTagMap[tagId] = [
...categorized[Category.Red],
...categorized[Category.Grey],
...categorized[Category.Bold],
...categorized[Category.Idle],
];
}
}
}
protected async regenerateTag(tagId: string | DefaultTagID, rooms: []): Promise<[]> {

View File

@ -14,12 +14,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { Algorithm, ListAlgorithm } from "./Algorithm";
import { Algorithm } from "./Algorithm";
import { ChaoticAlgorithm } from "./ChaoticAlgorithm";
import { ImportanceAlgorithm } from "./ImportanceAlgorithm";
import { ListAlgorithm } from "../models";
const ALGORITHM_FACTORIES: { [algorithm in ListAlgorithm]: () => Algorithm } = {
[ListAlgorithm.Natural]: () => new ChaoticAlgorithm(ListAlgorithm.Natural),
[ListAlgorithm.Natural]: () => new ChaoticAlgorithm(),
[ListAlgorithm.Importance]: () => new ImportanceAlgorithm(),
};
@ -28,7 +29,7 @@ const ALGORITHM_FACTORIES: { [algorithm in ListAlgorithm]: () => Algorithm } = {
* @param {ListAlgorithm} algorithm The algorithm to get an instance of.
* @returns {Algorithm} The algorithm instance.
*/
export function getAlgorithmInstance(algorithm: ListAlgorithm): Algorithm {
export function getListAlgorithmInstance(algorithm: ListAlgorithm): Algorithm {
if (!ALGORITHM_FACTORIES[algorithm]) {
throw new Error(`${algorithm} is not a known algorithm`);
}

View File

@ -0,0 +1,42 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { TagID } from "../models";
import { Room } from "matrix-js-sdk/src/models/room";
export enum SortAlgorithm {
Manual = "MANUAL",
Alphabetic = "ALPHABETIC",
Recent = "RECENT",
}
export enum ListAlgorithm {
// Orders Red > Grey > Bold > Idle
Importance = "IMPORTANCE",
// Orders however the SortAlgorithm decides
Natural = "NATURAL",
}
export interface ITagSortingMap {
// @ts-ignore - TypeScript really wants this to be [tagId: string] but we know better.
[tagId: TagID]: SortAlgorithm;
}
export interface ITagMap {
// @ts-ignore - TypeScript really wants this to be [tagId: string] but we know better.
[tagId: TagID]: Room[];
}

View File

@ -0,0 +1,29 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { Room } from "matrix-js-sdk/src/models/room";
import { TagID } from "../../models";
import { IAlgorithm } from "./IAlgorithm";
/**
* A demonstration to test the API surface.
* TODO: Remove this before landing
*/
export class ChaoticAlgorithm implements IAlgorithm {
public async sortRooms(rooms: Room[], tagId: TagID): Promise<Room[]> {
return rooms;
}
}

View File

@ -0,0 +1,31 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { Room } from "matrix-js-sdk/src/models/room";
import { TagID } from "../../models";
/**
* Represents a tag sorting algorithm.
*/
export interface IAlgorithm {
/**
* Sorts the given rooms according to the sorting rules of the algorithm.
* @param {Room[]} rooms The rooms to sort.
* @param {TagID} tagId The tag ID in which the rooms are being sorted.
* @returns {Promise<Room[]>} Resolves to the sorted rooms.
*/
sortRooms(rooms: Room[], tagId: TagID): Promise<Room[]>;
}

View File

@ -0,0 +1,31 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { Room } from "matrix-js-sdk/src/models/room";
import { TagID } from "../../models";
import { IAlgorithm } from "./IAlgorithm";
/**
* Sorts rooms according to the tag's `order` property on the room.
*/
export class ManualAlgorithm implements IAlgorithm {
public async sortRooms(rooms: Room[], tagId: TagID): Promise<Room[]> {
const getOrderProp = (r: Room) => r.tags[tagId].order || 0;
return rooms.sort((a, b) => {
return getOrderProp(a) - getOrderProp(b);
});
}
}

View File

@ -0,0 +1,52 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { ChaoticAlgorithm } from "./ChaoticAlgorithm";
import { SortAlgorithm } from "../models";
import { ManualAlgorithm } from "./ManualAlgorithm";
import { IAlgorithm } from "./IAlgorithm";
import { TagID } from "../../models";
import {Room} from "matrix-js-sdk/src/models/room";
const ALGORITHM_INSTANCES: { [algorithm in SortAlgorithm]: IAlgorithm } = {
[SortAlgorithm.Recent]: new ChaoticAlgorithm(),
[SortAlgorithm.Alphabetic]: new ChaoticAlgorithm(),
[SortAlgorithm.Manual]: new ManualAlgorithm(),
};
/**
* Gets an instance of the defined algorithm
* @param {SortAlgorithm} algorithm The algorithm to get an instance of.
* @returns {IAlgorithm} The algorithm instance.
*/
export function getSortingAlgorithmInstance(algorithm: SortAlgorithm): IAlgorithm {
if (!ALGORITHM_INSTANCES[algorithm]) {
throw new Error(`${algorithm} is not a known algorithm`);
}
return ALGORITHM_INSTANCES[algorithm];
}
/**
* Sorts rooms in a given tag according to the algorithm given.
* @param {Room[]} rooms The rooms to sort.
* @param {TagID} tagId The tag in which the sorting is occurring.
* @param {SortAlgorithm} algorithm The algorithm to use for sorting.
* @returns {Promise<Room[]>} Resolves to the sorted rooms.
*/
export function sortRoomsWithAlgorithm(rooms: Room[], tagId: TagID, algorithm: SortAlgorithm): Promise<Room[]> {
return getSortingAlgorithmInstance(algorithm).sortRooms(rooms, tagId);
}