mirror of https://github.com/vector-im/riot-web
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.pull/21833/head
parent
52b26deb2e
commit
fd029e8e80
|
@ -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<ActionPayload> {
|
|||
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<ActionPayload> {
|
|||
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<ActionPayload> {
|
|||
|
||||
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<ActionPayload> {
|
|||
}
|
||||
}
|
||||
|
||||
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 <SortAlgorithm>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 <ListAlgorithm>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<ActionPayload> {
|
|||
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<ActionPayload> {
|
|||
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<ActionPayload> {
|
|||
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;
|
||||
|
|
|
@ -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<IFilterCondition, Room[]> = new Map<IFilterCondition, Room[]>();
|
||||
protected allowedRoomsByFilters: Set<Room> = new Set<Room>();
|
||||
private allowedByFilter: Map<IFilterCondition, Room[]> = new Map<IFilterCondition, Room[]>();
|
||||
private allowedRoomsByFilters: Set<Room> = new Set<Room>();
|
||||
|
||||
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<any> {
|
||||
if (!tagSortingMap) throw new Error(`Map cannot be null or empty`);
|
||||
public async populateTags(tagSortingMap: ITagSortingMap, listOrderingMap: IListOrderingMap): Promise<any> {
|
||||
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<any>;
|
||||
private async generateFreshTags(updatedTagMap: ITagMap): Promise<any> {
|
||||
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<boolean>;
|
||||
public async handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise<boolean> {
|
||||
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;
|
||||
};
|
||||
}
|
|
@ -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<any> {
|
||||
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<any> {
|
||||
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<boolean> {
|
||||
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
|
||||
|
|
|
@ -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<any> {
|
||||
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<any> {
|
||||
this.cachedOrderedRooms = await sortRoomsWithAlgorithm(rooms, this.tagId, this.sortingAlgorithm);
|
||||
}
|
||||
|
||||
public async handleRoomUpdate(room, cause): Promise<boolean> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<any>;
|
||||
|
||||
/**
|
||||
* 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<boolean>;
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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[];
|
||||
|
|
Loading…
Reference in New Issue