From d244eeb5d585f3101b7450d220cbd91cba014b33 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 29 Apr 2020 16:57:06 -0600 Subject: [PATCH] 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. --- src/stores/room-list/RoomListStore2.ts | 7 +- .../room-list/RoomListStoreTempProxy.ts | 2 +- .../{ => list_ordering}/Algorithm.ts | 37 +++------ .../{ => list_ordering}/ChaoticAlgorithm.ts | 5 +- .../ImportanceAlgorithm.ts | 81 +++++++++++++++++-- .../algorithms/{ => list_ordering}/index.ts | 7 +- src/stores/room-list/algorithms/models.ts | 42 ++++++++++ .../tag_sorting/ChaoticAlgorithm.ts | 29 +++++++ .../algorithms/tag_sorting/IAlgorithm.ts | 31 +++++++ .../algorithms/tag_sorting/ManualAlgorithm.ts | 31 +++++++ .../room-list/algorithms/tag_sorting/index.ts | 52 ++++++++++++ 11 files changed, 283 insertions(+), 41 deletions(-) rename src/stores/room-list/algorithms/{ => list_ordering}/Algorithm.ts (90%) rename src/stores/room-list/algorithms/{ => list_ordering}/ChaoticAlgorithm.ts (90%) rename src/stores/room-list/algorithms/{ => list_ordering}/ImportanceAlgorithm.ts (63%) rename src/stores/room-list/algorithms/{ => list_ordering}/index.ts (84%) create mode 100644 src/stores/room-list/algorithms/models.ts create mode 100644 src/stores/room-list/algorithms/tag_sorting/ChaoticAlgorithm.ts create mode 100644 src/stores/room-list/algorithms/tag_sorting/IAlgorithm.ts create mode 100644 src/stores/room-list/algorithms/tag_sorting/ManualAlgorithm.ts create mode 100644 src/stores/room-list/algorithms/tag_sorting/index.ts diff --git a/src/stores/room-list/RoomListStore2.ts b/src/stores/room-list/RoomListStore2.ts index dc1cb49cd6..0b3f61e261 100644 --- a/src/stores/room-list/RoomListStore2.ts +++ b/src/stores/room-list/RoomListStore2.ts @@ -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 { } private setAlgorithmClass() { - this.algorithm = getAlgorithmInstance(this.state.preferredAlgorithm); + this.algorithm = getListAlgorithmInstance(this.state.preferredAlgorithm); } private async regenerateAllLists() { diff --git a/src/stores/room-list/RoomListStoreTempProxy.ts b/src/stores/room-list/RoomListStoreTempProxy.ts index 8ad3c5d35e..4edca2b9cd 100644 --- a/src/stores/room-list/RoomListStoreTempProxy.ts +++ b/src/stores/room-list/RoomListStoreTempProxy.ts @@ -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"; /** diff --git a/src/stores/room-list/algorithms/Algorithm.ts b/src/stores/room-list/algorithms/list_ordering/Algorithm.ts similarity index 90% rename from src/stores/room-list/algorithms/Algorithm.ts rename to src/stores/room-list/algorithms/list_ordering/Algorithm.ts index 15fc208b21..4c8c9e9c60 100644 --- a/src/stores/room-list/algorithms/Algorithm.ts +++ b/src/stores/room-list/algorithms/list_ordering/Algorithm.ts @@ -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} Resolves to the ordered rooms in the tag. */ + // TODO: Do we need this? protected abstract regenerateTag(tagId: TagID, rooms: Room[]): Promise; /** @@ -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; } diff --git a/src/stores/room-list/algorithms/ChaoticAlgorithm.ts b/src/stores/room-list/algorithms/list_ordering/ChaoticAlgorithm.ts similarity index 90% rename from src/stores/room-list/algorithms/ChaoticAlgorithm.ts rename to src/stores/room-list/algorithms/list_ordering/ChaoticAlgorithm.ts index 5d4177db8b..7c1a0b1acc 100644 --- a/src/stores/room-list/algorithms/ChaoticAlgorithm.ts +++ b/src/stores/room-list/algorithms/list_ordering/ChaoticAlgorithm.ts @@ -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. diff --git a/src/stores/room-list/algorithms/ImportanceAlgorithm.ts b/src/stores/room-list/algorithms/list_ordering/ImportanceAlgorithm.ts similarity index 63% rename from src/stores/room-list/algorithms/ImportanceAlgorithm.ts rename to src/stores/room-list/algorithms/list_ordering/ImportanceAlgorithm.ts index 1a7a73a9d5..d73fdee930 100644 --- a/src/stores/room-list/algorithms/ImportanceAlgorithm.ts +++ b/src/stores/room-list/algorithms/list_ordering/ImportanceAlgorithm.ts @@ -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 { - 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<[]> { diff --git a/src/stores/room-list/algorithms/index.ts b/src/stores/room-list/algorithms/list_ordering/index.ts similarity index 84% rename from src/stores/room-list/algorithms/index.ts rename to src/stores/room-list/algorithms/list_ordering/index.ts index 1277b66ac9..35f4af14cf 100644 --- a/src/stores/room-list/algorithms/index.ts +++ b/src/stores/room-list/algorithms/list_ordering/index.ts @@ -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`); } diff --git a/src/stores/room-list/algorithms/models.ts b/src/stores/room-list/algorithms/models.ts new file mode 100644 index 0000000000..284600a776 --- /dev/null +++ b/src/stores/room-list/algorithms/models.ts @@ -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[]; +} diff --git a/src/stores/room-list/algorithms/tag_sorting/ChaoticAlgorithm.ts b/src/stores/room-list/algorithms/tag_sorting/ChaoticAlgorithm.ts new file mode 100644 index 0000000000..31846d084a --- /dev/null +++ b/src/stores/room-list/algorithms/tag_sorting/ChaoticAlgorithm.ts @@ -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 { + return rooms; + } +} diff --git a/src/stores/room-list/algorithms/tag_sorting/IAlgorithm.ts b/src/stores/room-list/algorithms/tag_sorting/IAlgorithm.ts new file mode 100644 index 0000000000..6c22ee0c9c --- /dev/null +++ b/src/stores/room-list/algorithms/tag_sorting/IAlgorithm.ts @@ -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} Resolves to the sorted rooms. + */ + sortRooms(rooms: Room[], tagId: TagID): Promise; +} diff --git a/src/stores/room-list/algorithms/tag_sorting/ManualAlgorithm.ts b/src/stores/room-list/algorithms/tag_sorting/ManualAlgorithm.ts new file mode 100644 index 0000000000..b8c0357633 --- /dev/null +++ b/src/stores/room-list/algorithms/tag_sorting/ManualAlgorithm.ts @@ -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 { + const getOrderProp = (r: Room) => r.tags[tagId].order || 0; + return rooms.sort((a, b) => { + return getOrderProp(a) - getOrderProp(b); + }); + } +} diff --git a/src/stores/room-list/algorithms/tag_sorting/index.ts b/src/stores/room-list/algorithms/tag_sorting/index.ts new file mode 100644 index 0000000000..07f8f484d8 --- /dev/null +++ b/src/stores/room-list/algorithms/tag_sorting/index.ts @@ -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} Resolves to the sorted rooms. + */ +export function sortRoomsWithAlgorithm(rooms: Room[], tagId: TagID, algorithm: SortAlgorithm): Promise { + return getSortingAlgorithmInstance(algorithm).sortRooms(rooms, tagId); +}