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.
pull/21833/head
Travis Ralston 2020-06-08 21:38:56 -06:00
parent ae1d14d29f
commit 3fcd5505b3
6 changed files with 134 additions and 9 deletions

View File

@ -20,9 +20,11 @@ import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
import { EffectiveMembership, splitRoomsByMembership } from "../../membership"; import { EffectiveMembership, splitRoomsByMembership } from "../../membership";
import { ITagMap, ITagSortingMap } from "../models"; import { ITagMap, ITagSortingMap } from "../models";
import DMRoomMap from "../../../../utils/DMRoomMap"; 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 { EventEmitter } from "events";
import { UPDATE_EVENT } from "../../../AsyncStore"; import { UPDATE_EVENT } from "../../../AsyncStore";
import { ArrayUtil } from "../../../../utils/arrays";
import { getEnumValues } from "../../../../utils/enums";
// TODO: Add locking support to avoid concurrent writes? // TODO: Add locking support to avoid concurrent writes?
@ -184,22 +186,33 @@ export abstract class Algorithm extends EventEmitter {
} }
console.warn("Recalculating filtered room list"); console.warn("Recalculating filtered room list");
const allowedByFilters = new Set<Room>();
const filters = Array.from(this.allowedByFilter.keys()); const filters = Array.from(this.allowedByFilter.keys());
const orderedFilters = new ArrayUtil(filters)
.groupBy(f => f.relativePriority)
.orderBy(getEnumValues(FilterPriority))
.value;
const newMap: ITagMap = {}; const newMap: ITagMap = {};
for (const tagId of Object.keys(this.cachedRooms)) { for (const tagId of Object.keys(this.cachedRooms)) {
// Cheaply clone the rooms so we can more easily do operations on the list. // 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 // 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. // to the rooms we know will be deduped by the Set.
const rooms = this.cachedRooms[tagId]; const rooms = this.cachedRooms[tagId];
const remainingRooms = rooms.map(r => r).filter(r => !allowedByFilters.has(r)); let remainingRooms = rooms.map(r => r);
const allowedRoomsInThisTag = []; let allowedRoomsInThisTag = [];
for (const filter of filters) { 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)); const filteredRooms = remainingRooms.filter(r => filter.isVisible(r));
for (const room of filteredRooms) { for (const room of filteredRooms) {
const idx = remainingRooms.indexOf(room); const idx = remainingRooms.indexOf(room);
if (idx >= 0) remainingRooms.splice(idx, 1); if (idx >= 0) remainingRooms.splice(idx, 1);
allowedByFilters.add(room);
allowedRoomsInThisTag.push(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}`); 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; }, <Room[]>[]);
this.allowedRoomsByFilters = new Set(allowedRooms);
this.filteredRooms = newMap; this.filteredRooms = newMap;
this.emit(LIST_UPDATED_EVENT); this.emit(LIST_UPDATED_EVENT);
} }

View File

@ -15,7 +15,7 @@ limitations under the License.
*/ */
import { Room } from "matrix-js-sdk/src/models/room"; 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 { Group } from "matrix-js-sdk/src/models/group";
import { EventEmitter } from "events"; import { EventEmitter } from "events";
import GroupStore from "../../GroupStore"; 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 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 { public isVisible(room: Room): boolean {
return this.roomIds.includes(room.roomId); return this.roomIds.includes(room.roomId);
} }

View File

@ -19,6 +19,12 @@ import { EventEmitter } from "events";
export const FILTER_CHANGED = "filter_changed"; 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 * A filter condition for the room list, determining if a room
* should be shown or not. * 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. * as a change in the user's input), this emits FILTER_CHANGED.
*/ */
export interface IFilterCondition extends EventEmitter { 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 * Determines if a given room should be visible under this
* condition. * condition.

View File

@ -15,7 +15,7 @@ limitations under the License.
*/ */
import { Room } from "matrix-js-sdk/src/models/room"; 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"; import { EventEmitter } from "events";
/** /**
@ -29,6 +29,11 @@ export class NameFilterCondition extends EventEmitter implements IFilterConditio
super(); 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 { public get search(): string {
return this._search; return this._search;
} }

View File

@ -45,3 +45,63 @@ export function arrayDiff<T>(a: T[], b: T[]): { added: T[], removed: T[] } {
removed: a.filter(i => !b.includes(i)), removed: a.filter(i => !b.includes(i)),
}; };
} }
/**
* Helper functions to perform LINQ-like queries on arrays.
*/
export class ArrayUtil<T> {
/**
* 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<K>(fn: (a: T) => K): GroupedArray<K, T> {
const obj = this.a.reduce((rv: Map<K, T[]>, val: T) => {
const k = fn(val);
if (!rv.has(k)) rv.set(k, []);
rv.get(k).push(val);
return rv;
}, new Map<K, T[]>());
return new GroupedArray(obj);
}
}
/**
* Helper functions to perform LINQ-like queries on groups (maps).
*/
export class GroupedArray<K, T> {
/**
* Creates a new group helper.
* @param val The group to help. Can be modified in-place.
*/
constructor(private val: Map<K, T[]>) {
}
/**
* 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<T> {
const a: T[] = [];
for (const k of keyOrder) {
if (!this.val.has(k)) continue;
a.push(...this.val.get(k));
}
return new ArrayUtil(a);
}
}

29
src/utils/enums.ts Normal file
View File

@ -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<T>(e: any): T[] {
const keys = Object.keys(e);
return keys
.filter(k => ['string', 'number'].includes(typeof(e[k])))
.map(k => e[k]);
}