From 966e2cad7ee1587f3d1118b2645c73ccb674ea6c Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 11 Jun 2020 16:52:47 -0600 Subject: [PATCH 1/7] Update documentation for how this refactoring will go --- src/stores/room-list/README.md | 43 +++++++++++++++--------- src/stores/room-list/RoomListStore2.png | Bin 0 -> 76264 bytes 2 files changed, 27 insertions(+), 16 deletions(-) create mode 100644 src/stores/room-list/RoomListStore2.png diff --git a/src/stores/room-list/README.md b/src/stores/room-list/README.md index ba34691d34..4608570ae9 100644 --- a/src/stores/room-list/README.md +++ b/src/stores/room-list/README.md @@ -2,20 +2,31 @@ It's so complicated it needs its own README. +![](./RoomListStore2.png) + +Legend: +* Orange = External event. +* Purple = Deterministic flow. +* Green = Algorithm definition. +* Red = Exit condition/point. +* Blue = Process definition. + ## Algorithms involved There's two main kinds of algorithms involved in the room list store: list ordering and tag sorting. Throughout the code an intentional decision has been made to call them the List Algorithm and Sorting -Algorithm respectively. The list algorithm determines the behaviour of the room list whereas the sorting -algorithm determines how rooms get ordered within tags affected by the list algorithm. +Algorithm respectively. The list algorithm determines the primary ordering of a given tag whereas the +tag sorting defines how rooms within that tag get sorted, at the discretion of the list ordering. + +Behaviour of the overall room list (sticky rooms, etc) are determined by the generically-named Algorithm +class. Here is where much of the coordination from the room list store is done to figure out which list +algorithm to call, instead of having all the logic in the room list store itself. -Behaviour of the room list takes the shape of determining what features the room list supports, as well -as determining where and when to apply the sorting algorithm in a tag. The importance algorithm, which -is described later in this doc, is an example of an algorithm which makes heavy behavioural changes -to the room list. Tag sorting is effectively the comparator supplied to the list algorithm. This gives the list algorithm -the power to decide when and how to apply the tag sorting, if at all. +the power to decide when and how to apply the tag sorting, if at all. For example, The importance algorithm, +later described in this document, heavily uses the list ordering behaviour to break the tag into categories. +Each category then gets sorted by the appropriate tag sorting algorithm. ### Tag sorting algorithm: Alphabetical @@ -70,11 +81,11 @@ Conveniently, each tag gets ordered by those categories as presented: red rooms above bold, etc. Once the algorithm has determined which rooms belong in which categories, the tag sorting algorithm -gets applied to each category in a sub-sub-list fashion. This should result in the red rooms (for example) +gets applied to each category in a sub-list fashion. This should result in the red rooms (for example) being sorted alphabetically amongst each other as well as the grey rooms sorted amongst each other, but collectively the tag will be sorted into categories with red being at the top. -### Sticky rooms +## Sticky rooms When the user visits a room, that room becomes 'sticky' in the list, regardless of ordering algorithm. From a code perspective, the underlying algorithm is not aware of a sticky room and instead the base class @@ -128,13 +139,13 @@ maintain the caching behaviour described above. ## Class breakdowns -The `RoomListStore` is the major coordinator of various `Algorithm` implementations, which take care -of the various `ListAlgorithm` and `SortingAlgorithm` options. The `Algorithm` superclass is also -responsible for figuring out which tags get which rooms, as Matrix specifies them as a reverse map: -tags get defined on rooms and are not defined as a collection of rooms (unlike how they are presented -to the user). Various list-specific utilities are also included, though they are expected to move -somewhere more general when needed. For example, the `membership` utilities could easily be moved -elsewhere as needed. +The `RoomListStore` is the major coordinator of various algorithm implementations, which take care +of the various `ListAlgorithm` and `SortingAlgorithm` options. The `Algorithm` class is responsible +for figuring out which tags get which rooms, as Matrix specifies them as a reverse map: tags get +defined on rooms and are not defined as a collection of rooms (unlike how they are presented to the +user). Various list-specific utilities are also included, though they are expected to move somewhere +more general when needed. For example, the `membership` utilities could easily be moved elsewhere +as needed. The various bits throughout the room list store should also have jsdoc of some kind to help describe what they do and how they work. diff --git a/src/stores/room-list/RoomListStore2.png b/src/stores/room-list/RoomListStore2.png new file mode 100644 index 0000000000000000000000000000000000000000..9952d1c910f15eb2e8aebbe02b627204dc12d126 GIT binary patch literal 76264 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 xZ7}XLVzHNR{}&h2#jyYY literal 0 HcmV?d00001 From 52b26deb2e281ac57f22d7054233b3c28c8688b8 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 11 Jun 2020 16:56:12 -0600 Subject: [PATCH 2/7] Move room list store docs to docs directory This pulls it away from the code, but the code is sufficiently involved enough where it doesn't need another file near it. --- .../room-list => docs/img}/RoomListStore2.png | Bin .../room-list/README.md => docs/room-list-store.md | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename {src/stores/room-list => docs/img}/RoomListStore2.png (100%) rename src/stores/room-list/README.md => docs/room-list-store.md (99%) diff --git a/src/stores/room-list/RoomListStore2.png b/docs/img/RoomListStore2.png similarity index 100% rename from src/stores/room-list/RoomListStore2.png rename to docs/img/RoomListStore2.png diff --git a/src/stores/room-list/README.md b/docs/room-list-store.md similarity index 99% rename from src/stores/room-list/README.md rename to docs/room-list-store.md index 4608570ae9..82c83d3593 100644 --- a/src/stores/room-list/README.md +++ b/docs/room-list-store.md @@ -2,7 +2,7 @@ It's so complicated it needs its own README. -![](./RoomListStore2.png) +![](img/RoomListStore2.png) Legend: * Orange = External event. From fd029e8e8021cdf475d95ed13f65058900695d82 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 11 Jun 2020 21:24:25 -0600 Subject: [PATCH 3/7] Dumb down list algorithms in favour of smarter tags This commit is a bit involved, as it factors the tag specific handling out of `/list-ordering` (and moves the `Algorithm` class one higher as a result), leaving it in the `Algorithm`. The algorithms for list ordering now only know how to handle a single tag, and this is managed by the `Algorithm` class - which is also no longer the base class for the list ordering. The list ordering algorithms now inherit from a generic `OrderingAlgorithm` base class which handles some rudimentary things. Overall the logic hasn't changed much: the tag-specific stuff has been moved into the `Algorithm`, and the list ordering algorithms essentially just removed the iteration on tags. The `RoomListStore2` still shovels a bunch of information over to the `Algorithm`, which can lead to an awkward code flow however this commit is meant to keep the number of surfaces touched to a minimum. The RoomListStore has also gained the ability to set per-list (tag) ordering and sorting, which is required for the new room list. The assumption that it defaults from the account-level settings is not reviewed by design, yet. This decision is deferred. --- src/stores/room-list/RoomListStore2.ts | 98 +++++--- .../{list-ordering => }/Algorithm.ts | 153 ++++++++++--- .../list-ordering/ImportanceAlgorithm.ts | 210 +++++++----------- .../list-ordering/NaturalAlgorithm.ts | 43 ++-- .../list-ordering/OrderingAlgorithm.ts | 72 ++++++ .../algorithms/list-ordering/index.ts | 21 +- src/stores/room-list/algorithms/models.ts | 11 + 7 files changed, 374 insertions(+), 234 deletions(-) rename src/stores/room-list/algorithms/{list-ordering => }/Algorithm.ts (76%) create mode 100644 src/stores/room-list/algorithms/list-ordering/OrderingAlgorithm.ts diff --git a/src/stores/room-list/RoomListStore2.ts b/src/stores/room-list/RoomListStore2.ts index 63a06dcf81..1c4e66c4b0 100644 --- a/src/stores/room-list/RoomListStore2.ts +++ b/src/stores/room-list/RoomListStore2.ts @@ -17,25 +17,21 @@ limitations under the License. import { MatrixClient } from "matrix-js-sdk/src/client"; import SettingsStore from "../../settings/SettingsStore"; -import { DefaultTagID, OrderedDefaultTagIDs, RoomUpdateCause, TagID } from "./models"; -import { Algorithm, LIST_UPDATED_EVENT } from "./algorithms/list-ordering/Algorithm"; +import { OrderedDefaultTagIDs, RoomUpdateCause, TagID } from "./models"; import TagOrderStore from "../TagOrderStore"; import { AsyncStore } from "../AsyncStore"; import { Room } from "matrix-js-sdk/src/models/room"; -import { ITagMap, ITagSortingMap, ListAlgorithm, SortAlgorithm } from "./algorithms/models"; -import { getListAlgorithmInstance } from "./algorithms/list-ordering"; +import { IListOrderingMap, ITagMap, ITagSortingMap, ListAlgorithm, SortAlgorithm } from "./algorithms/models"; import { ActionPayload } from "../../dispatcher/payloads"; import defaultDispatcher from "../../dispatcher/dispatcher"; import { readReceiptChangeIsFor } from "../../utils/read-receipts"; import { IFilterCondition } from "./filters/IFilterCondition"; import { TagWatcher } from "./TagWatcher"; import RoomViewStore from "../RoomViewStore"; +import { Algorithm, LIST_UPDATED_EVENT } from "./algorithms/Algorithm"; interface IState { tagsEnabled?: boolean; - - preferredSort?: SortAlgorithm; - preferredAlgorithm?: ListAlgorithm; } /** @@ -48,7 +44,7 @@ export class RoomListStore2 extends AsyncStore { private _matrixClient: MatrixClient; private initialListsGenerated = false; private enabled = false; - private algorithm: Algorithm; + private algorithm = new Algorithm(); private filterConditions: IFilterCondition[] = []; private tagWatcher = new TagWatcher(this); @@ -64,6 +60,7 @@ export class RoomListStore2 extends AsyncStore { this.checkEnabled(); for (const settingName of this.watchedSettings) SettingsStore.monitorSetting(settingName, null); RoomViewStore.addListener(this.onRVSUpdate); + this.algorithm.on(LIST_UPDATED_EVENT, this.onAlgorithmListUpdated); } public get orderedLists(): ITagMap { @@ -85,14 +82,10 @@ export class RoomListStore2 extends AsyncStore { private async readAndCacheSettingsFromStore() { const tagsEnabled = SettingsStore.isFeatureEnabled("feature_custom_tags"); - const orderByImportance = SettingsStore.getValue("RoomList.orderByImportance"); - const orderAlphabetically = SettingsStore.getValue("RoomList.orderAlphabetically"); await this.updateState({ tagsEnabled, - preferredSort: orderAlphabetically ? SortAlgorithm.Alphabetic : SortAlgorithm.Recent, - preferredAlgorithm: orderByImportance ? ListAlgorithm.Importance : ListAlgorithm.Natural, }); - this.setAlgorithmClass(); + await this.updateAlgorithmInstances(); } private onRVSUpdate = () => { @@ -259,17 +252,57 @@ export class RoomListStore2 extends AsyncStore { } } - private getSortAlgorithmFor(tagId: TagID): SortAlgorithm { - switch (tagId) { - case DefaultTagID.Invite: - case DefaultTagID.Untagged: - case DefaultTagID.Archived: - case DefaultTagID.LowPriority: - case DefaultTagID.DM: - return this.state.preferredSort; - case DefaultTagID.Favourite: - default: - return SortAlgorithm.Manual; + public async setTagSorting(tagId: TagID, sort: SortAlgorithm) { + await this.algorithm.setTagSorting(tagId, sort); + localStorage.setItem(`mx_tagSort_${tagId}`, sort); + } + + public getTagSorting(tagId: TagID): SortAlgorithm { + return this.algorithm.getTagSorting(tagId); + } + + // noinspection JSMethodCanBeStatic + private getStoredTagSorting(tagId: TagID): SortAlgorithm { + return localStorage.getItem(`mx_tagSort_${tagId}`); + } + + public async setListOrder(tagId: TagID, order: ListAlgorithm) { + await this.algorithm.setListOrdering(tagId, order); + localStorage.setItem(`mx_listOrder_${tagId}`, order); + } + + public getListOrder(tagId: TagID): ListAlgorithm { + return this.algorithm.getListOrdering(tagId); + } + + // noinspection JSMethodCanBeStatic + private getStoredListOrder(tagId: TagID): ListAlgorithm { + return localStorage.getItem(`mx_listOrder_${tagId}`); + } + + private async updateAlgorithmInstances() { + const orderByImportance = SettingsStore.getValue("RoomList.orderByImportance"); + const orderAlphabetically = SettingsStore.getValue("RoomList.orderAlphabetically"); + + const defaultSort = orderAlphabetically ? SortAlgorithm.Alphabetic : SortAlgorithm.Recent; + const defaultOrder = orderByImportance ? ListAlgorithm.Importance : ListAlgorithm.Natural; + + for (const tag of Object.keys(this.orderedLists)) { + const definedSort = this.getTagSorting(tag); + const definedOrder = this.getListOrder(tag); + + const storedSort = this.getStoredTagSorting(tag); + const storedOrder = this.getStoredListOrder(tag); + + const tagSort = storedSort ? storedSort : (definedSort ? definedSort : defaultSort); + const listOrder = storedOrder ? storedOrder : (definedOrder ? definedOrder : defaultOrder); + + if (tagSort !== definedSort) { + await this.setTagSorting(tag, tagSort); + } + if (listOrder !== definedOrder) { + await this.setListOrder(tag, listOrder); + } } } @@ -279,15 +312,6 @@ export class RoomListStore2 extends AsyncStore { await super.updateState(newState); } - private setAlgorithmClass() { - if (this.algorithm) { - this.algorithm.off(LIST_UPDATED_EVENT, this.onAlgorithmListUpdated); - } - this.algorithm = getListAlgorithmInstance(this.state.preferredAlgorithm); - this.algorithm.setFilterConditions(this.filterConditions); - this.algorithm.on(LIST_UPDATED_EVENT, this.onAlgorithmListUpdated); - } - private onAlgorithmListUpdated = () => { console.log("Underlying algorithm has triggered a list update - refiring"); this.emit(LISTS_UPDATE_EVENT, this); @@ -296,9 +320,11 @@ export class RoomListStore2 extends AsyncStore { private async regenerateAllLists() { console.warn("Regenerating all room lists"); - const tags: ITagSortingMap = {}; + const sorts: ITagSortingMap = {}; + const orders: IListOrderingMap = {}; for (const tagId of OrderedDefaultTagIDs) { - tags[tagId] = this.getSortAlgorithmFor(tagId); + sorts[tagId] = this.getStoredTagSorting(tagId) || SortAlgorithm.Alphabetic; + orders[tagId] = this.getStoredListOrder(tagId) || ListAlgorithm.Natural; } if (this.state.tagsEnabled) { @@ -307,7 +333,7 @@ export class RoomListStore2 extends AsyncStore { console.log("rtags", roomTags); } - await this.algorithm.populateTags(tags); + await this.algorithm.populateTags(sorts, orders); await this.algorithm.setKnownRooms(this.matrixClient.getRooms()); this.initialListsGenerated = true; diff --git a/src/stores/room-list/algorithms/list-ordering/Algorithm.ts b/src/stores/room-list/algorithms/Algorithm.ts similarity index 76% rename from src/stores/room-list/algorithms/list-ordering/Algorithm.ts rename to src/stores/room-list/algorithms/Algorithm.ts index a8512c0bd6..438ef78e7e 100644 --- a/src/stores/room-list/algorithms/list-ordering/Algorithm.ts +++ b/src/stores/room-list/algorithms/Algorithm.ts @@ -14,17 +14,25 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { DefaultTagID, RoomUpdateCause, 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"; -import { ITagMap, ITagSortingMap } from "../models"; -import DMRoomMap from "../../../../utils/DMRoomMap"; -import { FILTER_CHANGED, FilterPriority, IFilterCondition } from "../../filters/IFilterCondition"; +import DMRoomMap from "../../../utils/DMRoomMap"; import { EventEmitter } from "events"; -import { UPDATE_EVENT } from "../../../AsyncStore"; -import { ArrayUtil } from "../../../../utils/arrays"; -import { getEnumValues } from "../../../../utils/enums"; +import { arrayHasDiff, ArrayUtil } from "../../../utils/arrays"; +import { getEnumValues } from "../../../utils/enums"; +import { DefaultTagID, RoomUpdateCause, TagID } from "../models"; +import { + IListOrderingMap, + IOrderingAlgorithmMap, + ITagMap, + ITagSortingMap, + ListAlgorithm, + SortAlgorithm +} from "./models"; +import { FILTER_CHANGED, FilterPriority, IFilterCondition } from "../filters/IFilterCondition"; +import { EffectiveMembership, splitRoomsByMembership } from "../membership"; +import { OrderingAlgorithm } from "./list-ordering/OrderingAlgorithm"; +import { getListAlgorithmInstance } from "./list-ordering"; // TODO: Add locking support to avoid concurrent writes? @@ -44,21 +52,22 @@ interface IStickyRoom { * management (which rooms go in which tags) and ask the implementation to * deal with ordering mechanics. */ -export abstract class Algorithm extends EventEmitter { +export class Algorithm extends EventEmitter { private _cachedRooms: ITagMap = {}; private _cachedStickyRooms: ITagMap = {}; // a clone of the _cachedRooms, with the sticky room private filteredRooms: ITagMap = {}; private _stickyRoom: IStickyRoom = null; - - protected sortAlgorithms: ITagSortingMap; - protected rooms: Room[] = []; - protected roomIdsToTags: { + private sortAlgorithms: ITagSortingMap; + private listAlgorithms: IListOrderingMap; + private algorithms: IOrderingAlgorithmMap; + private rooms: Room[] = []; + private roomIdsToTags: { [roomId: string]: TagID[]; } = {}; - protected allowedByFilter: Map = new Map(); - protected allowedRoomsByFilters: Set = new Set(); + private allowedByFilter: Map = new Map(); + private allowedRoomsByFilters: Set = new Set(); - protected constructor() { + public constructor() { super(); } @@ -68,6 +77,7 @@ export abstract class Algorithm extends EventEmitter { public set stickyRoom(val: Room) { // setters can't be async, so we call a private function to do the work + // noinspection JSIgnoredPromiseFromCall this.updateStickyRoom(val); } @@ -89,14 +99,38 @@ export abstract class Algorithm extends EventEmitter { return this._cachedRooms; } - /** - * Sets the filter conditions the Algorithm should use. - * @param filterConditions The filter conditions to use. - */ - public setFilterConditions(filterConditions: IFilterCondition[]): void { - for (const filter of filterConditions) { - this.addFilterCondition(filter); - } + public getTagSorting(tagId: TagID): SortAlgorithm { + return this.sortAlgorithms[tagId]; + } + + public async setTagSorting(tagId: TagID, sort: SortAlgorithm) { + if (!tagId) throw new Error("Tag ID must be defined"); + if (!sort) throw new Error("Algorithm must be defined"); + this.sortAlgorithms[tagId] = sort; + + const algorithm: OrderingAlgorithm = this.algorithms[tagId]; + await algorithm.setSortAlgorithm(sort); + this._cachedRooms[tagId] = algorithm.orderedRooms; + this.recalculateFilteredRoomsForTag(tagId); // update filter to re-sort the list + this.recalculateStickyRoom(tagId); // update sticky room to make sure it appears if needed + } + + public getListOrdering(tagId: TagID): ListAlgorithm { + return this.listAlgorithms[tagId]; + } + + public async setListOrdering(tagId: TagID, order: ListAlgorithm) { + if (!tagId) throw new Error("Tag ID must be defined"); + if (!order) throw new Error("Algorithm must be defined"); + this.listAlgorithms[tagId] = order; + + const algorithm = getListAlgorithmInstance(order, tagId, this.sortAlgorithms[tagId]); + this.algorithms[tagId] = algorithm; + + await algorithm.setRooms(this._cachedRooms[tagId]) + this._cachedRooms[tagId] = algorithm.orderedRooms; + this.recalculateFilteredRoomsForTag(tagId); // update filter to re-sort the list + this.recalculateStickyRoom(tagId); // update sticky room to make sure it appears if needed } public addFilterCondition(filterCondition: IFilterCondition): void { @@ -310,11 +344,21 @@ export abstract class Algorithm extends EventEmitter { * as reference for which lists to generate and which way to generate * them. * @param {ITagSortingMap} tagSortingMap The tags to generate. + * @param {IListOrderingMap} listOrderingMap The ordering of those tags. * @returns {Promise<*>} A promise which resolves when complete. */ - public async populateTags(tagSortingMap: ITagSortingMap): Promise { - if (!tagSortingMap) throw new Error(`Map cannot be null or empty`); + public async populateTags(tagSortingMap: ITagSortingMap, listOrderingMap: IListOrderingMap): Promise { + if (!tagSortingMap) throw new Error(`Sorting map cannot be null or empty`); + if (!listOrderingMap) throw new Error(`Ordering ma cannot be null or empty`); + if (arrayHasDiff(Object.keys(tagSortingMap), Object.keys(listOrderingMap))) { + throw new Error(`Both maps must contain the exact same tags`); + } this.sortAlgorithms = tagSortingMap; + this.listAlgorithms = listOrderingMap; + this.algorithms = {}; + for (const tag of Object.keys(tagSortingMap)) { + this.algorithms[tag] = getListAlgorithmInstance(this.listAlgorithms[tag], tag, this.sortAlgorithms[tag]); + } return this.setKnownRooms(this.rooms); } @@ -428,7 +472,17 @@ export abstract class Algorithm extends EventEmitter { * be mutated in place. * @returns {Promise<*>} A promise which resolves when complete. */ - protected abstract generateFreshTags(updatedTagMap: ITagMap): Promise; + private async generateFreshTags(updatedTagMap: ITagMap): Promise { + if (!this.algorithms) throw new Error("Not ready: no algorithms to determine tags from"); + + for (const tag of Object.keys(updatedTagMap)) { + const algorithm: OrderingAlgorithm = this.algorithms[tag]; + if (!algorithm) throw new Error(`No algorithm for ${tag}`); + + await algorithm.setRooms(updatedTagMap[tag]); + updatedTagMap[tag] = algorithm.orderedRooms; + } + } /** * Asks the Algorithm to update its knowledge of a room. For example, when @@ -441,5 +495,48 @@ export abstract class Algorithm extends EventEmitter { * depending on whether or not getOrderedRooms() should be called after * processing. */ - public abstract handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise; + public async handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise { + if (!this.algorithms) throw new Error("Not ready: no algorithms to determine tags from"); + + if (cause === RoomUpdateCause.PossibleTagChange) { + // TODO: Be smarter and splice rather than regen the planet. + // TODO: No-op if no change. + await this.setKnownRooms(this.rooms); + return true; + } + + if (cause === RoomUpdateCause.NewRoom) { + // TODO: Be smarter and insert rather than regen the planet. + await this.setKnownRooms([room, ...this.rooms]); + return; + } + + if (cause === RoomUpdateCause.RoomRemoved) { + // TODO: Be smarter and splice rather than regen the planet. + await this.setKnownRooms(this.rooms.filter(r => r !== room)); + return true; + } + + let tags = this.roomIdsToTags[room.roomId]; + if (!tags) { + console.warn(`No tags known for "${room.name}" (${room.roomId})`); + return false; + } + + let changed = false; + for (const tag of tags) { + const algorithm: OrderingAlgorithm = this.algorithms[tag]; + if (!algorithm) throw new Error(`No algorithm for ${tag}`); + + await algorithm.handleRoomUpdate(room, cause); + this.cachedRooms[tag] = algorithm.orderedRooms; + + // Flag that we've done something + this.recalculateFilteredRoomsForTag(tag); // update filter to re-sort the list + this.recalculateStickyRoom(tag); // update sticky room to make sure it appears if needed + changed = true; + } + + return true; + }; } diff --git a/src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm.ts b/src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm.ts index 7cbcc504c2..2e24cb23f1 100644 --- a/src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm.ts +++ b/src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm.ts @@ -15,12 +15,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { Algorithm } from "./Algorithm"; import { Room } from "matrix-js-sdk/src/models/room"; import { RoomUpdateCause, TagID } from "../../models"; -import { ITagMap, SortAlgorithm } from "../models"; +import { SortAlgorithm } from "../models"; import { sortRoomsWithAlgorithm } from "../tag-sorting"; import * as Unread from '../../../../Unread'; +import { OrderingAlgorithm } from "./OrderingAlgorithm"; /** * The determined category of a room. @@ -77,32 +77,16 @@ const CATEGORY_ORDER = [Category.Red, Category.Grey, Category.Bold, Category.Idl * within the same category. For more information, see the comments contained * within the class. */ -export class ImportanceAlgorithm extends Algorithm { +export class ImportanceAlgorithm extends OrderingAlgorithm { + // This tracks the category for the tag it represents by tracking the index of + // each category within the list, where zero is the top of the list. This then + // tracks when rooms change categories and splices the orderedRooms array as + // needed, preventing many ordering operations. - // HOW THIS WORKS - // -------------- - // - // This block of comments assumes you've read the README two levels higher. - // You should do that if you haven't already. - // - // Tags are fed into the algorithmic functions from the Algorithm superclass, - // which cause subsequent updates to the room list itself. Categories within - // those tags are tracked as index numbers within the array (zero = top), with - // each sticky room being tracked separately. Internally, the category index - // can be found from `this.indices[tag][category]`. - // - // The room list store is always provided with the `this.cachedRooms` results, which are - // updated as needed and not recalculated often. For example, when a room needs to - // move within a tag, the array in `this.cachedRooms` will be spliced instead of iterated. - // The `indices` help track the positions of each category to make splicing easier. + private indices: ICategoryIndex = {}; - private indices: { - // @ts-ignore - TS wants this to be a string but we know better than it - [tag: TagID]: ICategoryIndex; - } = {}; - - constructor() { - super(); + public constructor(tagId: TagID, initialSortingAlgorithm: SortAlgorithm) { + super(tagId, initialSortingAlgorithm); console.log("Constructed an ImportanceAlgorithm"); } @@ -143,122 +127,81 @@ export class ImportanceAlgorithm extends Algorithm { return Category.Idle; } - protected async generateFreshTags(updatedTagMap: ITagMap): Promise { - 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); - } - - const newlyOrganized: Room[] = []; - const newIndices: ICategoryIndex = {}; - - for (const category of CATEGORY_ORDER) { - newIndices[category] = newlyOrganized.length; - newlyOrganized.push(...categorized[category]); - } - - this.indices[tagId] = newIndices; - updatedTagMap[tagId] = newlyOrganized; + public async setRooms(rooms: Room[]): Promise { + if (this.sortingAlgorithm === SortAlgorithm.Manual) { + this.cachedOrderedRooms = await sortRoomsWithAlgorithm(rooms, this.tagId, this.sortingAlgorithm); + } else { + // Every other sorting type affects the categories, not the whole tag. + const categorized = this.categorizeRooms(rooms); + for (const category of Object.keys(categorized)) { + const roomsToOrder = categorized[category]; + categorized[category] = await sortRoomsWithAlgorithm(roomsToOrder, this.tagId, this.sortingAlgorithm); } + + const newlyOrganized: Room[] = []; + const newIndices: ICategoryIndex = {}; + + for (const category of CATEGORY_ORDER) { + newIndices[category] = newlyOrganized.length; + newlyOrganized.push(...categorized[category]); + } + + this.indices = newIndices; + this.cachedOrderedRooms = newlyOrganized; } } public async handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise { - if (cause === RoomUpdateCause.PossibleTagChange) { - // TODO: Be smarter and splice rather than regen the planet. - // TODO: No-op if no change. - await this.setKnownRooms(this.rooms); - return; - } - - if (cause === RoomUpdateCause.NewRoom) { - // TODO: Be smarter and insert rather than regen the planet. - await this.setKnownRooms([room, ...this.rooms]); - return; - } - - if (cause === RoomUpdateCause.RoomRemoved) { - // TODO: Be smarter and splice rather than regen the planet. - await this.setKnownRooms(this.rooms.filter(r => r !== room)); - return; - } - - let tags = this.roomIdsToTags[room.roomId]; - if (!tags) { - console.warn(`No tags known for "${room.name}" (${room.roomId})`); - return false; - } const category = this.getRoomCategory(room); - let changed = false; - for (const tag of tags) { - if (this.sortAlgorithms[tag] === SortAlgorithm.Manual) { - continue; // Nothing to do here. - } - - const taggedRooms = this.cachedRooms[tag]; - const indices = this.indices[tag]; - let roomIdx = taggedRooms.indexOf(room); - if (roomIdx === -1) { - console.warn(`Degrading performance to find missing room in "${tag}": ${room.roomId}`); - roomIdx = taggedRooms.findIndex(r => r.roomId === room.roomId); - } - if (roomIdx === -1) { - throw new Error(`Room ${room.roomId} has no index in ${tag}`); - } - - // Try to avoid doing array operations if we don't have to: only move rooms within - // the categories if we're jumping categories - const oldCategory = this.getCategoryFromIndices(roomIdx, indices); - if (oldCategory !== category) { - // Move the room and update the indices - this.moveRoomIndexes(1, oldCategory, category, indices); - taggedRooms.splice(roomIdx, 1); // splice out the old index (fixed position) - taggedRooms.splice(indices[category], 0, room); // splice in the new room (pre-adjusted) - // Note: if moveRoomIndexes() is called after the splice then the insert operation - // will happen in the wrong place. Because we would have already adjusted the index - // for the category, we don't need to determine how the room is moving in the list. - // If we instead tried to insert before updating the indices, we'd have to determine - // whether the room was moving later (towards IDLE) or earlier (towards RED) from its - // current position, as it'll affect the category's start index after we remove the - // room from the array. - } - - // The room received an update, so take out the slice and sort it. This should be relatively - // quick because the room is inserted at the top of the category, and most popular sorting - // algorithms will deal with trying to keep the active room at the top/start of the category. - // For the few algorithms that will have to move the thing quite far (alphabetic with a Z room - // for example), the list should already be sorted well enough that it can rip through the - // array and slot the changed room in quickly. - const nextCategoryStartIdx = category === CATEGORY_ORDER[CATEGORY_ORDER.length - 1] - ? Number.MAX_SAFE_INTEGER - : indices[CATEGORY_ORDER[CATEGORY_ORDER.indexOf(category) + 1]]; - const startIdx = indices[category]; - const numSort = nextCategoryStartIdx - startIdx; // splice() returns up to the max, so MAX_SAFE_INT is fine - const unsortedSlice = taggedRooms.splice(startIdx, numSort); - const sorted = await sortRoomsWithAlgorithm(unsortedSlice, tag, this.sortAlgorithms[tag]); - taggedRooms.splice(startIdx, 0, ...sorted); - - // Finally, flag that we've done something - this.recalculateFilteredRoomsForTag(tag); // update filter to re-sort the list - this.recalculateStickyRoom(tag); // update sticky room to make sure it appears if needed - changed = true; + if (this.sortingAlgorithm === SortAlgorithm.Manual) { + return; // Nothing to do here. } - return changed; + + let roomIdx = this.cachedOrderedRooms.indexOf(room); + if (roomIdx === -1) { // can only happen if the js-sdk's store goes sideways. + console.warn(`Degrading performance to find missing room in "${this.tagId}": ${room.roomId}`); + roomIdx = this.cachedOrderedRooms.findIndex(r => r.roomId === room.roomId); + } + if (roomIdx === -1) { + throw new Error(`Room ${room.roomId} has no index in ${this.tagId}`); + } + + // Try to avoid doing array operations if we don't have to: only move rooms within + // the categories if we're jumping categories + const oldCategory = this.getCategoryFromIndices(roomIdx, this.indices); + if (oldCategory !== category) { + // Move the room and update the indices + this.moveRoomIndexes(1, oldCategory, category, this.indices); + this.cachedOrderedRooms.splice(roomIdx, 1); // splice out the old index (fixed position) + this.cachedOrderedRooms.splice(this.indices[category], 0, room); // splice in the new room (pre-adjusted) + // Note: if moveRoomIndexes() is called after the splice then the insert operation + // will happen in the wrong place. Because we would have already adjusted the index + // for the category, we don't need to determine how the room is moving in the list. + // If we instead tried to insert before updating the indices, we'd have to determine + // whether the room was moving later (towards IDLE) or earlier (towards RED) from its + // current position, as it'll affect the category's start index after we remove the + // room from the array. + } + + // The room received an update, so take out the slice and sort it. This should be relatively + // quick because the room is inserted at the top of the category, and most popular sorting + // algorithms will deal with trying to keep the active room at the top/start of the category. + // For the few algorithms that will have to move the thing quite far (alphabetic with a Z room + // for example), the list should already be sorted well enough that it can rip through the + // array and slot the changed room in quickly. + const nextCategoryStartIdx = category === CATEGORY_ORDER[CATEGORY_ORDER.length - 1] + ? Number.MAX_SAFE_INTEGER + : this.indices[CATEGORY_ORDER[CATEGORY_ORDER.indexOf(category) + 1]]; + const startIdx = this.indices[category]; + const numSort = nextCategoryStartIdx - startIdx; // splice() returns up to the max, so MAX_SAFE_INT is fine + const unsortedSlice = this.cachedOrderedRooms.splice(startIdx, numSort); + const sorted = await sortRoomsWithAlgorithm(unsortedSlice, this.tagId, this.sortingAlgorithm); + this.cachedOrderedRooms.splice(startIdx, 0, ...sorted); + + return true; // change made } + // noinspection JSMethodCanBeStatic private getCategoryFromIndices(index: number, indices: ICategoryIndex): Category { for (let i = 0; i < CATEGORY_ORDER.length; i++) { const category = CATEGORY_ORDER[i]; @@ -274,6 +217,7 @@ export class ImportanceAlgorithm extends Algorithm { throw new Error("Programming error: somehow you've ended up with an index that isn't in a category"); } + // noinspection JSMethodCanBeStatic private moveRoomIndexes(nRooms: number, fromCategory: Category, toCategory: Category, indices: ICategoryIndex) { // We have to update the index of the category *after* the from/toCategory variables // in order to update the indices correctly. Because the room is moving from/to those diff --git a/src/stores/room-list/algorithms/list-ordering/NaturalAlgorithm.ts b/src/stores/room-list/algorithms/list-ordering/NaturalAlgorithm.ts index d544b1196f..2302768fbf 100644 --- a/src/stores/room-list/algorithms/list-ordering/NaturalAlgorithm.ts +++ b/src/stores/room-list/algorithms/list-ordering/NaturalAlgorithm.ts @@ -14,49 +14,32 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { Algorithm } from "./Algorithm"; -import { ITagMap } from "../models"; +import { SortAlgorithm } from "../models"; import { sortRoomsWithAlgorithm } from "../tag-sorting"; +import { OrderingAlgorithm } from "./OrderingAlgorithm"; +import { TagID } from "../../models"; +import { Room } from "matrix-js-sdk/src/models/room"; /** * Uses the natural tag sorting algorithm order to determine tag ordering. No * additional behavioural changes are present. */ -export class NaturalAlgorithm extends Algorithm { +export class NaturalAlgorithm extends OrderingAlgorithm { - constructor() { - super(); + public constructor(tagId: TagID, initialSortingAlgorithm: SortAlgorithm) { + super(tagId, initialSortingAlgorithm); console.log("Constructed a NaturalAlgorithm"); } - protected async generateFreshTags(updatedTagMap: ITagMap): Promise { - 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`); - - updatedTagMap[tagId] = await sortRoomsWithAlgorithm(unorderedRooms, tagId, sortBy); - } + public async setRooms(rooms: Room[]): Promise { + this.cachedOrderedRooms = await sortRoomsWithAlgorithm(rooms, this.tagId, this.sortingAlgorithm); } public async handleRoomUpdate(room, cause): Promise { - const tags = this.roomIdsToTags[room.roomId]; - if (!tags) { - console.warn(`No tags known for "${room.name}" (${room.roomId})`); - return false; - } - let changed = false; - for (const tag of tags) { - // TODO: Optimize this loop to avoid useless operations - // For example, we can skip updates to alphabetic (sometimes) and manually ordered tags - this.cachedRooms[tag] = await sortRoomsWithAlgorithm(this.cachedRooms[tag], tag, this.sortAlgorithms[tag]); + // TODO: Optimize this to avoid useless operations + // For example, we can skip updates to alphabetic (sometimes) and manually ordered tags + this.cachedOrderedRooms = await sortRoomsWithAlgorithm(this.cachedOrderedRooms, this.tagId, this.sortingAlgorithm); - // Flag that we've done something - this.recalculateFilteredRoomsForTag(tag); // update filter to re-sort the list - this.recalculateStickyRoom(tag); // update sticky room to make sure it appears if needed - changed = true; - } - return changed; + return true; } } diff --git a/src/stores/room-list/algorithms/list-ordering/OrderingAlgorithm.ts b/src/stores/room-list/algorithms/list-ordering/OrderingAlgorithm.ts new file mode 100644 index 0000000000..263e8a4cd4 --- /dev/null +++ b/src/stores/room-list/algorithms/list-ordering/OrderingAlgorithm.ts @@ -0,0 +1,72 @@ +/* +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 { RoomUpdateCause, TagID } from "../../models"; +import { SortAlgorithm } from "../models"; + +/** + * Represents a list ordering algorithm. Subclasses should populate the + * `cachedOrderedRooms` field. + */ +export abstract class OrderingAlgorithm { + protected cachedOrderedRooms: Room[]; + protected sortingAlgorithm: SortAlgorithm; + + protected constructor(protected tagId: TagID, initialSortingAlgorithm: SortAlgorithm) { + // noinspection JSIgnoredPromiseFromCall + this.setSortAlgorithm(initialSortingAlgorithm); // we use the setter for validation + } + + /** + * The rooms as ordered by the algorithm. + */ + public get orderedRooms(): Room[] { + return this.cachedOrderedRooms || []; + } + + /** + * Sets the sorting algorithm to use within the list. + * @param newAlgorithm The new algorithm. Must be defined. + * @returns Resolves when complete. + */ + public async setSortAlgorithm(newAlgorithm: SortAlgorithm) { + if (!newAlgorithm) throw new Error("A sorting algorithm must be defined"); + this.sortingAlgorithm = newAlgorithm; + + // Force regeneration of the rooms + await this.setRooms(this.orderedRooms); + } + + /** + * Sets the rooms the algorithm should be handling, implying a reconstruction + * of the ordering. + * @param rooms The rooms to use going forward. + * @returns Resolves when complete. + */ + public abstract setRooms(rooms: Room[]): Promise; + + /** + * Handle a room update. The Algorithm will only call this for causes which + * the list ordering algorithm can handle within the same tag. For example, + * tag changes will not be sent here. + * @param room The room where the update happened. + * @param cause The cause of the update. + * @returns True if the update requires the Algorithm to update the presentation layers. + */ + // XXX: TODO: We assume this will only ever be a position update and NOT a NewRoom or RemoveRoom change!! + public abstract handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise; +} diff --git a/src/stores/room-list/algorithms/list-ordering/index.ts b/src/stores/room-list/algorithms/list-ordering/index.ts index bcccd150cd..8156c3a250 100644 --- a/src/stores/room-list/algorithms/list-ordering/index.ts +++ b/src/stores/room-list/algorithms/list-ordering/index.ts @@ -14,25 +14,32 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { Algorithm } from "./Algorithm"; import { ImportanceAlgorithm } from "./ImportanceAlgorithm"; -import { ListAlgorithm } from "../models"; +import { ListAlgorithm, SortAlgorithm } from "../models"; import { NaturalAlgorithm } from "./NaturalAlgorithm"; +import { TagID } from "../../models"; +import { OrderingAlgorithm } from "./OrderingAlgorithm"; -const ALGORITHM_FACTORIES: { [algorithm in ListAlgorithm]: () => Algorithm } = { - [ListAlgorithm.Natural]: () => new NaturalAlgorithm(), - [ListAlgorithm.Importance]: () => new ImportanceAlgorithm(), +interface AlgorithmFactory { + (tagId: TagID, initialSortingAlgorithm: SortAlgorithm): OrderingAlgorithm; +} + +const ALGORITHM_FACTORIES: { [algorithm in ListAlgorithm]: AlgorithmFactory } = { + [ListAlgorithm.Natural]: (tagId, initSort) => new NaturalAlgorithm(tagId, initSort), + [ListAlgorithm.Importance]: (tagId, initSort) => new ImportanceAlgorithm(tagId, initSort), }; /** * Gets an instance of the defined algorithm * @param {ListAlgorithm} algorithm The algorithm to get an instance of. + * @param {TagID} tagId The tag the algorithm is for. + * @param {SortAlgorithm} initSort The initial sorting algorithm for the ordering algorithm. * @returns {Algorithm} The algorithm instance. */ -export function getListAlgorithmInstance(algorithm: ListAlgorithm): Algorithm { +export function getListAlgorithmInstance(algorithm: ListAlgorithm, tagId: TagID, initSort: SortAlgorithm): OrderingAlgorithm { if (!ALGORITHM_FACTORIES[algorithm]) { throw new Error(`${algorithm} is not a known algorithm`); } - return ALGORITHM_FACTORIES[algorithm](); + return ALGORITHM_FACTORIES[algorithm](tagId, initSort); } diff --git a/src/stores/room-list/algorithms/models.ts b/src/stores/room-list/algorithms/models.ts index 284600a776..943d833b67 100644 --- a/src/stores/room-list/algorithms/models.ts +++ b/src/stores/room-list/algorithms/models.ts @@ -16,6 +16,7 @@ limitations under the License. import { TagID } from "../models"; import { Room } from "matrix-js-sdk/src/models/room"; +import { OrderingAlgorithm } from "./list-ordering/OrderingAlgorithm"; export enum SortAlgorithm { Manual = "MANUAL", @@ -36,6 +37,16 @@ export interface ITagSortingMap { [tagId: TagID]: SortAlgorithm; } +export interface IListOrderingMap { + // @ts-ignore - TypeScript really wants this to be [tagId: string] but we know better. + [tagId: TagID]: ListAlgorithm; +} + +export interface IOrderingAlgorithmMap { + // @ts-ignore - TypeScript really wants this to be [tagId: string] but we know better. + [tagId: TagID]: OrderingAlgorithm; +} + export interface ITagMap { // @ts-ignore - TypeScript really wants this to be [tagId: string] but we know better. [tagId: TagID]: Room[]; From 4aa15ee19187d4e8f9b9bc22176a1919ab28cd81 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 11 Jun 2020 22:04:10 -0600 Subject: [PATCH 4/7] Wire up the context menu to the room list store Updates are passed along magically to the sublist, so we don't need to listen for the room list store's response to our changes. This just hits the functions introduced in the last commit. --- res/css/views/rooms/_RoomSublist2.scss | 10 +++++- src/components/views/rooms/RoomSublist2.tsx | 35 +++++++++++++++++---- src/i18n/strings/en_EN.json | 2 ++ 3 files changed, 40 insertions(+), 7 deletions(-) diff --git a/res/css/views/rooms/_RoomSublist2.scss b/res/css/views/rooms/_RoomSublist2.scss index ed17c071b6..afedf64e32 100644 --- a/res/css/views/rooms/_RoomSublist2.scss +++ b/res/css/views/rooms/_RoomSublist2.scss @@ -240,6 +240,14 @@ limitations under the License. font-size: $font-15px; line-height: $font-20px; font-weight: 600; - margin-bottom: 12px; + margin-bottom: 4px; + } + + .mx_RadioButton, .mx_Checkbox { + margin-top: 8px; + } + + .mx_Checkbox { + margin-left: -8px; // to counteract the indent from the component } } diff --git a/src/components/views/rooms/RoomSublist2.tsx b/src/components/views/rooms/RoomSublist2.tsx index fa37eef975..91baf4ff81 100644 --- a/src/components/views/rooms/RoomSublist2.tsx +++ b/src/components/views/rooms/RoomSublist2.tsx @@ -27,8 +27,11 @@ import RoomTile2 from "./RoomTile2"; import { ResizableBox, ResizeCallbackData } from "react-resizable"; import { ListLayout } from "../../../stores/room-list/ListLayout"; import NotificationBadge, { ListNotificationState } from "./NotificationBadge"; -import {ContextMenu, ContextMenuButton} from "../../structures/ContextMenu"; +import { ContextMenu, ContextMenuButton } from "../../structures/ContextMenu"; import StyledCheckbox from "../elements/StyledCheckbox"; +import StyledRadioButton from "../elements/StyledRadioButton"; +import RoomListStore from "../../../stores/room-list/RoomListStore2"; +import { ListAlgorithm, SortAlgorithm } from "../../../stores/room-list/algorithms/models"; /******************************************************************* * CAUTION * @@ -115,9 +118,14 @@ export default class RoomSublist2 extends React.Component { this.setState({menuDisplayed: false}); }; - private onUnreadFirstChanged = () => { - // TODO: Support per-list algorithm changes - console.log("Unread first changed"); + private onUnreadFirstChanged = async () => { + const isUnreadFirst = RoomListStore.instance.getListOrder(this.props.layout.tagId) === ListAlgorithm.Importance; + const newAlgorithm = isUnreadFirst ? ListAlgorithm.Natural : ListAlgorithm.Importance; + await RoomListStore.instance.setListOrder(this.props.layout.tagId, newAlgorithm); + }; + + private onTagSortChanged = async (sort: SortAlgorithm) => { + await RoomListStore.instance.setTagSorting(this.props.layout.tagId, sort); }; private onMessagePreviewChanged = () => { @@ -147,6 +155,8 @@ export default class RoomSublist2 extends React.Component { let contextMenu = null; if (this.state.menuDisplayed) { const elementRect = this.menuButtonRef.current.getBoundingClientRect(); + const isAlphabetical = RoomListStore.instance.getTagSorting(this.props.layout.tagId) === SortAlgorithm.Alphabetic; + const isUnreadFirst = RoomListStore.instance.getListOrder(this.props.layout.tagId) === ListAlgorithm.Importance; contextMenu = ( {
{_t("Sort by")}
- TODO: Radios are blocked by https://github.com/matrix-org/matrix-react-sdk/pull/4731 + this.onTagSortChanged(SortAlgorithm.Recent)} + checked={!isAlphabetical} + name={`mx_${this.props.layout.tagId}_sortBy`} + > + {_t("Activity")} + + this.onTagSortChanged(SortAlgorithm.Alphabetic)} + checked={isAlphabetical} + name={`mx_${this.props.layout.tagId}_sortBy`} + > + {_t("A-Z")} +

{_t("Unread rooms")}
{_t("Always show first")} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 81577d740e..31da52a494 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1138,6 +1138,8 @@ "Not now": "Not now", "Don't ask me again": "Don't ask me again", "Sort by": "Sort by", + "Activity": "Activity", + "A-Z": "A-Z", "Unread rooms": "Unread rooms", "Always show first": "Always show first", "Show": "Show", From cc17409943d03b92f38087b11584c592303454ca Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 12 Jun 2020 08:37:08 -0600 Subject: [PATCH 5/7] Fix words Co-authored-by: J. Ryan Stinnett --- docs/room-list-store.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/room-list-store.md b/docs/room-list-store.md index 82c83d3593..53f0527209 100644 --- a/docs/room-list-store.md +++ b/docs/room-list-store.md @@ -24,7 +24,7 @@ algorithm to call, instead of having all the logic in the room list store itself Tag sorting is effectively the comparator supplied to the list algorithm. This gives the list algorithm -the power to decide when and how to apply the tag sorting, if at all. For example, The importance algorithm, +the power to decide when and how to apply the tag sorting, if at all. For example, the importance algorithm, later described in this document, heavily uses the list ordering behaviour to break the tag into categories. Each category then gets sorted by the appropriate tag sorting algorithm. From 6b54c3a492a26a20213a19660b6b2486b664eb50 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 12 Jun 2020 08:39:59 -0600 Subject: [PATCH 6/7] Throw if the update cause is unsupported --- .../algorithms/list-ordering/ImportanceAlgorithm.ts | 5 +++++ .../room-list/algorithms/list-ordering/NaturalAlgorithm.ts | 7 ++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm.ts b/src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm.ts index 2e24cb23f1..325aaf19e6 100644 --- a/src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm.ts +++ b/src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm.ts @@ -152,6 +152,11 @@ export class ImportanceAlgorithm extends OrderingAlgorithm { } public async handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise { + // TODO: Handle NewRoom and RoomRemoved + if (cause !== RoomUpdateCause.Timeline && cause !== RoomUpdateCause.ReadReceipt) { + throw new Error(`Unsupported update cause: ${cause}`); + } + const category = this.getRoomCategory(room); if (this.sortingAlgorithm === SortAlgorithm.Manual) { return; // Nothing to do here. diff --git a/src/stores/room-list/algorithms/list-ordering/NaturalAlgorithm.ts b/src/stores/room-list/algorithms/list-ordering/NaturalAlgorithm.ts index 2302768fbf..cce7372986 100644 --- a/src/stores/room-list/algorithms/list-ordering/NaturalAlgorithm.ts +++ b/src/stores/room-list/algorithms/list-ordering/NaturalAlgorithm.ts @@ -17,7 +17,7 @@ limitations under the License. import { SortAlgorithm } from "../models"; import { sortRoomsWithAlgorithm } from "../tag-sorting"; import { OrderingAlgorithm } from "./OrderingAlgorithm"; -import { TagID } from "../../models"; +import { RoomUpdateCause, TagID } from "../../models"; import { Room } from "matrix-js-sdk/src/models/room"; /** @@ -36,6 +36,11 @@ export class NaturalAlgorithm extends OrderingAlgorithm { } public async handleRoomUpdate(room, cause): Promise { + // TODO: Handle NewRoom and RoomRemoved + if (cause !== RoomUpdateCause.Timeline && cause !== RoomUpdateCause.ReadReceipt) { + throw new Error(`Unsupported update cause: ${cause}`); + } + // TODO: Optimize this to avoid useless operations // For example, we can skip updates to alphabetic (sometimes) and manually ordered tags this.cachedOrderedRooms = await sortRoomsWithAlgorithm(this.cachedOrderedRooms, this.tagId, this.sortingAlgorithm); From 6de6d94f7c8c2ff7adcea554d98dcd8707d61651 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 12 Jun 2020 08:40:08 -0600 Subject: [PATCH 7/7] Fix return type --- src/stores/room-list/algorithms/Algorithm.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stores/room-list/algorithms/Algorithm.ts b/src/stores/room-list/algorithms/Algorithm.ts index 438ef78e7e..e8b167c1ba 100644 --- a/src/stores/room-list/algorithms/Algorithm.ts +++ b/src/stores/room-list/algorithms/Algorithm.ts @@ -508,7 +508,7 @@ export class Algorithm extends EventEmitter { if (cause === RoomUpdateCause.NewRoom) { // TODO: Be smarter and insert rather than regen the planet. await this.setKnownRooms([room, ...this.rooms]); - return; + return true; } if (cause === RoomUpdateCause.RoomRemoved) {