diff --git a/src/stores/room-list/algorithms/list-ordering/Algorithm.ts b/src/stores/room-list/algorithms/list-ordering/Algorithm.ts index c3ac8c150f..a8512c0bd6 100644 --- a/src/stores/room-list/algorithms/list-ordering/Algorithm.ts +++ b/src/stores/room-list/algorithms/list-ordering/Algorithm.ts @@ -20,9 +20,11 @@ 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, IFilterCondition } from "../../filters/IFilterCondition"; +import { FILTER_CHANGED, FilterPriority, IFilterCondition } from "../../filters/IFilterCondition"; import { EventEmitter } from "events"; import { UPDATE_EVENT } from "../../../AsyncStore"; +import { ArrayUtil } from "../../../../utils/arrays"; +import { getEnumValues } from "../../../../utils/enums"; // TODO: Add locking support to avoid concurrent writes? @@ -184,22 +186,33 @@ export abstract class Algorithm extends EventEmitter { } console.warn("Recalculating filtered room list"); - const allowedByFilters = new Set(); const filters = Array.from(this.allowedByFilter.keys()); + const orderedFilters = new ArrayUtil(filters) + .groupBy(f => f.relativePriority) + .orderBy(getEnumValues(FilterPriority)) + .value; const newMap: ITagMap = {}; for (const tagId of Object.keys(this.cachedRooms)) { // Cheaply clone the rooms so we can more easily do operations on the list. // We optimize our lookups by trying to reduce sample size as much as possible // to the rooms we know will be deduped by the Set. const rooms = this.cachedRooms[tagId]; - const remainingRooms = rooms.map(r => r).filter(r => !allowedByFilters.has(r)); - const allowedRoomsInThisTag = []; - for (const filter of filters) { + let remainingRooms = rooms.map(r => r); + let allowedRoomsInThisTag = []; + let lastFilterPriority = orderedFilters[0].relativePriority; + for (const filter of orderedFilters) { + if (filter.relativePriority !== lastFilterPriority) { + // Every time the filter changes priority, we want more specific filtering. + // To accomplish that, reset the variables to make it look like the process + // has started over, but using the filtered rooms as the seed. + remainingRooms = allowedRoomsInThisTag; + allowedRoomsInThisTag = []; + lastFilterPriority = filter.relativePriority; + } const filteredRooms = remainingRooms.filter(r => filter.isVisible(r)); for (const room of filteredRooms) { const idx = remainingRooms.indexOf(room); if (idx >= 0) remainingRooms.splice(idx, 1); - allowedByFilters.add(room); allowedRoomsInThisTag.push(room); } } @@ -207,7 +220,8 @@ export abstract class Algorithm extends EventEmitter { console.log(`[DEBUG] ${newMap[tagId].length}/${rooms.length} rooms filtered into ${tagId}`); } - this.allowedRoomsByFilters = allowedByFilters; + const allowedRooms = Object.values(newMap).reduce((rv, v) => { rv.push(...v); return rv; }, []); + this.allowedRoomsByFilters = new Set(allowedRooms); this.filteredRooms = newMap; this.emit(LIST_UPDATED_EVENT); } diff --git a/src/stores/room-list/filters/CommunityFilterCondition.ts b/src/stores/room-list/filters/CommunityFilterCondition.ts index 13d0084ae1..3dfaffee3b 100644 --- a/src/stores/room-list/filters/CommunityFilterCondition.ts +++ b/src/stores/room-list/filters/CommunityFilterCondition.ts @@ -15,7 +15,7 @@ limitations under the License. */ import { Room } from "matrix-js-sdk/src/models/room"; -import { FILTER_CHANGED, IFilterCondition } from "./IFilterCondition"; +import { FILTER_CHANGED, FilterPriority, IFilterCondition } from "./IFilterCondition"; import { Group } from "matrix-js-sdk/src/models/group"; import { EventEmitter } from "events"; import GroupStore from "../../GroupStore"; @@ -37,6 +37,11 @@ export class CommunityFilterCondition extends EventEmitter implements IFilterCon this.onStoreUpdate(); // trigger a false update to seed the store } + public get relativePriority(): FilterPriority { + // Lowest priority so we can coarsely find rooms. + return FilterPriority.Lowest; + } + public isVisible(room: Room): boolean { return this.roomIds.includes(room.roomId); } diff --git a/src/stores/room-list/filters/IFilterCondition.ts b/src/stores/room-list/filters/IFilterCondition.ts index f7f0f61194..3b054eaece 100644 --- a/src/stores/room-list/filters/IFilterCondition.ts +++ b/src/stores/room-list/filters/IFilterCondition.ts @@ -19,6 +19,12 @@ import { EventEmitter } from "events"; export const FILTER_CHANGED = "filter_changed"; +export enum FilterPriority { + Lowest, + // in the middle would be Low, Normal, and High if we had a need + Highest, +} + /** * A filter condition for the room list, determining if a room * should be shown or not. @@ -32,6 +38,12 @@ export const FILTER_CHANGED = "filter_changed"; * as a change in the user's input), this emits FILTER_CHANGED. */ export interface IFilterCondition extends EventEmitter { + /** + * The relative priority that this filter should be applied with. + * Lower priorities get applied first. + */ + relativePriority: FilterPriority; + /** * Determines if a given room should be visible under this * condition. diff --git a/src/stores/room-list/filters/NameFilterCondition.ts b/src/stores/room-list/filters/NameFilterCondition.ts index 6a76569df3..f238cdeb09 100644 --- a/src/stores/room-list/filters/NameFilterCondition.ts +++ b/src/stores/room-list/filters/NameFilterCondition.ts @@ -15,7 +15,7 @@ limitations under the License. */ import { Room } from "matrix-js-sdk/src/models/room"; -import { FILTER_CHANGED, IFilterCondition } from "./IFilterCondition"; +import { FILTER_CHANGED, FilterPriority, IFilterCondition } from "./IFilterCondition"; import { EventEmitter } from "events"; /** @@ -29,6 +29,11 @@ export class NameFilterCondition extends EventEmitter implements IFilterConditio super(); } + public get relativePriority(): FilterPriority { + // We want this one to be at the highest priority so it can search within other filters. + return FilterPriority.Highest; + } + public get search(): string { return this._search; } diff --git a/src/utils/arrays.ts b/src/utils/arrays.ts index 3a84990b56..fea376afcd 100644 --- a/src/utils/arrays.ts +++ b/src/utils/arrays.ts @@ -45,3 +45,63 @@ export function arrayDiff(a: T[], b: T[]): { added: T[], removed: T[] } { removed: a.filter(i => !b.includes(i)), }; } + +/** + * Helper functions to perform LINQ-like queries on arrays. + */ +export class ArrayUtil { + /** + * Create a new array helper. + * @param a The array to help. Can be modified in-place. + */ + constructor(private a: T[]) { + } + + /** + * The value of this array, after all appropriate alterations. + */ + public get value(): T[] { + return this.a; + } + + /** + * Groups an array by keys. + * @param fn The key-finding function. + * @returns This. + */ + public groupBy(fn: (a: T) => K): GroupedArray { + const obj = this.a.reduce((rv: Map, val: T) => { + const k = fn(val); + if (!rv.has(k)) rv.set(k, []); + rv.get(k).push(val); + return rv; + }, new Map()); + return new GroupedArray(obj); + } +} + +/** + * Helper functions to perform LINQ-like queries on groups (maps). + */ +export class GroupedArray { + /** + * Creates a new group helper. + * @param val The group to help. Can be modified in-place. + */ + constructor(private val: Map) { + } + + /** + * Orders the grouping into an array using the provided key order. + * @param keyOrder The key order. + * @returns An array helper of the result. + */ + public orderBy(keyOrder: K[]): ArrayUtil { + const a: T[] = []; + for (const k of keyOrder) { + if (!this.val.has(k)) continue; + a.push(...this.val.get(k)); + } + return new ArrayUtil(a); + } +} diff --git a/src/utils/enums.ts b/src/utils/enums.ts new file mode 100644 index 0000000000..42abaa0269 --- /dev/null +++ b/src/utils/enums.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. +*/ + +export type EnumValue = string | number; + +/** + * Get the values for an enum. + * @param e The enum. + * @returns The enum values. + */ +export function getEnumValues(e: any): T[] { + const keys = Object.keys(e); + return keys + .filter(k => ['string', 'number'].includes(typeof(e[k]))) + .map(k => e[k]); +}