diff --git a/src/stores/room-list-v3/RoomListStoreV3.ts b/src/stores/room-list-v3/RoomListStoreV3.ts index d6ce63713c..dba6c65fe0 100644 --- a/src/stores/room-list-v3/RoomListStoreV3.ts +++ b/src/stores/room-list-v3/RoomListStoreV3.ts @@ -8,6 +8,7 @@ Please see LICENSE files in the repository root for full details. import type { EmptyObject, Room } from "matrix-js-sdk/src/matrix"; import type { MatrixDispatcher } from "../../dispatcher/dispatcher"; import type { ActionPayload } from "../../dispatcher/payloads"; +import type { Filter } from "./skip-list/filters"; import { AsyncStoreWithClient } from "../AsyncStoreWithClient"; import SettingsStore from "../../settings/SettingsStore"; import { VisibilityProvider } from "../room-list/filters/VisibilityProvider"; @@ -16,10 +17,12 @@ import { LISTS_UPDATE_EVENT } from "../room-list/RoomListStore"; import { RoomSkipList } from "./skip-list/RoomSkipList"; import { RecencySorter } from "./skip-list/sorters/RecencySorter"; import { AlphabeticSorter } from "./skip-list/sorters/AlphabeticSorter"; +import { FavouriteFilter } from "./skip-list/filters/FavouriteFilter"; export class RoomListStoreV3Class extends AsyncStoreWithClient { private roomSkipList?: RoomSkipList; private readonly msc3946ProcessDynamicPredecessor: boolean; + private filters: Filter[] = [new FavouriteFilter()]; public constructor(dispatcher: MatrixDispatcher) { super(dispatcher); @@ -54,7 +57,7 @@ export class RoomListStoreV3Class extends AsyncStoreWithClient { protected async onReady(): Promise { if (this.roomSkipList?.initialized || !this.matrixClient) return; const sorter = new RecencySorter(this.matrixClient.getSafeUserId()); - this.roomSkipList = new RoomSkipList(sorter); + this.roomSkipList = new RoomSkipList(sorter, this.filters); const rooms = this.getRooms(); this.roomSkipList.seed(rooms); this.emit(LISTS_UPDATE_EVENT); diff --git a/src/stores/room-list-v3/skip-list/RoomNode.ts b/src/stores/room-list-v3/skip-list/RoomNode.ts index cbc2a3346f..3bd9a2d224 100644 --- a/src/stores/room-list-v3/skip-list/RoomNode.ts +++ b/src/stores/room-list-v3/skip-list/RoomNode.ts @@ -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, Filters } from "./filters"; /** * Room skip list stores room nodes. @@ -26,4 +27,18 @@ export class RoomNode { * eg: previous[i] gives the previous room node from this room node in level i. */ public previous: RoomNode[] = []; + + /** + * Aggregates all the filters that apply to this room. + * eg: if filters[Filter.FavouriteFilter] is true, then this room is a favourite + * room. + */ + public filters: Map = new Map(); + + public calculateFilters(filters: Filter[]): void { + for (const filter of filters) { + const matchesFilter = filter.matches(this.room); + this.filters.set(filter.key, matchesFilter); + } + } } diff --git a/src/stores/room-list-v3/skip-list/RoomSkipList.ts b/src/stores/room-list-v3/skip-list/RoomSkipList.ts index 260786594f..2da18593da 100644 --- a/src/stores/room-list-v3/skip-list/RoomSkipList.ts +++ b/src/stores/room-list-v3/skip-list/RoomSkipList.ts @@ -7,6 +7,7 @@ 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, Filters } from "./filters"; import { RoomNode } from "./RoomNode"; import { shouldPromote } from "./utils"; import { Level } from "./Level"; @@ -20,7 +21,10 @@ export class RoomSkipList implements Iterable { private roomNodeMap: Map = 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 +39,7 @@ export class RoomSkipList implements Iterable { const sortedRoomNodes = this.sorter.sort(rooms).map((room) => new RoomNode(room)); let currentLevel = this.levels[0]; for (const node of sortedRoomNodes) { + node.calculateFilters(this.filters); currentLevel.setNext(node); this.roomNodeMap.set(node.room.roomId, node); } @@ -81,6 +86,7 @@ export class RoomSkipList implements Iterable { this.removeRoom(room); const newNode = new RoomNode(room); + newNode.calculateFilters(this.filters); this.roomNodeMap.set(room.roomId, newNode); /** @@ -159,6 +165,10 @@ export class RoomSkipList implements Iterable { return new SortedRoomIterator(this.levels[0].head!); } + public getFiltered(filterKeys: Filters[]): SortedFilteredIterator { + return new SortedFilteredIterator(this.levels[0].head!, filterKeys); + } + /** * The number of rooms currently in the skip list. */ @@ -179,3 +189,26 @@ class SortedRoomIterator implements Iterator { }; } } + +class SortedFilteredIterator implements Iterator { + public constructor( + private current: RoomNode, + private filterKeys: Filters[], + ) {} + + public [Symbol.iterator](): SortedFilteredIterator { + return this; + } + + public next(): IteratorResult { + let current = this.current; + while (current && this.filterKeys.some((key) => !current.filters.get(key))) { + current = current.next[0]; + } + if (!current) return { value: undefined, done: true }; + this.current = current.next[0]; + return { + value: current.room, + }; + } +} diff --git a/src/stores/room-list-v3/skip-list/filters/FavouriteFilter.ts b/src/stores/room-list-v3/skip-list/filters/FavouriteFilter.ts new file mode 100644 index 0000000000..0e21d55d71 --- /dev/null +++ b/src/stores/room-list-v3/skip-list/filters/FavouriteFilter.ts @@ -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 { Filters } 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(): Filters.FavouriteFilter { + return Filters.FavouriteFilter; + } +} diff --git a/src/stores/room-list-v3/skip-list/filters/index.ts b/src/stores/room-list-v3/skip-list/filters/index.ts new file mode 100644 index 0000000000..f372afe114 --- /dev/null +++ b/src/stores/room-list-v3/skip-list/filters/index.ts @@ -0,0 +1,25 @@ +/* +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 Filters { + FavouriteFilter, +} + +export interface Filter { + /** + * Boolean return value indicates whether this room satisfies + * the filter condition. + */ + matches(room: Room): boolean; + + /** + * An unique string used to identify a given filter. + */ + key: Filters; +}