Move the room list to the new ListView(backed by react-virtuoso) (#30515)

* Move Room List to ListView

- Also remove Space/Enter handing from keyboard navigation we can just leave the default behaviour of those keys and handle via onClick

* Update rooms when the primary filter changes

Otherwise when changing spaces, the filter does not reset until the next update to the RVS is made.

* Fix stickyRow/scrollIntoView when switiching space or changing filters

- Also remove the rest of space/enter keyboard handling use

* Remove the rest of space/enter keyboard handling use

* Remove useCombinedRef and add @radix-ui/react-compose-refs as we already depend on it

- Also remove eact-virtualized dep

* Update RoomList unit test

* Update snapshots and unit tests

* Fix e2e tests

* Remove react-virtualized from tests

* Fix e2e flake

* Update more screenshots

* Fix e2e test case where were should scroll to the top when the active room is no longer in the list

* Move from gitpkg to package-patch

* Update to latest react virtuoso release/api.

Also pass spaceId to the room list and scroll the activeIndex into view when spaceId or primaryFilter change.

* Use listbox/option roles to improve ScreenReader experience

* Change onKeyDown e.stopPropogation to cover context menu

* lint

* Remove unneeded exposure of the listView ref

Also move scrollIntoViewOnChange to useCallback

* Update unit test and snapshot

* Fix e2e tests and update screenshots

* Fix unit test and snapshot

* Update more unit tests

* Fix keyboard shortcuts and e2e test

* Fix another e2e and unit test

* lint

* Improve the naming for RoomResult and the documentation on it's fields meaning.

Also update the login in RoomList to check for any change in filters, this is a bit more future proof for when we introduce multi select than using activePrimaryFilter.

* Put back and fix landmark tests

* Fix test import

* Add comment regarding context object getting rendered.

* onKeyDown should be optional

* Use SpaceKey type on RoomResult

* lint
This commit is contained in:
David Langley
2025-08-21 15:43:40 +01:00
committed by GitHub
parent ef3a6a9429
commit c842b615db
50 changed files with 1139 additions and 1021 deletions

View File

@@ -18,6 +18,7 @@ import { Action } from "../../../dispatcher/actions";
import { useMatrixClientContext } from "../../../contexts/MatrixClientContext";
import { useStickyRoomList } from "./useStickyRoomList";
import { useRoomListNavigation } from "./useRoomListNavigation";
import { type RoomsResult } from "../../../stores/room-list-v3/RoomListStoreV3";
export interface RoomListViewState {
/**
@@ -26,9 +27,9 @@ export interface RoomListViewState {
isLoadingRooms: boolean;
/**
* A list of rooms to be displayed in the left panel.
* The room results to be displayed (along with the spaceId and filter keys at the time of query)
*/
rooms: Room[];
roomsResult: RoomsResult;
/**
* Create a chat room
@@ -71,10 +72,10 @@ export interface RoomListViewState {
*/
export function useRoomListViewModel(): RoomListViewState {
const matrixClient = useMatrixClientContext();
const { isLoadingRooms, primaryFilters, activePrimaryFilter, rooms: filteredRooms } = useFilteredRooms();
const { activeIndex, rooms } = useStickyRoomList(filteredRooms);
const { isLoadingRooms, primaryFilters, activePrimaryFilter, roomsResult: filteredRooms } = useFilteredRooms();
const { activeIndex, roomsResult } = useStickyRoomList(filteredRooms);
useRoomListNavigation(rooms);
useRoomListNavigation(roomsResult.rooms);
const currentSpace = useEventEmitterState<Room | null>(
SpaceStore.instance,
@@ -88,7 +89,7 @@ export function useRoomListViewModel(): RoomListViewState {
return {
isLoadingRooms,
rooms,
roomsResult,
canCreateRoom,
createRoom,
createChatRoom,

View File

@@ -5,12 +5,15 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
import { useCallback, useMemo, useState } from "react";
import { useCallback, useEffect, useMemo, useState } from "react";
import type { Room } from "matrix-js-sdk/src/matrix";
import { FilterKey } from "../../../stores/room-list-v3/skip-list/filters";
import { _t, _td, type TranslationKey } from "../../../languageHandler";
import RoomListStoreV3, { LISTS_LOADED_EVENT, LISTS_UPDATE_EVENT } from "../../../stores/room-list-v3/RoomListStoreV3";
import RoomListStoreV3, {
LISTS_LOADED_EVENT,
LISTS_UPDATE_EVENT,
type RoomsResult,
} from "../../../stores/room-list-v3/RoomListStoreV3";
import { useEventEmitter } from "../../../hooks/useEventEmitter";
import SpaceStore from "../../../stores/spaces/SpaceStore";
import { UPDATE_SELECTED_SPACE } from "../../../stores/spaces";
@@ -35,7 +38,7 @@ export interface PrimaryFilter {
interface FilteredRooms {
primaryFilters: PrimaryFilter[];
isLoadingRooms: boolean;
rooms: Room[];
roomsResult: RoomsResult;
/**
* The currently active primary filter.
* If no primary filter is active, this will be undefined.
@@ -63,12 +66,12 @@ export function useFilteredRooms(): FilteredRooms {
*/
const [primaryFilter, setPrimaryFilter] = useState<FilterKey | undefined>();
const [rooms, setRooms] = useState(() => RoomListStoreV3.instance.getSortedRoomsInActiveSpace());
const [roomsResult, setRoomsResult] = useState(() => RoomListStoreV3.instance.getSortedRoomsInActiveSpace());
const [isLoadingRooms, setIsLoadingRooms] = useState(() => RoomListStoreV3.instance.isLoadingRooms);
const updateRoomsFromStore = useCallback((filters: FilterKey[] = []): void => {
const newRooms = RoomListStoreV3.instance.getSortedRoomsInActiveSpace(filters);
setRooms(newRooms);
setRoomsResult(newRooms);
}, []);
// Reset filters when active space changes
@@ -77,9 +80,15 @@ export function useFilteredRooms(): FilteredRooms {
const filterUndefined = (array: (FilterKey | undefined)[]): FilterKey[] =>
array.filter((f) => f !== undefined) as FilterKey[];
const getAppliedFilters = (): FilterKey[] => {
const getAppliedFilters = useCallback((): FilterKey[] => {
return filterUndefined([primaryFilter]);
};
}, [primaryFilter]);
useEffect(() => {
// Update the rooms state when the primary filter changes
const filters = getAppliedFilters();
updateRoomsFromStore(filters);
}, [getAppliedFilters, updateRoomsFromStore]);
useEventEmitter(RoomListStoreV3.instance, LISTS_UPDATE_EVENT, () => {
const filters = getAppliedFilters();
@@ -122,6 +131,6 @@ export function useFilteredRooms(): FilteredRooms {
isLoadingRooms,
primaryFilters,
activePrimaryFilter,
rooms,
roomsResult,
};
}

View File

@@ -14,6 +14,7 @@ import { Action } from "../../../dispatcher/actions";
import type { Room } from "matrix-js-sdk/src/matrix";
import type { Optional } from "matrix-events-sdk";
import SpaceStore from "../../../stores/spaces/SpaceStore";
import { type RoomsResult } from "../../../stores/room-list-v3/RoomListStoreV3";
function getIndexByRoomId(rooms: Room[], roomId: Optional<string>): number | undefined {
const index = rooms.findIndex((room) => room.roomId === roomId);
@@ -67,11 +68,11 @@ function getRoomsWithStickyRoom(
return { newIndex: oldIndex, newRooms };
}
interface StickyRoomListResult {
export interface StickyRoomListResult {
/**
* List of rooms with sticky active room.
* The rooms result with the active sticky room applied
*/
rooms: Room[];
roomsResult: RoomsResult;
/**
* Index of the active room in the room list.
*/
@@ -85,10 +86,10 @@ interface StickyRoomListResult {
* @param rooms list of rooms
* @see {@link StickyRoomListResult} details what this hook returns..
*/
export function useStickyRoomList(rooms: Room[]): StickyRoomListResult {
const [listState, setListState] = useState<{ index: number | undefined; roomsWithStickyRoom: Room[] }>({
index: undefined,
roomsWithStickyRoom: rooms,
export function useStickyRoomList(roomsResult: RoomsResult): StickyRoomListResult {
const [listState, setListState] = useState<StickyRoomListResult>({
activeIndex: getIndexByRoomId(roomsResult.rooms, SdkContextClass.instance.roomViewStore.getRoomId()),
roomsResult: roomsResult,
});
const currentSpaceRef = useRef(SpaceStore.instance.activeSpace);
@@ -97,13 +98,18 @@ export function useStickyRoomList(rooms: Room[]): StickyRoomListResult {
(newRoomId: string | null, isRoomChange: boolean = false) => {
setListState((current) => {
const activeRoomId = newRoomId ?? SdkContextClass.instance.roomViewStore.getRoomId();
const newActiveIndex = getIndexByRoomId(rooms, activeRoomId);
const oldIndex = current.index;
const { newIndex, newRooms } = getRoomsWithStickyRoom(rooms, oldIndex, newActiveIndex, isRoomChange);
return { index: newIndex, roomsWithStickyRoom: newRooms };
const newActiveIndex = getIndexByRoomId(roomsResult.rooms, activeRoomId);
const oldIndex = current.activeIndex;
const { newIndex, newRooms } = getRoomsWithStickyRoom(
roomsResult.rooms,
oldIndex,
newActiveIndex,
isRoomChange,
);
return { activeIndex: newIndex, roomsResult: { ...roomsResult, rooms: newRooms } };
});
},
[rooms],
[roomsResult],
);
// Re-calculate the index when the active room has changed.
@@ -115,20 +121,19 @@ export function useStickyRoomList(rooms: Room[]): StickyRoomListResult {
useEffect(() => {
let newRoomId: string | null = null;
let isRoomChange = false;
const newSpace = SpaceStore.instance.activeSpace;
if (currentSpaceRef.current !== newSpace) {
if (currentSpaceRef.current !== roomsResult.spaceId) {
/*
If the space has changed, we check if we can immediately set the active
index to the last opened room in that space. Otherwise, we might see a
flicker because of the delay between the space change event and
active room change dispatch.
*/
newRoomId = SpaceStore.instance.getLastSelectedRoomIdForSpace(newSpace);
newRoomId = SpaceStore.instance.getLastSelectedRoomIdForSpace(roomsResult.spaceId);
isRoomChange = true;
currentSpaceRef.current = newSpace;
currentSpaceRef.current = roomsResult.spaceId;
}
updateRoomsAndIndex(newRoomId, isRoomChange);
}, [rooms, updateRoomsAndIndex]);
}, [roomsResult, updateRoomsAndIndex]);
return { activeIndex: listState.index, rooms: listState.roomsWithStickyRoom };
return listState;
}