Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Support prioritized room list filters #4737

Merged
merged 2 commits into from
Jun 9, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 21 additions & 7 deletions src/stores/room-list/algorithms/list-ordering/Algorithm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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?

Expand Down Expand Up @@ -184,30 +186,42 @@ export abstract class Algorithm extends EventEmitter {
}

console.warn("Recalculating filtered room list");
const allowedByFilters = new Set<Room>();
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);
}
}
newMap[tagId] = allowedRoomsInThisTag;
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.emit(LIST_UPDATED_EVENT);
}
Expand Down
7 changes: 6 additions & 1 deletion src/stores/room-list/filters/CommunityFilterCondition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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);
}
Expand Down
12 changes: 12 additions & 0 deletions src/stores/room-list/filters/IFilterCondition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand Down
7 changes: 6 additions & 1 deletion src/stores/room-list/filters/NameFilterCondition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

/**
Expand All @@ -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;
}
Expand Down
60 changes: 60 additions & 0 deletions src/utils/arrays.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,63 @@ export function arrayDiff<T>(a: T[], b: T[]): { added: T[], removed: T[] } {
removed: a.filter(i => !b.includes(i)),
};
}

/**
* Helper functions to perform LINQ-like queries on arrays.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see you're slowly implementing every C# / .NET Framework feature... 😜

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really wanted groupBy

*/
export class ArrayUtil<T> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Util sounds like a module of static functions rather than a class to me... Not sure what to suggest at the moment though.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Possibly LINQArray or Array2 to fully embrace .net bad practices. Or dump these on the Array prototype and forget that they're custom to the project

Copy link
Collaborator

@jryans jryans Jun 9, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yikes. 😨 Let's just leave it as-is for now, can change if we're inspired later.

/**
* 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);
}
}
27 changes: 27 additions & 0 deletions src/utils/enums.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
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.
*/

/**
* 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]);
}