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

Add initial filtering support to new room list #4681

Merged
merged 2 commits into from
Jun 3, 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
2 changes: 2 additions & 0 deletions src/@types/global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import ContentMessages from "../ContentMessages";
import { IMatrixClientPeg } from "../MatrixClientPeg";
import ToastStore from "../stores/ToastStore";
import DeviceListener from "../DeviceListener";
import { RoomListStore2 } from "../stores/room-list/RoomListStore2";

declare global {
interface Window {
Expand All @@ -31,6 +32,7 @@ declare global {
mx_ContentMessages: ContentMessages;
mx_ToastStore: ToastStore;
mx_DeviceListener: DeviceListener;
mx_RoomListStore2: RoomListStore2;
}

// workaround for https://github.com/microsoft/TypeScript/issues/30933
Expand Down
18 changes: 18 additions & 0 deletions src/components/views/rooms/RoomList2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import { Dispatcher } from "flux";
import dis from "../../../dispatcher/dispatcher";
import RoomSublist2 from "./RoomSublist2";
import { ActionPayload } from "../../../dispatcher/payloads";
import { IFilterCondition } from "../../../stores/room-list/filters/IFilterCondition";
import { NameFilterCondition } from "../../../stores/room-list/filters/NameFilterCondition";

/*******************************************************************
* CAUTION *
Expand Down Expand Up @@ -130,6 +132,7 @@ export default class RoomList2 extends React.Component<IProps, IState> {
private sublistCollapseStates: { [tagId: string]: boolean } = {};
private unfilteredLayout: Layout;
private filteredLayout: Layout;
private searchFilter: NameFilterCondition = new NameFilterCondition();

constructor(props: IProps) {
super(props);
Expand All @@ -139,6 +142,21 @@ export default class RoomList2 extends React.Component<IProps, IState> {
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 {
RoomListStore.instance.on(LISTS_UPDATE_EVENT, (store) => {
console.log("new lists", store.orderedLists);
Expand Down
15 changes: 15 additions & 0 deletions src/stores/room-list/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
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

The `RoomListStore` is the major coordinator of various `Algorithm` implementations, which take care
Expand Down
87 changes: 75 additions & 12 deletions src/stores/room-list/RoomListStore2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ limitations under the License.
import { MatrixClient } from "matrix-js-sdk/src/client";
import SettingsStore from "../../settings/SettingsStore";
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 { AsyncStore } from "../AsyncStore";
import { Room } from "matrix-js-sdk/src/models/room";
Expand All @@ -27,6 +27,8 @@ import { getListAlgorithmInstance } from "./algorithms/list-ordering";
import { ActionPayload } from "../../dispatcher/payloads";
import defaultDispatcher from "../../dispatcher/dispatcher";
import { readReceiptChangeIsFor } from "../../utils/read-receipts";
import { IFilterCondition } from "./filters/IFilterCondition";
import { TagWatcher } from "./TagWatcher";

interface IState {
tagsEnabled?: boolean;
Expand All @@ -41,11 +43,13 @@ interface IState {
*/
export const LISTS_UPDATE_EVENT = "lists_update";

class _RoomListStore extends AsyncStore<ActionPayload> {
private matrixClient: MatrixClient;
export class RoomListStore2 extends AsyncStore<ActionPayload> {
private _matrixClient: MatrixClient;
private initialListsGenerated = false;
private enabled = false;
private algorithm: Algorithm;
private filterConditions: IFilterCondition[] = [];
private tagWatcher = new TagWatcher(this);

private readonly watchedSettings = [
'RoomList.orderAlphabetically',
Expand All @@ -65,6 +69,10 @@ class _RoomListStore extends AsyncStore<ActionPayload> {
return this.algorithm.getOrderedRooms();
}

public get matrixClient(): MatrixClient {
return this._matrixClient;
}

// TODO: Remove enabled flag when the old RoomListStore goes away
private checkEnabled() {
this.enabled = SettingsStore.isFeatureEnabled("feature_new_room_list");
Expand Down Expand Up @@ -96,7 +104,7 @@ class _RoomListStore extends AsyncStore<ActionPayload> {
this.checkEnabled();
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.
console.log("Regenerating room lists: Startup");
Expand All @@ -111,7 +119,7 @@ class _RoomListStore extends AsyncStore<ActionPayload> {
// Reset state without causing updates as the client will have been destroyed
// and downstream code will throw NPE errors.
this.reset(null, true);
this.matrixClient = null;
this._matrixClient = null;
this.initialListsGenerated = false; // we'll want to regenerate them
}

Expand Down Expand Up @@ -152,8 +160,21 @@ class _RoomListStore extends AsyncStore<ActionPayload> {

const roomId = eventPayload.event.getRoomId();
const room = this.matrixClient.getRoom(roomId);
console.log(`[RoomListDebug] Live timeline event ${eventPayload.event.getId()} in ${roomId}`);
await this.handleRoomUpdate(room, RoomUpdateCause.Timeline);
const tryUpdate = async (updatedRoom: Room) => {
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') {
const eventPayload = (<any>payload); // TODO: Type out the dispatcher types
const roomId = eventPayload.event.getRoomId();
Expand All @@ -171,11 +192,20 @@ class _RoomListStore extends AsyncStore<ActionPayload> {
// TODO: Update DMs
console.log(payload);
} 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
console.log(payload);
} else if (payload.action === 'MatrixActions.Room') {
// TODO: Update room from creation/join
console.log(payload);
// TODO: Improve new room check
// 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') {
// TODO: Update sticky room
console.log(payload);
Expand Down Expand Up @@ -211,11 +241,22 @@ class _RoomListStore extends AsyncStore<ActionPayload> {
}

private setAlgorithmClass() {
if (this.algorithm) {
this.algorithm.off(LIST_UPDATED_EVENT, this.onAlgorithmListUpdated);
}
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() {
console.warn("Regenerating all room lists");

const tags: ITagSortingMap = {};
for (const tagId of OrderedDefaultTagIDs) {
tags[tagId] = this.getSortAlgorithmFor(tagId);
Expand All @@ -234,16 +275,38 @@ class _RoomListStore extends AsyncStore<ActionPayload> {

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 {
private static internalInstance: _RoomListStore;
private static internalInstance: RoomListStore2;

public static get instance(): _RoomListStore {
public static get instance(): RoomListStore2 {
if (!RoomListStore.internalInstance) {
RoomListStore.internalInstance = new _RoomListStore();
RoomListStore.internalInstance = new RoomListStore2();
}

return RoomListStore.internalInstance;
}
}

window.mx_RoomListStore2 = RoomListStore.instance;
80 changes: 80 additions & 0 deletions src/stores/room-list/TagWatcher.ts
Original file line number Diff line number Diff line change
@@ -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;
}
};
}
Loading