Merge pull request #4681 from matrix-org/travis/room-list/filtering
Add initial filtering support to new room list
This commit is contained in:
commit
380ba163e4
14 changed files with 541 additions and 27 deletions
2
src/@types/global.d.ts
vendored
2
src/@types/global.d.ts
vendored
|
@ -19,6 +19,7 @@ import ContentMessages from "../ContentMessages";
|
||||||
import { IMatrixClientPeg } from "../MatrixClientPeg";
|
import { IMatrixClientPeg } from "../MatrixClientPeg";
|
||||||
import ToastStore from "../stores/ToastStore";
|
import ToastStore from "../stores/ToastStore";
|
||||||
import DeviceListener from "../DeviceListener";
|
import DeviceListener from "../DeviceListener";
|
||||||
|
import { RoomListStore2 } from "../stores/room-list/RoomListStore2";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
|
@ -31,6 +32,7 @@ declare global {
|
||||||
mx_ContentMessages: ContentMessages;
|
mx_ContentMessages: ContentMessages;
|
||||||
mx_ToastStore: ToastStore;
|
mx_ToastStore: ToastStore;
|
||||||
mx_DeviceListener: DeviceListener;
|
mx_DeviceListener: DeviceListener;
|
||||||
|
mx_RoomListStore2: RoomListStore2;
|
||||||
}
|
}
|
||||||
|
|
||||||
// workaround for https://github.com/microsoft/TypeScript/issues/30933
|
// workaround for https://github.com/microsoft/TypeScript/issues/30933
|
||||||
|
|
|
@ -28,6 +28,8 @@ import { Dispatcher } from "flux";
|
||||||
import dis from "../../../dispatcher/dispatcher";
|
import dis from "../../../dispatcher/dispatcher";
|
||||||
import RoomSublist2 from "./RoomSublist2";
|
import RoomSublist2 from "./RoomSublist2";
|
||||||
import { ActionPayload } from "../../../dispatcher/payloads";
|
import { ActionPayload } from "../../../dispatcher/payloads";
|
||||||
|
import { IFilterCondition } from "../../../stores/room-list/filters/IFilterCondition";
|
||||||
|
import { NameFilterCondition } from "../../../stores/room-list/filters/NameFilterCondition";
|
||||||
|
|
||||||
/*******************************************************************
|
/*******************************************************************
|
||||||
* CAUTION *
|
* CAUTION *
|
||||||
|
@ -130,6 +132,7 @@ export default class RoomList2 extends React.Component<IProps, IState> {
|
||||||
private sublistCollapseStates: { [tagId: string]: boolean } = {};
|
private sublistCollapseStates: { [tagId: string]: boolean } = {};
|
||||||
private unfilteredLayout: Layout;
|
private unfilteredLayout: Layout;
|
||||||
private filteredLayout: Layout;
|
private filteredLayout: Layout;
|
||||||
|
private searchFilter: NameFilterCondition = new NameFilterCondition();
|
||||||
|
|
||||||
constructor(props: IProps) {
|
constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
@ -139,6 +142,21 @@ export default class RoomList2 extends React.Component<IProps, IState> {
|
||||||
this.prepareLayouts();
|
this.prepareLayouts();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public componentDidUpdate(prevProps: Readonly<IProps>): void {
|
||||||
|
if (prevProps.searchFilter !== this.props.searchFilter) {
|
||||||
|
const hadSearch = !!this.searchFilter.search.trim();
|
||||||
|
const haveSearch = !!this.props.searchFilter.trim();
|
||||||
|
this.searchFilter.search = this.props.searchFilter;
|
||||||
|
if (!hadSearch && haveSearch) {
|
||||||
|
// started a new filter - add the condition
|
||||||
|
RoomListStore.instance.addFilter(this.searchFilter);
|
||||||
|
} else if (hadSearch && !haveSearch) {
|
||||||
|
// cleared a filter - remove the condition
|
||||||
|
RoomListStore.instance.removeFilter(this.searchFilter);
|
||||||
|
} // else the filter hasn't changed enough for us to care here
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public componentDidMount(): void {
|
public componentDidMount(): void {
|
||||||
RoomListStore.instance.on(LISTS_UPDATE_EVENT, (store) => {
|
RoomListStore.instance.on(LISTS_UPDATE_EVENT, (store) => {
|
||||||
console.log("new lists", store.orderedLists);
|
console.log("new lists", store.orderedLists);
|
||||||
|
|
|
@ -111,6 +111,21 @@ an object containing the tags it needs to worry about and the rooms within. The
|
||||||
decide which tags need rendering (as it commonly filters out empty tags in most cases), and will deal with
|
decide which tags need rendering (as it commonly filters out empty tags in most cases), and will deal with
|
||||||
all kinds of filtering.
|
all kinds of filtering.
|
||||||
|
|
||||||
|
## Filtering
|
||||||
|
|
||||||
|
Filters are provided to the store as condition classes, which are then passed along to the algorithm
|
||||||
|
implementations. The implementations then get to decide how to actually filter the rooms, however in
|
||||||
|
practice the base `Algorithm` class deals with the filtering in a more optimized/generic way.
|
||||||
|
|
||||||
|
The results of filters get cached to avoid needlessly iterating over potentially thousands of rooms,
|
||||||
|
as the old room list store does. When a filter condition changes, it emits an update which (in this
|
||||||
|
case) the `Algorithm` class will pick up and act accordingly. Typically, this also means filtering a
|
||||||
|
minor subset where possible to avoid over-iterating rooms.
|
||||||
|
|
||||||
|
All filter conditions are considered "stable" by the consumers, meaning that the consumer does not
|
||||||
|
expect a change in the condition unless the condition says it has changed. This is intentional to
|
||||||
|
maintain the caching behaviour described above.
|
||||||
|
|
||||||
## Class breakdowns
|
## Class breakdowns
|
||||||
|
|
||||||
The `RoomListStore` is the major coordinator of various `Algorithm` implementations, which take care
|
The `RoomListStore` is the major coordinator of various `Algorithm` implementations, which take care
|
||||||
|
|
|
@ -18,7 +18,7 @@ limitations under the License.
|
||||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
import SettingsStore from "../../settings/SettingsStore";
|
import SettingsStore from "../../settings/SettingsStore";
|
||||||
import { DefaultTagID, OrderedDefaultTagIDs, RoomUpdateCause, TagID } from "./models";
|
import { DefaultTagID, OrderedDefaultTagIDs, RoomUpdateCause, TagID } from "./models";
|
||||||
import { Algorithm } from "./algorithms/list-ordering/Algorithm";
|
import { Algorithm, LIST_UPDATED_EVENT } from "./algorithms/list-ordering/Algorithm";
|
||||||
import TagOrderStore from "../TagOrderStore";
|
import TagOrderStore from "../TagOrderStore";
|
||||||
import { AsyncStore } from "../AsyncStore";
|
import { AsyncStore } from "../AsyncStore";
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
@ -27,6 +27,8 @@ import { getListAlgorithmInstance } from "./algorithms/list-ordering";
|
||||||
import { ActionPayload } from "../../dispatcher/payloads";
|
import { ActionPayload } from "../../dispatcher/payloads";
|
||||||
import defaultDispatcher from "../../dispatcher/dispatcher";
|
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||||
import { readReceiptChangeIsFor } from "../../utils/read-receipts";
|
import { readReceiptChangeIsFor } from "../../utils/read-receipts";
|
||||||
|
import { IFilterCondition } from "./filters/IFilterCondition";
|
||||||
|
import { TagWatcher } from "./TagWatcher";
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
tagsEnabled?: boolean;
|
tagsEnabled?: boolean;
|
||||||
|
@ -41,11 +43,13 @@ interface IState {
|
||||||
*/
|
*/
|
||||||
export const LISTS_UPDATE_EVENT = "lists_update";
|
export const LISTS_UPDATE_EVENT = "lists_update";
|
||||||
|
|
||||||
class _RoomListStore extends AsyncStore<ActionPayload> {
|
export class RoomListStore2 extends AsyncStore<ActionPayload> {
|
||||||
private matrixClient: MatrixClient;
|
private _matrixClient: MatrixClient;
|
||||||
private initialListsGenerated = false;
|
private initialListsGenerated = false;
|
||||||
private enabled = false;
|
private enabled = false;
|
||||||
private algorithm: Algorithm;
|
private algorithm: Algorithm;
|
||||||
|
private filterConditions: IFilterCondition[] = [];
|
||||||
|
private tagWatcher = new TagWatcher(this);
|
||||||
|
|
||||||
private readonly watchedSettings = [
|
private readonly watchedSettings = [
|
||||||
'RoomList.orderAlphabetically',
|
'RoomList.orderAlphabetically',
|
||||||
|
@ -65,6 +69,10 @@ class _RoomListStore extends AsyncStore<ActionPayload> {
|
||||||
return this.algorithm.getOrderedRooms();
|
return this.algorithm.getOrderedRooms();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get matrixClient(): MatrixClient {
|
||||||
|
return this._matrixClient;
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Remove enabled flag when the old RoomListStore goes away
|
// TODO: Remove enabled flag when the old RoomListStore goes away
|
||||||
private checkEnabled() {
|
private checkEnabled() {
|
||||||
this.enabled = SettingsStore.isFeatureEnabled("feature_new_room_list");
|
this.enabled = SettingsStore.isFeatureEnabled("feature_new_room_list");
|
||||||
|
@ -96,7 +104,7 @@ class _RoomListStore extends AsyncStore<ActionPayload> {
|
||||||
this.checkEnabled();
|
this.checkEnabled();
|
||||||
if (!this.enabled) return;
|
if (!this.enabled) return;
|
||||||
|
|
||||||
this.matrixClient = payload.matrixClient;
|
this._matrixClient = payload.matrixClient;
|
||||||
|
|
||||||
// Update any settings here, as some may have happened before we were logically ready.
|
// Update any settings here, as some may have happened before we were logically ready.
|
||||||
console.log("Regenerating room lists: Startup");
|
console.log("Regenerating room lists: Startup");
|
||||||
|
@ -111,7 +119,7 @@ class _RoomListStore extends AsyncStore<ActionPayload> {
|
||||||
// Reset state without causing updates as the client will have been destroyed
|
// Reset state without causing updates as the client will have been destroyed
|
||||||
// and downstream code will throw NPE errors.
|
// and downstream code will throw NPE errors.
|
||||||
this.reset(null, true);
|
this.reset(null, true);
|
||||||
this.matrixClient = null;
|
this._matrixClient = null;
|
||||||
this.initialListsGenerated = false; // we'll want to regenerate them
|
this.initialListsGenerated = false; // we'll want to regenerate them
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -152,8 +160,21 @@ class _RoomListStore extends AsyncStore<ActionPayload> {
|
||||||
|
|
||||||
const roomId = eventPayload.event.getRoomId();
|
const roomId = eventPayload.event.getRoomId();
|
||||||
const room = this.matrixClient.getRoom(roomId);
|
const room = this.matrixClient.getRoom(roomId);
|
||||||
console.log(`[RoomListDebug] Live timeline event ${eventPayload.event.getId()} in ${roomId}`);
|
const tryUpdate = async (updatedRoom: Room) => {
|
||||||
await this.handleRoomUpdate(room, RoomUpdateCause.Timeline);
|
console.log(`[RoomListDebug] Live timeline event ${eventPayload.event.getId()} in ${updatedRoom.roomId}`);
|
||||||
|
await this.handleRoomUpdate(updatedRoom, RoomUpdateCause.Timeline);
|
||||||
|
};
|
||||||
|
if (!room) {
|
||||||
|
console.warn(`Live timeline event ${eventPayload.event.getId()} received without associated room`);
|
||||||
|
console.warn(`Queuing failed room update for retry as a result.`);
|
||||||
|
setTimeout(async () => {
|
||||||
|
const updatedRoom = this.matrixClient.getRoom(roomId);
|
||||||
|
await tryUpdate(updatedRoom);
|
||||||
|
}, 100); // 100ms should be enough for the room to show up
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
await tryUpdate(room);
|
||||||
|
}
|
||||||
} else if (payload.action === 'MatrixActions.Event.decrypted') {
|
} else if (payload.action === 'MatrixActions.Event.decrypted') {
|
||||||
const eventPayload = (<any>payload); // TODO: Type out the dispatcher types
|
const eventPayload = (<any>payload); // TODO: Type out the dispatcher types
|
||||||
const roomId = eventPayload.event.getRoomId();
|
const roomId = eventPayload.event.getRoomId();
|
||||||
|
@ -171,11 +192,20 @@ class _RoomListStore extends AsyncStore<ActionPayload> {
|
||||||
// TODO: Update DMs
|
// TODO: Update DMs
|
||||||
console.log(payload);
|
console.log(payload);
|
||||||
} else if (payload.action === 'MatrixActions.Room.myMembership') {
|
} else if (payload.action === 'MatrixActions.Room.myMembership') {
|
||||||
|
// TODO: Improve new room check
|
||||||
|
const membershipPayload = (<any>payload); // TODO: Type out the dispatcher types
|
||||||
|
if (!membershipPayload.oldMembership && membershipPayload.membership === "join") {
|
||||||
|
console.log(`[RoomListDebug] Handling new room ${membershipPayload.room.roomId}`);
|
||||||
|
await this.algorithm.handleRoomUpdate(membershipPayload.room, RoomUpdateCause.NewRoom);
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Update room from membership change
|
// TODO: Update room from membership change
|
||||||
console.log(payload);
|
console.log(payload);
|
||||||
} else if (payload.action === 'MatrixActions.Room') {
|
} else if (payload.action === 'MatrixActions.Room') {
|
||||||
// TODO: Update room from creation/join
|
// TODO: Improve new room check
|
||||||
console.log(payload);
|
// const roomPayload = (<any>payload); // TODO: Type out the dispatcher types
|
||||||
|
// console.log(`[RoomListDebug] Handling new room ${roomPayload.room.roomId}`);
|
||||||
|
// await this.algorithm.handleRoomUpdate(roomPayload.room, RoomUpdateCause.NewRoom);
|
||||||
} else if (payload.action === 'view_room') {
|
} else if (payload.action === 'view_room') {
|
||||||
// TODO: Update sticky room
|
// TODO: Update sticky room
|
||||||
console.log(payload);
|
console.log(payload);
|
||||||
|
@ -211,11 +241,22 @@ class _RoomListStore extends AsyncStore<ActionPayload> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private setAlgorithmClass() {
|
private setAlgorithmClass() {
|
||||||
|
if (this.algorithm) {
|
||||||
|
this.algorithm.off(LIST_UPDATED_EVENT, this.onAlgorithmListUpdated);
|
||||||
|
}
|
||||||
this.algorithm = getListAlgorithmInstance(this.state.preferredAlgorithm);
|
this.algorithm = getListAlgorithmInstance(this.state.preferredAlgorithm);
|
||||||
|
this.algorithm.setFilterConditions(this.filterConditions);
|
||||||
|
this.algorithm.on(LIST_UPDATED_EVENT, this.onAlgorithmListUpdated);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private onAlgorithmListUpdated = () => {
|
||||||
|
console.log("Underlying algorithm has triggered a list update - refiring");
|
||||||
|
this.emit(LISTS_UPDATE_EVENT, this);
|
||||||
|
};
|
||||||
|
|
||||||
private async regenerateAllLists() {
|
private async regenerateAllLists() {
|
||||||
console.warn("Regenerating all room lists");
|
console.warn("Regenerating all room lists");
|
||||||
|
|
||||||
const tags: ITagSortingMap = {};
|
const tags: ITagSortingMap = {};
|
||||||
for (const tagId of OrderedDefaultTagIDs) {
|
for (const tagId of OrderedDefaultTagIDs) {
|
||||||
tags[tagId] = this.getSortAlgorithmFor(tagId);
|
tags[tagId] = this.getSortAlgorithmFor(tagId);
|
||||||
|
@ -234,16 +275,38 @@ class _RoomListStore extends AsyncStore<ActionPayload> {
|
||||||
|
|
||||||
this.emit(LISTS_UPDATE_EVENT, this);
|
this.emit(LISTS_UPDATE_EVENT, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public addFilter(filter: IFilterCondition): void {
|
||||||
|
console.log("Adding filter condition:", filter);
|
||||||
|
this.filterConditions.push(filter);
|
||||||
|
if (this.algorithm) {
|
||||||
|
this.algorithm.addFilterCondition(filter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public removeFilter(filter: IFilterCondition): void {
|
||||||
|
console.log("Removing filter condition:", filter);
|
||||||
|
const idx = this.filterConditions.indexOf(filter);
|
||||||
|
if (idx >= 0) {
|
||||||
|
this.filterConditions.splice(idx, 1);
|
||||||
|
|
||||||
|
if (this.algorithm) {
|
||||||
|
this.algorithm.removeFilterCondition(filter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class RoomListStore {
|
export default class RoomListStore {
|
||||||
private static internalInstance: _RoomListStore;
|
private static internalInstance: RoomListStore2;
|
||||||
|
|
||||||
public static get instance(): _RoomListStore {
|
public static get instance(): RoomListStore2 {
|
||||||
if (!RoomListStore.internalInstance) {
|
if (!RoomListStore.internalInstance) {
|
||||||
RoomListStore.internalInstance = new _RoomListStore();
|
RoomListStore.internalInstance = new RoomListStore2();
|
||||||
}
|
}
|
||||||
|
|
||||||
return RoomListStore.internalInstance;
|
return RoomListStore.internalInstance;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
window.mx_RoomListStore2 = RoomListStore.instance;
|
||||||
|
|
80
src/stores/room-list/TagWatcher.ts
Normal file
80
src/stores/room-list/TagWatcher.ts
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
/*
|
||||||
|
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 { RoomListStore2 } from "./RoomListStore2";
|
||||||
|
import TagOrderStore from "../TagOrderStore";
|
||||||
|
import { CommunityFilterCondition } from "./filters/CommunityFilterCondition";
|
||||||
|
import { arrayDiff, arrayHasDiff } from "../../utils/arrays";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Watches for changes in tags/groups to manage filters on the provided RoomListStore
|
||||||
|
*/
|
||||||
|
export class TagWatcher {
|
||||||
|
// TODO: Support custom tags, somehow (deferred to later work - need support elsewhere)
|
||||||
|
private filters = new Map<string, CommunityFilterCondition>();
|
||||||
|
|
||||||
|
constructor(private store: RoomListStore2) {
|
||||||
|
TagOrderStore.addListener(this.onTagsUpdated);
|
||||||
|
}
|
||||||
|
|
||||||
|
private onTagsUpdated = () => {
|
||||||
|
const lastTags = Array.from(this.filters.keys());
|
||||||
|
const newTags = TagOrderStore.getSelectedTags();
|
||||||
|
|
||||||
|
if (arrayHasDiff(lastTags, newTags)) {
|
||||||
|
// Selected tags changed, do some filtering
|
||||||
|
|
||||||
|
if (!this.store.matrixClient) {
|
||||||
|
console.warn("Tag update without an associated matrix client - ignoring");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newFilters = new Map<string, CommunityFilterCondition>();
|
||||||
|
|
||||||
|
// TODO: Support custom tags properly
|
||||||
|
const filterableTags = newTags.filter(t => t.startsWith("+"));
|
||||||
|
|
||||||
|
for (const tag of filterableTags) {
|
||||||
|
const group = this.store.matrixClient.getGroup(tag);
|
||||||
|
if (!group) {
|
||||||
|
console.warn(`Group selected with no group object available: ${tag}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
newFilters.set(tag, new CommunityFilterCondition(group));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the room list store's filters
|
||||||
|
const diff = arrayDiff(lastTags, newTags);
|
||||||
|
for (const tag of diff.added) {
|
||||||
|
// TODO: Remove this check when custom tags are supported (as we shouldn't be losing filters)
|
||||||
|
const filter = newFilters.get(tag);
|
||||||
|
if (!filter) continue;
|
||||||
|
|
||||||
|
this.store.addFilter(filter);
|
||||||
|
}
|
||||||
|
for (const tag of diff.removed) {
|
||||||
|
// TODO: Remove this check when custom tags are supported (as we shouldn't be losing filters)
|
||||||
|
const filter = this.filters.get(tag);
|
||||||
|
if (!filter) continue;
|
||||||
|
|
||||||
|
this.store.removeFilter(filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.filters = newFilters;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
|
@ -20,24 +20,138 @@ 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 { EventEmitter } from "events";
|
||||||
|
|
||||||
// TODO: Add locking support to avoid concurrent writes?
|
// TODO: Add locking support to avoid concurrent writes?
|
||||||
// TODO: EventEmitter support? Might not be needed.
|
|
||||||
|
/**
|
||||||
|
* Fired when the Algorithm has determined a list has been updated.
|
||||||
|
*/
|
||||||
|
export const LIST_UPDATED_EVENT = "list_updated_event";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a list ordering algorithm. This class will take care of tag
|
* Represents a list ordering algorithm. This class will take care of tag
|
||||||
* management (which rooms go in which tags) and ask the implementation to
|
* management (which rooms go in which tags) and ask the implementation to
|
||||||
* deal with ordering mechanics.
|
* deal with ordering mechanics.
|
||||||
*/
|
*/
|
||||||
export abstract class Algorithm {
|
export abstract class Algorithm extends EventEmitter {
|
||||||
protected cached: ITagMap = {};
|
private _cachedRooms: ITagMap = {};
|
||||||
|
private filteredRooms: ITagMap = {};
|
||||||
|
|
||||||
protected sortAlgorithms: ITagSortingMap;
|
protected sortAlgorithms: ITagSortingMap;
|
||||||
protected rooms: Room[] = [];
|
protected rooms: Room[] = [];
|
||||||
protected roomIdsToTags: {
|
protected roomIdsToTags: {
|
||||||
[roomId: string]: TagID[];
|
[roomId: string]: TagID[];
|
||||||
} = {};
|
} = {};
|
||||||
|
protected allowedByFilter: Map<IFilterCondition, Room[]> = new Map<IFilterCondition, Room[]>();
|
||||||
|
protected allowedRoomsByFilters: Set<Room> = new Set<Room>();
|
||||||
|
|
||||||
protected constructor() {
|
protected constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected get hasFilters(): boolean {
|
||||||
|
return this.allowedByFilter.size > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected set cachedRooms(val: ITagMap) {
|
||||||
|
this._cachedRooms = val;
|
||||||
|
this.recalculateFilteredRooms();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected get cachedRooms(): ITagMap {
|
||||||
|
return this._cachedRooms;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the filter conditions the Algorithm should use.
|
||||||
|
* @param filterConditions The filter conditions to use.
|
||||||
|
*/
|
||||||
|
public setFilterConditions(filterConditions: IFilterCondition[]): void {
|
||||||
|
for (const filter of filterConditions) {
|
||||||
|
this.addFilterCondition(filter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public addFilterCondition(filterCondition: IFilterCondition): void {
|
||||||
|
// Populate the cache of the new filter
|
||||||
|
this.allowedByFilter.set(filterCondition, this.rooms.filter(r => filterCondition.isVisible(r)));
|
||||||
|
this.recalculateFilteredRooms();
|
||||||
|
filterCondition.on(FILTER_CHANGED, this.recalculateFilteredRooms.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
public removeFilterCondition(filterCondition: IFilterCondition): void {
|
||||||
|
filterCondition.off(FILTER_CHANGED, this.recalculateFilteredRooms.bind(this));
|
||||||
|
if (this.allowedByFilter.has(filterCondition)) {
|
||||||
|
this.allowedByFilter.delete(filterCondition);
|
||||||
|
|
||||||
|
// If we removed the last filter, tell consumers that we've "updated" our filtered
|
||||||
|
// view. This will trick them into getting the complete room list.
|
||||||
|
if (!this.hasFilters) {
|
||||||
|
this.emit(LIST_UPDATED_EVENT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected recalculateFilteredRooms() {
|
||||||
|
if (!this.hasFilters) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.warn("Recalculating filtered room list");
|
||||||
|
const allowedByFilters = new Set<Room>();
|
||||||
|
const filters = Array.from(this.allowedByFilter.keys());
|
||||||
|
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) {
|
||||||
|
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;
|
||||||
|
this.filteredRooms = newMap;
|
||||||
|
this.emit(LIST_UPDATED_EVENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected addPossiblyFilteredRoomsToTag(tagId: TagID, added: Room[]): void {
|
||||||
|
const filters = this.allowedByFilter.keys();
|
||||||
|
for (const room of added) {
|
||||||
|
for (const filter of filters) {
|
||||||
|
if (filter.isVisible(room)) {
|
||||||
|
this.allowedRoomsByFilters.add(room);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now that we've updated the allowed rooms, recalculate the tag
|
||||||
|
this.recalculateFilteredRoomsForTag(tagId);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected recalculateFilteredRoomsForTag(tagId: TagID): void {
|
||||||
|
console.log(`Recalculating filtered rooms for ${tagId}`);
|
||||||
|
delete this.filteredRooms[tagId];
|
||||||
|
const rooms = this.cachedRooms[tagId];
|
||||||
|
const filteredRooms = rooms.filter(r => this.allowedRoomsByFilters.has(r));
|
||||||
|
if (filteredRooms.length > 0) {
|
||||||
|
this.filteredRooms[tagId] = filteredRooms;
|
||||||
|
}
|
||||||
|
console.log(`[DEBUG] ${filteredRooms.length}/${rooms.length} rooms filtered into ${tagId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -54,12 +168,15 @@ export abstract class Algorithm {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets an ordered set of rooms for the all known tags.
|
* Gets an ordered set of rooms for the all known tags, filtered.
|
||||||
* @returns {ITagMap} The cached list of rooms, ordered,
|
* @returns {ITagMap} The cached list of rooms, ordered,
|
||||||
* for each tag. May be empty, but never null/undefined.
|
* for each tag. May be empty, but never null/undefined.
|
||||||
*/
|
*/
|
||||||
public getOrderedRooms(): ITagMap {
|
public getOrderedRooms(): ITagMap {
|
||||||
return this.cached;
|
if (!this.hasFilters) {
|
||||||
|
return this.cachedRooms;
|
||||||
|
}
|
||||||
|
return this.filteredRooms;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -83,7 +200,7 @@ export abstract class Algorithm {
|
||||||
// If we can avoid doing work, do so.
|
// If we can avoid doing work, do so.
|
||||||
if (!rooms.length) {
|
if (!rooms.length) {
|
||||||
await this.generateFreshTags(newTags); // just in case it wants to do something
|
await this.generateFreshTags(newTags); // just in case it wants to do something
|
||||||
this.cached = newTags;
|
this.cachedRooms = newTags;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -130,7 +247,7 @@ export abstract class Algorithm {
|
||||||
|
|
||||||
await this.generateFreshTags(newTags);
|
await this.generateFreshTags(newTags);
|
||||||
|
|
||||||
this.cached = newTags;
|
this.cachedRooms = newTags;
|
||||||
this.updateTagsFromCache();
|
this.updateTagsFromCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -140,9 +257,9 @@ export abstract class Algorithm {
|
||||||
protected updateTagsFromCache() {
|
protected updateTagsFromCache() {
|
||||||
const newMap = {};
|
const newMap = {};
|
||||||
|
|
||||||
const tags = Object.keys(this.cached);
|
const tags = Object.keys(this.cachedRooms);
|
||||||
for (const tagId of tags) {
|
for (const tagId of tags) {
|
||||||
const rooms = this.cached[tagId];
|
const rooms = this.cachedRooms[tagId];
|
||||||
for (const room of rooms) {
|
for (const room of rooms) {
|
||||||
if (!newMap[room.roomId]) newMap[room.roomId] = [];
|
if (!newMap[room.roomId]) newMap[room.roomId] = [];
|
||||||
newMap[room.roomId].push(tagId);
|
newMap[room.roomId].push(tagId);
|
||||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
||||||
|
|
||||||
import { Algorithm } from "./Algorithm";
|
import { Algorithm } from "./Algorithm";
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import { RoomUpdateCause, TagID } from "../../models";
|
import { DefaultTagID, RoomUpdateCause, TagID } from "../../models";
|
||||||
import { ITagMap, SortAlgorithm } from "../models";
|
import { ITagMap, SortAlgorithm } from "../models";
|
||||||
import { sortRoomsWithAlgorithm } from "../tag-sorting";
|
import { sortRoomsWithAlgorithm } from "../tag-sorting";
|
||||||
import * as Unread from '../../../../Unread';
|
import * as Unread from '../../../../Unread';
|
||||||
|
@ -92,9 +92,9 @@ export class ImportanceAlgorithm extends Algorithm {
|
||||||
// can be found from `this.indices[tag][category]` and the sticky room information
|
// can be found from `this.indices[tag][category]` and the sticky room information
|
||||||
// from `this.stickyRoom`.
|
// from `this.stickyRoom`.
|
||||||
//
|
//
|
||||||
// The room list store is always provided with the `this.cached` results, which are
|
// The room list store is always provided with the `this.cachedRooms` results, which are
|
||||||
// updated as needed and not recalculated often. For example, when a room needs to
|
// updated as needed and not recalculated often. For example, when a room needs to
|
||||||
// move within a tag, the array in `this.cached` will be spliced instead of iterated.
|
// move within a tag, the array in `this.cachedRooms` will be spliced instead of iterated.
|
||||||
// The `indices` help track the positions of each category to make splicing easier.
|
// The `indices` help track the positions of each category to make splicing easier.
|
||||||
|
|
||||||
private indices: {
|
private indices: {
|
||||||
|
@ -189,7 +189,13 @@ export class ImportanceAlgorithm extends Algorithm {
|
||||||
}
|
}
|
||||||
|
|
||||||
public async handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise<boolean> {
|
public async handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise<boolean> {
|
||||||
const tags = this.roomIdsToTags[room.roomId];
|
if (cause === RoomUpdateCause.NewRoom) {
|
||||||
|
// TODO: Be smarter and insert rather than regen the planet.
|
||||||
|
await this.setKnownRooms([room, ...this.rooms]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let tags = this.roomIdsToTags[room.roomId];
|
||||||
if (!tags) {
|
if (!tags) {
|
||||||
console.warn(`No tags known for "${room.name}" (${room.roomId})`);
|
console.warn(`No tags known for "${room.name}" (${room.roomId})`);
|
||||||
return false;
|
return false;
|
||||||
|
@ -201,7 +207,7 @@ export class ImportanceAlgorithm extends Algorithm {
|
||||||
continue; // Nothing to do here.
|
continue; // Nothing to do here.
|
||||||
}
|
}
|
||||||
|
|
||||||
const taggedRooms = this.cached[tag];
|
const taggedRooms = this.cachedRooms[tag];
|
||||||
const indices = this.indices[tag];
|
const indices = this.indices[tag];
|
||||||
let roomIdx = taggedRooms.indexOf(room);
|
let roomIdx = taggedRooms.indexOf(room);
|
||||||
if (roomIdx === -1) {
|
if (roomIdx === -1) {
|
||||||
|
|
|
@ -49,7 +49,7 @@ export class NaturalAlgorithm extends Algorithm {
|
||||||
for (const tag of tags) {
|
for (const tag of tags) {
|
||||||
// TODO: Optimize this loop to avoid useless operations
|
// TODO: Optimize this loop to avoid useless operations
|
||||||
// For example, we can skip updates to alphabetic (sometimes) and manually ordered tags
|
// For example, we can skip updates to alphabetic (sometimes) and manually ordered tags
|
||||||
this.cached[tag] = await sortRoomsWithAlgorithm(this.cached[tag], tag, this.sortAlgorithms[tag]);
|
this.cachedRooms[tag] = await sortRoomsWithAlgorithm(this.cachedRooms[tag], tag, this.sortAlgorithms[tag]);
|
||||||
}
|
}
|
||||||
return true; // assume we changed something
|
return true; // assume we changed something
|
||||||
}
|
}
|
||||||
|
|
58
src/stores/room-list/filters/CommunityFilterCondition.ts
Normal file
58
src/stores/room-list/filters/CommunityFilterCondition.ts
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
/*
|
||||||
|
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 { FILTER_CHANGED, IFilterCondition } from "./IFilterCondition";
|
||||||
|
import { Group } from "matrix-js-sdk/src/models/group";
|
||||||
|
import { EventEmitter } from "events";
|
||||||
|
import GroupStore from "../../GroupStore";
|
||||||
|
import { arrayHasDiff } from "../../../utils/arrays";
|
||||||
|
import { IDisposable } from "../../../utils/IDisposable";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A filter condition for the room list which reveals rooms which
|
||||||
|
* are a member of a given community.
|
||||||
|
*/
|
||||||
|
export class CommunityFilterCondition extends EventEmitter implements IFilterCondition, IDisposable {
|
||||||
|
private roomIds: string[] = [];
|
||||||
|
|
||||||
|
constructor(private community: Group) {
|
||||||
|
super();
|
||||||
|
GroupStore.on("update", this.onStoreUpdate);
|
||||||
|
|
||||||
|
// noinspection JSIgnoredPromiseFromCall
|
||||||
|
this.onStoreUpdate(); // trigger a false update to seed the store
|
||||||
|
}
|
||||||
|
|
||||||
|
public isVisible(room: Room): boolean {
|
||||||
|
return this.roomIds.includes(room.roomId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private onStoreUpdate = async (): Promise<any> => {
|
||||||
|
// We don't actually know if the room list changed for the community, so just
|
||||||
|
// check it again.
|
||||||
|
const beforeRoomIds = this.roomIds;
|
||||||
|
this.roomIds = (await GroupStore.getGroupRooms(this.community.groupId)).map(r => r.roomId);
|
||||||
|
if (arrayHasDiff(beforeRoomIds, this.roomIds)) {
|
||||||
|
console.log("Updating filter for group: ", this.community.groupId);
|
||||||
|
this.emit(FILTER_CHANGED);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public dispose(): void {
|
||||||
|
GroupStore.off("update", this.onStoreUpdate);
|
||||||
|
}
|
||||||
|
}
|
42
src/stores/room-list/filters/IFilterCondition.ts
Normal file
42
src/stores/room-list/filters/IFilterCondition.ts
Normal file
|
@ -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 { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
import { EventEmitter } from "events";
|
||||||
|
|
||||||
|
export const FILTER_CHANGED = "filter_changed";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A filter condition for the room list, determining if a room
|
||||||
|
* should be shown or not.
|
||||||
|
*
|
||||||
|
* All filter conditions are expected to be stable executions,
|
||||||
|
* meaning that given the same input the same answer will be
|
||||||
|
* returned (thus allowing caching). As such, filter conditions
|
||||||
|
* can, but shouldn't, do heavier logic and not worry about being
|
||||||
|
* called constantly by the room list. When the condition changes
|
||||||
|
* such that different inputs lead to different answers (such
|
||||||
|
* as a change in the user's input), this emits FILTER_CHANGED.
|
||||||
|
*/
|
||||||
|
export interface IFilterCondition extends EventEmitter {
|
||||||
|
/**
|
||||||
|
* Determines if a given room should be visible under this
|
||||||
|
* condition.
|
||||||
|
* @param room The room to check.
|
||||||
|
* @returns True if the room should be visible.
|
||||||
|
*/
|
||||||
|
isVisible(room: Room): boolean;
|
||||||
|
}
|
46
src/stores/room-list/filters/NameFilterCondition.ts
Normal file
46
src/stores/room-list/filters/NameFilterCondition.ts
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
/*
|
||||||
|
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 { FILTER_CHANGED, IFilterCondition } from "./IFilterCondition";
|
||||||
|
import { EventEmitter } from "events";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A filter condition for the room list which reveals rooms of a particular
|
||||||
|
* name, or associated name (like a room alias).
|
||||||
|
*/
|
||||||
|
export class NameFilterCondition extends EventEmitter implements IFilterCondition {
|
||||||
|
private _search = "";
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public get search(): string {
|
||||||
|
return this._search;
|
||||||
|
}
|
||||||
|
|
||||||
|
public set search(val: string) {
|
||||||
|
this._search = val;
|
||||||
|
console.log("Updating filter for room name search:", this._search);
|
||||||
|
this.emit(FILTER_CHANGED);
|
||||||
|
}
|
||||||
|
|
||||||
|
public isVisible(room: Room): boolean {
|
||||||
|
// TODO: Improve this filter to include aliases and such
|
||||||
|
return room.name.toLowerCase().indexOf(this.search.toLowerCase()) >= 0;
|
||||||
|
}
|
||||||
|
}
|
|
@ -39,4 +39,5 @@ export type TagID = string | DefaultTagID;
|
||||||
export enum RoomUpdateCause {
|
export enum RoomUpdateCause {
|
||||||
Timeline = "TIMELINE",
|
Timeline = "TIMELINE",
|
||||||
RoomRead = "ROOM_READ", // TODO: Use this.
|
RoomRead = "ROOM_READ", // TODO: Use this.
|
||||||
|
NewRoom = "NEW_ROOM",
|
||||||
}
|
}
|
||||||
|
|
19
src/utils/IDisposable.ts
Normal file
19
src/utils/IDisposable.ts
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
/*
|
||||||
|
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 interface IDisposable {
|
||||||
|
dispose(): void;
|
||||||
|
}
|
47
src/utils/arrays.ts
Normal file
47
src/utils/arrays.ts
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
/*
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if two arrays are different through a shallow comparison.
|
||||||
|
* @param a The first array. Must be defined.
|
||||||
|
* @param b The second array. Must be defined.
|
||||||
|
* @returns True if they are the same, false otherwise.
|
||||||
|
*/
|
||||||
|
export function arrayHasDiff(a: any[], b: any[]): boolean {
|
||||||
|
if (a.length === b.length) {
|
||||||
|
// When the lengths are equal, check to see if either array is missing
|
||||||
|
// an element from the other.
|
||||||
|
if (b.some(i => !a.includes(i))) return true;
|
||||||
|
if (a.some(i => !b.includes(i))) return true;
|
||||||
|
} else {
|
||||||
|
return true; // different lengths means they are naturally diverged
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs a diff on two arrays. The result is what is different with the
|
||||||
|
* first array (`added` in the returned object means objects in B that aren't
|
||||||
|
* in A). Shallow comparisons are used to perform the diff.
|
||||||
|
* @param a The first array. Must be defined.
|
||||||
|
* @param b The second array. Must be defined.
|
||||||
|
* @returns The diff between the arrays.
|
||||||
|
*/
|
||||||
|
export function arrayDiff<T>(a: T[], b: T[]): { added: T[], removed: T[] } {
|
||||||
|
return {
|
||||||
|
added: b.filter(i => !a.includes(i)),
|
||||||
|
removed: a.filter(i => !b.includes(i)),
|
||||||
|
};
|
||||||
|
}
|
Loading…
Reference in a new issue