Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement filtering in the room list store #29375

Draft
wants to merge 10 commits into
base: develop
Choose a base branch
from
18 changes: 18 additions & 0 deletions src/components/viewmodels/roomlist/RoomListViewModel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
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 RoomListStoreV3 from "../../../stores/room-list-v3/RoomListStoreV3";

export interface RoomListViewState {
rooms: Room[];
}

export function useRoomListViewModel(): RoomListViewState {
const rooms = RoomListStoreV3.instance.getSortedRooms();
return { rooms };
}
23 changes: 23 additions & 0 deletions src/components/views/rooms/RoomListView/RoomListView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@ Please see LICENSE files in the repository root for full details.
*/

import React from "react";
import { AutoSizer, List } from "react-virtualized";

import type { ListRowProps } from "react-virtualized";
import { shouldShowComponent } from "../../../../customisations/helpers/UIComponents";
import { UIComponent } from "../../../../settings/UIFeature";
import { RoomListSearch } from "./RoomListSearch";
import { RoomListHeaderView } from "./RoomListHeaderView";
import { useRoomListViewModel } from "../../../viewmodels/roomlist/RoomListViewModel";

type RoomListViewProps = {
/**
Expand All @@ -25,11 +28,31 @@ type RoomListViewProps = {
*/
export const RoomListView: React.FC<RoomListViewProps> = ({ activeSpace }) => {
const displayRoomSearch = shouldShowComponent(UIComponent.FilterContainer);
const { rooms } = useRoomListViewModel();

const rowRenderer = ({ key, index, style }: ListRowProps): React.JSX.Element => {
return (
<div key={key} style={style}>
{rooms[index].name}
</div>
);
};

return (
<section className="mx_RoomListView" data-testid="room-list-view">
{displayRoomSearch && <RoomListSearch activeSpace={activeSpace} />}
<RoomListHeaderView />
<AutoSizer>
{({ height, width }) => (
<List
rowRenderer={rowRenderer}
rowCount={rooms.length}
rowHeight={20}
height={height}
width={width}
/>
)}
</AutoSizer>
</section>
);
};
83 changes: 83 additions & 0 deletions src/stores/room-list-v3/RoomListStoreV3.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
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 { 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";
import defaultDispatcher from "../../dispatcher/dispatcher";
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<EmptyObject> {
private roomSkipList?: RoomSkipList;
private readonly msc3946ProcessDynamicPredecessor: boolean;
private filters: Filter[] = [new FavouriteFilter()];

public constructor(dispatcher: MatrixDispatcher) {
super(dispatcher);
this.msc3946ProcessDynamicPredecessor = SettingsStore.getValue("feature_dynamic_room_predecessors");
}

public getRooms(): Room[] {
let rooms = this.matrixClient?.getVisibleRooms(this.msc3946ProcessDynamicPredecessor) ?? [];
rooms = rooms.filter((r) => VisibilityProvider.instance.isRoomVisible(r));
return rooms;
}

public getSortedRooms(): Room[] {
if (this.roomSkipList?.initialized) return Array.from(this.roomSkipList);
else return [];
}

public useAlphabeticSorting(): void {
if (this.roomSkipList) {
const sorter = new AlphabeticSorter();
this.roomSkipList.useNewSorter(sorter, this.getRooms());
}
}

public useRecencySorting(): void {
if (this.roomSkipList && this.matrixClient) {
const sorter = new RecencySorter(this.matrixClient?.getSafeUserId() ?? "");
this.roomSkipList.useNewSorter(sorter, this.getRooms());
}
}

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.filters);
const rooms = this.getRooms();
this.roomSkipList.seed(rooms);
this.emit(LISTS_UPDATE_EVENT);
}

protected async onAction(payload: ActionPayload): Promise<void> {
return;
}
}

export default class RoomListStoreV3 {
private static internalInstance: RoomListStoreV3Class;

public static get instance(): RoomListStoreV3Class {
if (!RoomListStoreV3.internalInstance) {
const instance = new RoomListStoreV3Class(defaultDispatcher);
instance.start();
RoomListStoreV3.internalInstance = instance;
}

return this.internalInstance;
}
}
15 changes: 15 additions & 0 deletions src/stores/room-list-v3/skip-list/RoomNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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<Filters, boolean> = new Map();

public calculateFilters(filters: Filter[]): void {
for (const filter of filters) {
const matchesFilter = filter.matches(this.room);
this.filters.set(filter.key, matchesFilter);
}
}
}
35 changes: 34 additions & 1 deletion src/stores/room-list-v3/skip-list/RoomSkipList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -20,7 +21,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)];
Expand All @@ -35,6 +39,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.calculateFilters(this.filters);
currentLevel.setNext(node);
this.roomNodeMap.set(node.room.roomId, node);
}
Expand Down Expand Up @@ -81,6 +86,7 @@ export class RoomSkipList implements Iterable<Room> {
this.removeRoom(room);

const newNode = new RoomNode(room);
newNode.calculateFilters(this.filters);
this.roomNodeMap.set(room.roomId, newNode);

/**
Expand Down Expand Up @@ -159,6 +165,10 @@ export class RoomSkipList implements Iterable<Room> {
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.
*/
Expand All @@ -179,3 +189,26 @@ class SortedRoomIterator implements Iterator<Room> {
};
}
}

class SortedFilteredIterator implements Iterator<Room> {
public constructor(
private current: RoomNode,
private filterKeys: Filters[],
) {}

public [Symbol.iterator](): SortedFilteredIterator {
return this;
}

public next(): IteratorResult<Room> {
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,
};
}
}
20 changes: 20 additions & 0 deletions src/stores/room-list-v3/skip-list/filters/FavouriteFilter.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
25 changes: 25 additions & 0 deletions src/stores/room-list-v3/skip-list/filters/index.ts
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,32 @@ exports[`<RoomListView /> should not render the RoomListSearch component when UI
</h1>
</div>
</header>
<div
style="overflow: visible; height: 0px; width: 0px;"
>
<div
aria-label="grid"
aria-readonly="true"
class="ReactVirtualized__Grid ReactVirtualized__List"
role="grid"
style="box-sizing: border-box; direction: ltr; height: 0px; position: relative; width: 0px; will-change: transform; overflow-x: hidden; overflow-y: hidden;"
tabindex="0"
/>
</div>
<div
class="resize-triggers"
>
<div
class="expand-trigger"
>
<div
style="width: 1px; height: 1px;"
/>
</div>
<div
class="contract-trigger"
/>
</div>
</section>
</DocumentFragment>
`;
Expand Down Expand Up @@ -137,6 +163,32 @@ exports[`<RoomListView /> should render the RoomListSearch component when UIComp
</div>
</button>
</header>
<div
style="overflow: visible; height: 0px; width: 0px;"
>
<div
aria-label="grid"
aria-readonly="true"
class="ReactVirtualized__Grid ReactVirtualized__List"
role="grid"
style="box-sizing: border-box; direction: ltr; height: 0px; position: relative; width: 0px; will-change: transform; overflow-x: hidden; overflow-y: hidden;"
tabindex="0"
/>
</div>
<div
class="resize-triggers"
>
<div
class="expand-trigger"
>
<div
style="width: 1px; height: 1px;"
/>
</div>
<div
class="contract-trigger"
/>
</div>
</section>
</DocumentFragment>
`;
Loading
Loading