Room list: add default sections (#32785)

* feat: add sections to RLSV3

* feat: add sections in vms

* feat: add room list section labs flag

* fix: wrong margin for room list item when in sections

* feat: hide favourites and low priority filters

* fix: crash when changing filter

* feat: support sticky room in sections

* test: update SC snapshot

* test: update SC screenshot

* test: update RLS tests

* test: add tests to RoomListSectionHeaderViewModel

* test: fix existing test in RoomListViewModel

* test: add sections tests for RoomListViewModel

* test: add e2e tests for sections

* fix: incorrect selected room when expanding/collasping a section

* fix: typo in `roomSkipList`

* feat: use one skip list with all filters instead of one list by tag

* chore: put back comment about `roomIndexInSection`

* chore: add missing `readonly`

* chore: add doc about possible undefined value for room item vm
This commit is contained in:
Florian Duros
2026-03-31 20:43:32 +02:00
committed by GitHub
parent 1974b50213
commit 0f515f581e
28 changed files with 1299 additions and 202 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 39 KiB

@@ -58,8 +58,11 @@ export interface RoomListViewActions {
createChatRoom: () => void;
/** Called to create a new room */
createRoom: () => void;
/** Get view model for a specific room (virtualization API) */
getRoomItemViewModel: (roomId: string) => RoomListItemViewModel;
/**
* Get view model for a specific room (virtualization API)
* Allow undefined to be returned if we don't have a view model for the room. In this case the room will not be rendered.
*/
getRoomItemViewModel: (roomId: string) => RoomListItemViewModel | undefined;
/** Called when the visible range changes (virtualization API) */
updateVisibleRooms: (startIndex: number, endIndex: number) => void;
/** Get view model for a specific section header (virtualization API) */
@@ -8352,7 +8352,7 @@ exports[`<RoomListView /> > renders LargeSectionList story 1`] = `
aria-haspopup="menu"
aria-label="Open room General"
aria-selected="false"
class="flex roomListItem mx_RoomListItemView bold firstItem"
class="flex roomListItem mx_RoomListItemView bold"
data-state="closed"
role="gridcell"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: stretch; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
@@ -13652,7 +13652,7 @@ exports[`<RoomListView /> > renders SmallSectionList story 1`] = `
aria-haspopup="menu"
aria-label="Open room General"
aria-selected="false"
class="flex roomListItem mx_RoomListItemView bold firstItem"
class="flex roomListItem mx_RoomListItemView bold"
data-state="closed"
role="gridcell"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: stretch; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
@@ -13806,7 +13806,7 @@ exports[`<RoomListView /> > renders SmallSectionList story 1`] = `
aria-haspopup="menu"
aria-label="Open room Random"
aria-selected="false"
class="flex roomListItem mx_RoomListItemView lastItem"
class="flex roomListItem mx_RoomListItemView"
data-state="closed"
role="gridcell"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: stretch; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
@@ -139,6 +139,13 @@ export function VirtualizedRoomListView({ vm, renderAvatar, onKeyDown }: Virtual
/**
* Get the item component for a specific index
* Gets the room's view model and passes it to RoomListItemView
*
* @param index - The index of the item in the list
* @param roomId - The ID of the room for this item
* @param context - The virtualization context containing list state
* @param onFocus - Callback to call when the item is focused
* @param isInLastSection - Whether this item is in the last section
* @param roomIndexInSection - The index of this room within its section
*/
const getItemComponent = useCallback(
(
@@ -146,18 +153,24 @@ export function VirtualizedRoomListView({ vm, renderAvatar, onKeyDown }: Virtual
roomId: string,
context: VirtualizedListContext<Context>,
onFocus: (item: string, e: React.FocusEvent) => void,
roomIndexInSection: number,
isInLastSection?: boolean,
roomIndexInSection?: number,
): JSX.Element => {
const { activeRoomIndex, roomCount, vm, isFlatList } = context.context;
const isSelected = activeRoomIndex === index;
const roomItemVM = vm.getRoomItemViewModel(roomId);
// If we don't have a view model for this room, it means the room has been removed since the list was rendered - return an empty placeholder
if (!roomItemVM) {
return <React.Fragment key={`stale-${index}`} />;
}
// Item is focused when the list has focus AND this item's key matches tabIndexKey
// This matches the old RoomList implementation's roving tabindex pattern
const isFocused = context.focused && context.tabIndexKey === roomId;
const isFirstItem = index === 0;
const isLastItem = index === roomCount - 1;
const isFirstItem = isFlatList && index === 0;
const isLastItem = Boolean((isFlatList || isInLastSection) && index === roomCount - 1);
return (
<RoomListItemAccessibilityWrapper
@@ -168,7 +181,8 @@ export function VirtualizedRoomListView({ vm, renderAvatar, onKeyDown }: Virtual
isFocused={isFocused}
onFocus={onFocus}
roomIndex={index}
roomIndexInSection={roomIndexInSection}
// For a flat list, we don't have sections, so roomIndexInSection is unused and can be set to 0
roomIndexInSection={roomIndexInSection || 0}
roomCount={roomCount}
isFirstItem={isFirstItem}
isLastItem={isLastItem}
@@ -181,7 +195,6 @@ export function VirtualizedRoomListView({ vm, renderAvatar, onKeyDown }: Virtual
/**
* Get the item component for a specific index in a grouped list
* Since we have sections, we can calculate the room's index within its section and pass it to getItemComponent
* Gets the room's view model and passes it to RoomListItemView
*/
const getItemComponentForGroupedList = useCallback(
@@ -194,14 +207,14 @@ export function VirtualizedRoomListView({ vm, renderAvatar, onKeyDown }: Virtual
): JSX.Element => {
const { sections } = context.context;
const roomIndexInSection = sections[groupIndex].roomIds.findIndex((id) => id === roomId);
return getItemComponent(index, roomId, context, onFocus, roomIndexInSection);
const isInLastSection = groupIndex === sections.length - 1;
return getItemComponent(index, roomId, context, onFocus, isInLastSection, roomIndexInSection);
},
[getItemComponent],
);
/**
* Get the item component for a specific index in a flat list
* Since we don't have sections, we can pass 0 for the room's index within its section to getItemComponent
* Gets the room's view model and passes it to RoomListItemView
*/
const getItemComponentForFlatList = useCallback(
@@ -211,8 +224,7 @@ export function VirtualizedRoomListView({ vm, renderAvatar, onKeyDown }: Virtual
context: VirtualizedListContext<Context>,
onFocus: (item: string, e: React.FocusEvent) => void,
): JSX.Element => {
// For a flat list, we don't have sections, so roomIndexInSection is unused and can be set to 0
return getItemComponent(index, roomId, context, onFocus, 0);
return getItemComponent(index, roomId, context, onFocus);
},
[getItemComponent],
);