diff --git a/src/stores/room-list/RoomListStore2.ts b/src/stores/room-list/RoomListStore2.ts index dc1cb49cd6..0b3f61e261 100644 --- a/src/stores/room-list/RoomListStore2.ts +++ b/src/stores/room-list/RoomListStore2.ts @@ -19,10 +19,11 @@ import { MatrixClient } from "matrix-js-sdk/src/client"; import { ActionPayload, defaultDispatcher } from "../../dispatcher-types"; import SettingsStore from "../../settings/SettingsStore"; import { DefaultTagID, OrderedDefaultTagIDs, TagID } from "./models"; -import { Algorithm, ITagMap, ITagSortingMap, ListAlgorithm, SortAlgorithm } from "./algorithms/Algorithm"; +import { Algorithm } from "./algorithms/list_ordering/Algorithm"; import TagOrderStore from "../TagOrderStore"; -import { getAlgorithmInstance } from "./algorithms"; +import { getListAlgorithmInstance } from "./algorithms/list_ordering"; import { AsyncStore } from "../AsyncStore"; +import { ITagMap, ITagSortingMap, ListAlgorithm, SortAlgorithm } from "./algorithms/models"; interface IState { tagsEnabled?: boolean; @@ -172,7 +173,7 @@ class _RoomListStore extends AsyncStore { } private setAlgorithmClass() { - this.algorithm = getAlgorithmInstance(this.state.preferredAlgorithm); + this.algorithm = getListAlgorithmInstance(this.state.preferredAlgorithm); } private async regenerateAllLists() { diff --git a/src/stores/room-list/RoomListStoreTempProxy.ts b/src/stores/room-list/RoomListStoreTempProxy.ts index 8ad3c5d35e..4edca2b9cd 100644 --- a/src/stores/room-list/RoomListStoreTempProxy.ts +++ b/src/stores/room-list/RoomListStoreTempProxy.ts @@ -19,7 +19,7 @@ import { Room } from "matrix-js-sdk/src/models/room"; import SettingsStore from "../../settings/SettingsStore"; import RoomListStore from "./RoomListStore2"; import OldRoomListStore from "../RoomListStore"; -import { ITagMap } from "./algorithms/Algorithm"; +import { ITagMap } from "./algorithms/models"; import { UPDATE_EVENT } from "../AsyncStore"; /** diff --git a/src/stores/room-list/algorithms/Algorithm.ts b/src/stores/room-list/algorithms/list_ordering/Algorithm.ts similarity index 90% rename from src/stores/room-list/algorithms/Algorithm.ts rename to src/stores/room-list/algorithms/list_ordering/Algorithm.ts index 15fc208b21..4c8c9e9c60 100644 --- a/src/stores/room-list/algorithms/Algorithm.ts +++ b/src/stores/room-list/algorithms/list_ordering/Algorithm.ts @@ -14,38 +14,20 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { DefaultTagID, TagID } from "../models"; +import { DefaultTagID, TagID } from "../../models"; import { Room } from "matrix-js-sdk/src/models/room"; import { isNullOrUndefined } from "matrix-js-sdk/src/utils"; -import { EffectiveMembership, splitRoomsByMembership } from "../membership"; - -export enum SortAlgorithm { - Manual = "MANUAL", - Alphabetic = "ALPHABETIC", - Recent = "RECENT", -} - -export enum ListAlgorithm { - // Orders Red > Grey > Bold > Idle - Importance = "IMPORTANCE", - - // Orders however the SortAlgorithm decides - Natural = "NATURAL", -} - -export interface ITagSortingMap { - // @ts-ignore - TypeScript really wants this to be [tagId: string] but we know better. - [tagId: TagID]: SortAlgorithm; -} - -export interface ITagMap { - // @ts-ignore - TypeScript really wants this to be [tagId: string] but we know better. - [tagId: TagID]: Room[]; -} +import { EffectiveMembership, splitRoomsByMembership } from "../../membership"; +import { ITagMap, ITagSortingMap } from "../models"; // TODO: Add locking support to avoid concurrent writes? // TODO: EventEmitter support? Might not be needed. +/** + * Represents a list ordering algorithm. This class will take care of tag + * management (which rooms go in which tags) and ask the implementation to + * deal with ordering mechanics. + */ export abstract class Algorithm { protected cached: ITagMap = {}; protected sortAlgorithms: ITagSortingMap; @@ -160,6 +142,7 @@ export abstract class Algorithm { * @param {Room[]} rooms The rooms within the tag, unordered. * @returns {Promise} Resolves to the ordered rooms in the tag. */ + // TODO: Do we need this? protected abstract regenerateTag(tagId: TagID, rooms: Room[]): Promise; /** @@ -173,6 +156,6 @@ export abstract class Algorithm { * processing. */ // TODO: Take a ReasonForChange to better predict the behaviour? - // TODO: Intercept here and handle tag changes automatically + // TODO: Intercept here and handle tag changes automatically? May be best to let the impl do that. public abstract handleRoomUpdate(room: Room): Promise; } diff --git a/src/stores/room-list/algorithms/ChaoticAlgorithm.ts b/src/stores/room-list/algorithms/list_ordering/ChaoticAlgorithm.ts similarity index 90% rename from src/stores/room-list/algorithms/ChaoticAlgorithm.ts rename to src/stores/room-list/algorithms/list_ordering/ChaoticAlgorithm.ts index 5d4177db8b..7c1a0b1acc 100644 --- a/src/stores/room-list/algorithms/ChaoticAlgorithm.ts +++ b/src/stores/room-list/algorithms/list_ordering/ChaoticAlgorithm.ts @@ -14,8 +14,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { Algorithm, ITagMap } from "./Algorithm"; -import { DefaultTagID } from "../models"; +import { Algorithm } from "./Algorithm"; +import { DefaultTagID } from "../../models"; +import { ITagMap } from "../models"; /** * A demonstration/temporary algorithm to verify the API surface works. diff --git a/src/stores/room-list/algorithms/ImportanceAlgorithm.ts b/src/stores/room-list/algorithms/list_ordering/ImportanceAlgorithm.ts similarity index 63% rename from src/stores/room-list/algorithms/ImportanceAlgorithm.ts rename to src/stores/room-list/algorithms/list_ordering/ImportanceAlgorithm.ts index 1a7a73a9d5..d73fdee930 100644 --- a/src/stores/room-list/algorithms/ImportanceAlgorithm.ts +++ b/src/stores/room-list/algorithms/list_ordering/ImportanceAlgorithm.ts @@ -1,4 +1,5 @@ /* +Copyright 2018, 2019 New Vector Ltd Copyright 2020 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,11 +15,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { Algorithm, ITagMap, ITagSortingMap } from "./Algorithm"; +import { Algorithm } from "./Algorithm"; import { Room } from "matrix-js-sdk/src/models/room"; -import { isNullOrUndefined } from "matrix-js-sdk/src/utils"; -import { DefaultTagID, TagID } from "../models"; -import { splitRoomsByMembership } from "../membership"; +import { DefaultTagID, TagID } from "../../models"; +import { ITagMap, SortAlgorithm } from "../models"; +import { getSortingAlgorithmInstance, sortRoomsWithAlgorithm } from "../tag_sorting"; +import * as Unread from '../../../../Unread'; /** * The determined category of a room. @@ -44,6 +46,11 @@ export enum Category { Idle = "IDLE", } +interface ICategorizedRoomMap { + // @ts-ignore - TS wants this to be a string, but we know better + [category: Category]: Room[]; +} + /** * An implementation of the "importance" algorithm for room list sorting. Where * the tag sorting algorithm does not interfere, rooms will be ordered into @@ -119,8 +126,72 @@ export class ImportanceAlgorithm extends Algorithm { console.log("Constructed an ImportanceAlgorithm"); } + // noinspection JSMethodCanBeStatic + private categorizeRooms(rooms: Room[]): ICategorizedRoomMap { + const map: ICategorizedRoomMap = { + [Category.Red]: [], + [Category.Grey]: [], + [Category.Bold]: [], + [Category.Idle]: [], + }; + for (const room of rooms) { + const category = this.getRoomCategory(room); + console.log(`[DEBUG] "${room.name}" (${room.roomId}) is a ${category} room`); + map[category].push(room); + } + return map; + } + + // noinspection JSMethodCanBeStatic + private getRoomCategory(room: Room): Category { + // Function implementation borrowed from old RoomListStore + + const mentions = room.getUnreadNotificationCount('highlight') > 0; + if (mentions) { + return Category.Red; + } + + let unread = room.getUnreadNotificationCount() > 0; + if (unread) { + return Category.Grey; + } + + unread = Unread.doesRoomHaveUnreadMessages(room); + if (unread) { + return Category.Bold; + } + + return Category.Idle; + } + protected async generateFreshTags(updatedTagMap: ITagMap): Promise { - return Promise.resolve(); + for (const tagId of Object.keys(updatedTagMap)) { + const unorderedRooms = updatedTagMap[tagId]; + + const sortBy = this.sortAlgorithms[tagId]; + if (!sortBy) throw new Error(`${tagId} does not have a sorting algorithm`); + + if (sortBy === SortAlgorithm.Manual) { + // Manual tags essentially ignore the importance algorithm, so don't do anything + // special about them. + updatedTagMap[tagId] = await sortRoomsWithAlgorithm(unorderedRooms, tagId, sortBy); + } else { + // Every other sorting type affects the categories, not the whole tag. + const categorized = this.categorizeRooms(unorderedRooms); + for (const category of Object.keys(categorized)) { + const roomsToOrder = categorized[category]; + categorized[category] = await sortRoomsWithAlgorithm(roomsToOrder, tagId, sortBy); + } + + // TODO: Update positions of categories in cache + updatedTagMap[tagId] = [ + ...categorized[Category.Red], + ...categorized[Category.Grey], + ...categorized[Category.Bold], + ...categorized[Category.Idle], + ]; + } + } } protected async regenerateTag(tagId: string | DefaultTagID, rooms: []): Promise<[]> { diff --git a/src/stores/room-list/algorithms/index.ts b/src/stores/room-list/algorithms/list_ordering/index.ts similarity index 84% rename from src/stores/room-list/algorithms/index.ts rename to src/stores/room-list/algorithms/list_ordering/index.ts index 1277b66ac9..35f4af14cf 100644 --- a/src/stores/room-list/algorithms/index.ts +++ b/src/stores/room-list/algorithms/list_ordering/index.ts @@ -14,12 +14,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { Algorithm, ListAlgorithm } from "./Algorithm"; +import { Algorithm } from "./Algorithm"; import { ChaoticAlgorithm } from "./ChaoticAlgorithm"; import { ImportanceAlgorithm } from "./ImportanceAlgorithm"; +import { ListAlgorithm } from "../models"; const ALGORITHM_FACTORIES: { [algorithm in ListAlgorithm]: () => Algorithm } = { - [ListAlgorithm.Natural]: () => new ChaoticAlgorithm(ListAlgorithm.Natural), + [ListAlgorithm.Natural]: () => new ChaoticAlgorithm(), [ListAlgorithm.Importance]: () => new ImportanceAlgorithm(), }; @@ -28,7 +29,7 @@ const ALGORITHM_FACTORIES: { [algorithm in ListAlgorithm]: () => Algorithm } = { * @param {ListAlgorithm} algorithm The algorithm to get an instance of. * @returns {Algorithm} The algorithm instance. */ -export function getAlgorithmInstance(algorithm: ListAlgorithm): Algorithm { +export function getListAlgorithmInstance(algorithm: ListAlgorithm): Algorithm { if (!ALGORITHM_FACTORIES[algorithm]) { throw new Error(`${algorithm} is not a known algorithm`); } diff --git a/src/stores/room-list/algorithms/models.ts b/src/stores/room-list/algorithms/models.ts new file mode 100644 index 0000000000..284600a776 --- /dev/null +++ b/src/stores/room-list/algorithms/models.ts @@ -0,0 +1,42 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { TagID } from "../models"; +import { Room } from "matrix-js-sdk/src/models/room"; + +export enum SortAlgorithm { + Manual = "MANUAL", + Alphabetic = "ALPHABETIC", + Recent = "RECENT", +} + +export enum ListAlgorithm { + // Orders Red > Grey > Bold > Idle + Importance = "IMPORTANCE", + + // Orders however the SortAlgorithm decides + Natural = "NATURAL", +} + +export interface ITagSortingMap { + // @ts-ignore - TypeScript really wants this to be [tagId: string] but we know better. + [tagId: TagID]: SortAlgorithm; +} + +export interface ITagMap { + // @ts-ignore - TypeScript really wants this to be [tagId: string] but we know better. + [tagId: TagID]: Room[]; +} diff --git a/src/stores/room-list/algorithms/tag_sorting/ChaoticAlgorithm.ts b/src/stores/room-list/algorithms/tag_sorting/ChaoticAlgorithm.ts new file mode 100644 index 0000000000..31846d084a --- /dev/null +++ b/src/stores/room-list/algorithms/tag_sorting/ChaoticAlgorithm.ts @@ -0,0 +1,29 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { Room } from "matrix-js-sdk/src/models/room"; +import { TagID } from "../../models"; +import { IAlgorithm } from "./IAlgorithm"; + +/** + * A demonstration to test the API surface. + * TODO: Remove this before landing + */ +export class ChaoticAlgorithm implements IAlgorithm { + public async sortRooms(rooms: Room[], tagId: TagID): Promise { + return rooms; + } +} diff --git a/src/stores/room-list/algorithms/tag_sorting/IAlgorithm.ts b/src/stores/room-list/algorithms/tag_sorting/IAlgorithm.ts new file mode 100644 index 0000000000..6c22ee0c9c --- /dev/null +++ b/src/stores/room-list/algorithms/tag_sorting/IAlgorithm.ts @@ -0,0 +1,31 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { Room } from "matrix-js-sdk/src/models/room"; +import { TagID } from "../../models"; + +/** + * Represents a tag sorting algorithm. + */ +export interface IAlgorithm { + /** + * Sorts the given rooms according to the sorting rules of the algorithm. + * @param {Room[]} rooms The rooms to sort. + * @param {TagID} tagId The tag ID in which the rooms are being sorted. + * @returns {Promise} Resolves to the sorted rooms. + */ + sortRooms(rooms: Room[], tagId: TagID): Promise; +} diff --git a/src/stores/room-list/algorithms/tag_sorting/ManualAlgorithm.ts b/src/stores/room-list/algorithms/tag_sorting/ManualAlgorithm.ts new file mode 100644 index 0000000000..b8c0357633 --- /dev/null +++ b/src/stores/room-list/algorithms/tag_sorting/ManualAlgorithm.ts @@ -0,0 +1,31 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { Room } from "matrix-js-sdk/src/models/room"; +import { TagID } from "../../models"; +import { IAlgorithm } from "./IAlgorithm"; + +/** + * Sorts rooms according to the tag's `order` property on the room. + */ +export class ManualAlgorithm implements IAlgorithm { + public async sortRooms(rooms: Room[], tagId: TagID): Promise { + const getOrderProp = (r: Room) => r.tags[tagId].order || 0; + return rooms.sort((a, b) => { + return getOrderProp(a) - getOrderProp(b); + }); + } +} diff --git a/src/stores/room-list/algorithms/tag_sorting/index.ts b/src/stores/room-list/algorithms/tag_sorting/index.ts new file mode 100644 index 0000000000..07f8f484d8 --- /dev/null +++ b/src/stores/room-list/algorithms/tag_sorting/index.ts @@ -0,0 +1,52 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { ChaoticAlgorithm } from "./ChaoticAlgorithm"; +import { SortAlgorithm } from "../models"; +import { ManualAlgorithm } from "./ManualAlgorithm"; +import { IAlgorithm } from "./IAlgorithm"; +import { TagID } from "../../models"; +import {Room} from "matrix-js-sdk/src/models/room"; + +const ALGORITHM_INSTANCES: { [algorithm in SortAlgorithm]: IAlgorithm } = { + [SortAlgorithm.Recent]: new ChaoticAlgorithm(), + [SortAlgorithm.Alphabetic]: new ChaoticAlgorithm(), + [SortAlgorithm.Manual]: new ManualAlgorithm(), +}; + +/** + * Gets an instance of the defined algorithm + * @param {SortAlgorithm} algorithm The algorithm to get an instance of. + * @returns {IAlgorithm} The algorithm instance. + */ +export function getSortingAlgorithmInstance(algorithm: SortAlgorithm): IAlgorithm { + if (!ALGORITHM_INSTANCES[algorithm]) { + throw new Error(`${algorithm} is not a known algorithm`); + } + + return ALGORITHM_INSTANCES[algorithm]; +} + +/** + * Sorts rooms in a given tag according to the algorithm given. + * @param {Room[]} rooms The rooms to sort. + * @param {TagID} tagId The tag in which the sorting is occurring. + * @param {SortAlgorithm} algorithm The algorithm to use for sorting. + * @returns {Promise} Resolves to the sorted rooms. + */ +export function sortRoomsWithAlgorithm(rooms: Room[], tagId: TagID, algorithm: SortAlgorithm): Promise { + return getSortingAlgorithmInstance(algorithm).sortRooms(rooms, tagId); +}