From 3fcd5505b3ac21ad8298e9ee31ea5ad1b5b9425d Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 8 Jun 2020 21:38:56 -0600 Subject: [PATCH 1/2] Support prioritized room list filters This is to fix an issue where when using both the community filter panel and the search box it's an AND rather than further refining the results. This makes the search box further refine the community filter panel results. --- .../algorithms/list-ordering/Algorithm.ts | 28 ++++++--- .../filters/CommunityFilterCondition.ts | 7 ++- .../room-list/filters/IFilterCondition.ts | 12 ++++ .../room-list/filters/NameFilterCondition.ts | 7 ++- src/utils/arrays.ts | 60 +++++++++++++++++++ src/utils/enums.ts | 29 +++++++++ 6 files changed, 134 insertions(+), 9 deletions(-) create mode 100644 src/utils/enums.ts 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]); +} From 1f82396ec54fd0d0228227e4c4924c4c5830e036 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 8 Jun 2020 21:40:25 -0600 Subject: [PATCH 2/2] Remove unused type --- src/utils/enums.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/utils/enums.ts b/src/utils/enums.ts index 42abaa0269..79656d7675 100644 --- a/src/utils/enums.ts +++ b/src/utils/enums.ts @@ -14,8 +14,6 @@ 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.