Room List Store: Support filters by implementing just the favourite filter (#29433)

* Implement the favourite filter

* Make the room node capable of dealing with filters

- Holds data to indicate which filters apply
- Provides method to check if a given set of filters apply to this node
- Provides a method to recalculate which filters apply

* Wire up the filtering mechanism in skip list

* Use filters in the store

* Remove else

* Use a set instead of map
This commit is contained in:
R Midhun Suresh
2025-03-06 18:13:25 +05:30
committed by GitHub
parent 8d891cde53
commit 7ff1fd259d
7 changed files with 245 additions and 49 deletions

View File

@@ -11,6 +11,7 @@ import { EventType } from "matrix-js-sdk/src/matrix";
import type { EmptyObject, Room, RoomState } from "matrix-js-sdk/src/matrix";
import type { MatrixDispatcher } from "../../dispatcher/dispatcher";
import type { ActionPayload } from "../../dispatcher/payloads";
import type { FilterKey } from "./skip-list/filters";
import { AsyncStoreWithClient } from "../AsyncStoreWithClient";
import SettingsStore from "../../settings/SettingsStore";
import { VisibilityProvider } from "../room-list/filters/VisibilityProvider";
@@ -23,6 +24,7 @@ import { readReceiptChangeIsFor } from "../../utils/read-receipts";
import { EffectiveMembership, getEffectiveMembership, getEffectiveMembershipTag } from "../../utils/membership";
import SpaceStore from "../spaces/SpaceStore";
import { UPDATE_HOME_BEHAVIOUR, UPDATE_SELECTED_SPACE } from "../spaces";
import { FavouriteFilter } from "./skip-list/filters/FavouriteFilter";
/**
* This store allows for fast retrieval of the room list in a sorted and filtered manner.
@@ -61,9 +63,13 @@ export class RoomListStoreV3Class extends AsyncStoreWithClient<EmptyObject> {
/**
* Get a list of sorted rooms that belong to the currently active space.
* If filterKeys is passed, only the rooms that match the given filters are
* returned.
* @param filterKeys Optional array of filters that the rooms must match against.
*/
public getSortedRoomsInActiveSpace(): Room[] {
if (this.roomSkipList?.initialized) return Array.from(this.roomSkipList.getRoomsInActiveSpace());
public getSortedRoomsInActiveSpace(filterKeys?: FilterKey[]): Room[] {
if (this.roomSkipList?.initialized) return Array.from(this.roomSkipList.getRoomsInActiveSpace(filterKeys));
else return [];
}
@@ -90,7 +96,7 @@ export class RoomListStoreV3Class extends AsyncStoreWithClient<EmptyObject> {
protected async onReady(): Promise<any> {
if (this.roomSkipList?.initialized || !this.matrixClient) return;
const sorter = new RecencySorter(this.matrixClient.getSafeUserId());
this.roomSkipList = new RoomSkipList(sorter);
this.roomSkipList = new RoomSkipList(sorter, [new FavouriteFilter()]);
const rooms = this.getRooms();
await SpaceStore.instance.storeReadyPromise;
this.roomSkipList.seed(rooms);

View File

@@ -6,6 +6,7 @@ Please see LICENSE files in the repository root for full details.
*/
import type { Room } from "matrix-js-sdk/src/matrix";
import type { Filter, FilterKey } from "./filters";
import SpaceStore from "../../spaces/SpaceStore";
/**
@@ -48,4 +49,31 @@ export class RoomNode {
const activeSpace = SpaceStore.instance.activeSpace;
this._isInActiveSpace = SpaceStore.instance.isRoomInSpace(activeSpace, this.room.roomId);
}
/**
* Aggregates all the filter keys that apply to this room.
* eg: if filterKeysSet.has(Filter.FavouriteFilter) is true, then this room is a favourite room.
*/
private filterKeysSet: Set<FilterKey> = new Set();
/**
* Returns true if the associated room matches all the provided filters.
* Returns false otherwise.
* @param filterKeys An array of filter keys to check against.
*/
public doesRoomMatchFilters(filterKeys: FilterKey[]): boolean {
return !filterKeys.some((key) => !this.filterKeysSet.has(key));
}
/**
* Populates {@link RoomNode#filterKeysSet} by checking if the associated room
* satisfies the given filters.
* @param filters A list of filters
*/
public applyFilters(filters: Filter[]): void {
this.filterKeysSet = new Set();
for (const filter of filters) {
if (filter.matches(this.room)) this.filterKeysSet.add(filter.key);
}
}
}

View File

@@ -7,9 +7,11 @@ Please see LICENSE files in the repository root for full details.
import type { Room } from "matrix-js-sdk/src/matrix";
import type { Sorter } from "./sorters";
import type { Filter, FilterKey } from "./filters";
import { RoomNode } from "./RoomNode";
import { shouldPromote } from "./utils";
import { Level } from "./Level";
import { SortedRoomIterator, SortedSpaceFilteredIterator } from "./iterators";
/**
* Implements a skip list that stores rooms using a given sorting algorithm.
@@ -20,7 +22,10 @@ export class RoomSkipList implements Iterable<Room> {
private roomNodeMap: Map<string, RoomNode> = new Map();
public initialized: boolean = false;
public constructor(private sorter: Sorter) {}
public constructor(
private sorter: Sorter,
private filters: Filter[] = [],
) {}
private reset(): void {
this.levels = [new Level(0)];
@@ -35,6 +40,7 @@ export class RoomSkipList implements Iterable<Room> {
const sortedRoomNodes = this.sorter.sort(rooms).map((room) => new RoomNode(room));
let currentLevel = this.levels[0];
for (const node of sortedRoomNodes) {
node.applyFilters(this.filters);
currentLevel.setNext(node);
this.roomNodeMap.set(node.room.roomId, node);
}
@@ -95,6 +101,7 @@ export class RoomSkipList implements Iterable<Room> {
const newNode = new RoomNode(room);
newNode.checkIfRoomBelongsToActiveSpace();
newNode.applyFilters(this.filters);
this.roomNodeMap.set(room.roomId, newNode);
/**
@@ -173,8 +180,22 @@ export class RoomSkipList implements Iterable<Room> {
return new SortedRoomIterator(this.levels[0].head!);
}
public getRoomsInActiveSpace(): SortedSpaceFilteredIterator {
return new SortedSpaceFilteredIterator(this.levels[0].head!);
/**
* Returns an iterator that can be used to generate a list of sorted rooms that belong
* to the currently active space. Passing filterKeys will further filter the list such
* that only rooms that match the filters are returned.
*
* @example To get an array of rooms:
* Array.from(RLS.getRoomsInActiveSpace());
*
* @example Use a for ... of loop to iterate over rooms:
* for(const room of RLS.getRoomsInActiveSpace()) { something(room); }
*
* @example Additional filtering:
* Array.from(RLS.getRoomsInActiveSpace([FilterKeys.Favourite]));
*/
public getRoomsInActiveSpace(filterKeys: FilterKey[] = []): SortedSpaceFilteredIterator {
return new SortedSpaceFilteredIterator(this.levels[0].head!, filterKeys);
}
/**
@@ -184,36 +205,3 @@ export class RoomSkipList implements Iterable<Room> {
return this.levels[0].size;
}
}
class SortedRoomIterator implements Iterator<Room> {
public constructor(private current: RoomNode) {}
public next(): IteratorResult<Room> {
const current = this.current;
if (!current) return { value: undefined, done: true };
this.current = current.next[0];
return {
value: current.room,
};
}
}
class SortedSpaceFilteredIterator implements Iterator<Room> {
public constructor(private current: RoomNode) {}
public [Symbol.iterator](): SortedSpaceFilteredIterator {
return this;
}
public next(): IteratorResult<Room> {
let current = this.current;
while (current && !current.isInActiveSpace) {
current = current.next[0];
}
if (!current) return { value: undefined, done: true };
this.current = current.next[0];
return {
value: current.room,
};
}
}

View File

@@ -0,0 +1,20 @@
/*
Copyright 2025 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import type { Room } from "matrix-js-sdk/src/matrix";
import type { Filter } from ".";
import { FilterKey } from ".";
import { DefaultTagID } from "../../../room-list/models";
export class FavouriteFilter implements Filter {
public matches(room: Room): boolean {
return !!room.tags[DefaultTagID.Favourite];
}
public get key(): FilterKey.FavouriteFilter {
return FilterKey.FavouriteFilter;
}
}

View File

@@ -0,0 +1,24 @@
/*
Copyright 2025 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import type { Room } from "matrix-js-sdk/src/matrix";
export const enum FilterKey {
FavouriteFilter,
}
export interface Filter {
/**
* Boolean return value indicates whether this room satisfies
* the filter condition.
*/
matches(room: Room): boolean;
/**
* Used to identify this particular filter.
*/
key: FilterKey;
}

View File

@@ -0,0 +1,47 @@
/*
Copyright 2025 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import type { Room } from "matrix-js-sdk/src/matrix";
import type { RoomNode } from "./RoomNode";
import type { FilterKey } from "./filters";
export class SortedRoomIterator implements Iterator<Room> {
public constructor(private current: RoomNode) {}
public next(): IteratorResult<Room> {
const current = this.current;
if (!current) return { value: undefined, done: true };
this.current = current.next[0];
return {
value: current.room,
};
}
}
export class SortedSpaceFilteredIterator implements Iterator<Room> {
public constructor(
private current: RoomNode,
private readonly filters: FilterKey[],
) {}
public [Symbol.iterator](): SortedSpaceFilteredIterator {
return this;
}
public next(): IteratorResult<Room> {
let current = this.current;
while (current) {
if (current.isInActiveSpace && current.doesRoomMatchFilters(this.filters)) break;
current = current.next[0];
}
if (!current) return { value: undefined, done: true };
this.current = current.next[0];
return {
value: current.room,
};
}
}