From fda6c7bd7eaaa4fab30721d80856a96ee961c866 Mon Sep 17 00:00:00 2001 From: heaven Date: Wed, 27 May 2026 23:49:41 +0300 Subject: [PATCH] fix(channels): render rooms list non-virtualized so re-picking a workspace on native doesn't strand the list empty after a remount --- .../pages/client/channels/ChannelsList.tsx | 99 ++++++++----------- 1 file changed, 41 insertions(+), 58 deletions(-) diff --git a/src/app/pages/client/channels/ChannelsList.tsx b/src/app/pages/client/channels/ChannelsList.tsx index 1dbecdf6..42ed8e89 100644 --- a/src/app/pages/client/channels/ChannelsList.tsx +++ b/src/app/pages/client/channels/ChannelsList.tsx @@ -3,14 +3,12 @@ import { useTranslation } from 'react-i18next'; import { useAtom, useAtomValue } from 'jotai'; import { Box, config } from 'folds'; import { Room } from 'matrix-js-sdk'; -import { useVirtualizer } from '@tanstack/react-virtual'; import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { mDirectAtom } from '../../../state/mDirectList'; import { NavCategory, NavCategoryHeader } from '../../../components/nav'; import { getCanonicalAliasOrRoomId } from '../../../utils/matrix'; import { useSelectedRoom } from '../../../hooks/router/useSelectedRoom'; import { useSpace } from '../../../hooks/useSpace'; -import { VirtualTile } from '../../../components/virtualizer'; import { RoomNavCategoryButton, RoomNavItem } from '../../../features/room-nav'; import { makeNavCategoryId } from '../../../state/closedNavCategories'; import { roomToUnreadAtom } from '../../../state/room/roomToUnread'; @@ -26,19 +24,28 @@ import { import { getChannelsRoomPath } from '../../pathUtils'; type ChannelsListProps = { - // Scroll container ref — owned by the parent `PageNavContent` so virtualizer - // measures the actual scrollable element instead of an inner div nested - // inside ``. Wrong scroll target → clientHeight reads as 0 → - // virtualizer renders no rows. Match `Space.tsx` pattern. + // Kept for API compatibility with PageNavContent's scrollRef wiring even + // though the list itself no longer virtualizes — parent still passes the + // ref so other consumers (StreamHeader curtain gesture) can read it. scrollRef: MutableRefObject; }; // Joined-hierarchy list rendered inside the channels left pane. Mirrors the -// scaffold of `Space.tsx` (virtualized hierarchy of joined rooms grouped by -// sub-spaces) but routes selections through the /channels/// -// path so the room timeline opens inside the channels surface, not the -// legacy /// tree. -export function ChannelsList({ scrollRef }: ChannelsListProps) { +// scaffold of `Space.tsx` (hierarchy of joined rooms grouped by sub-spaces) +// but routes selections through the /channels/// path so the +// room timeline opens inside the channels surface, not the legacy +// /// tree. +// +// NOT virtualized on purpose. Lists in the channels pane are bounded (one +// orphan workspace + its joined rooms — typically tens, not thousands), +// and @tanstack/react-virtual had a fragile race here with folds' Scroll +// component: every re-render of `` creates a fresh inline ref +// callback, React 18 detaches the old callback (nulling scrollRef.current) +// before children's useLayoutEffects run, and the virtualizer's +// `_willUpdate` saw null on its first chance to subscribe — leaving the +// list permanently empty on the 2nd+ remount of the same workspace. Plain +// `.map()` sidesteps the whole subscription dance. +export function ChannelsList({ scrollRef: _scrollRef }: ChannelsListProps) { const { t } = useTranslation(); const mx = useMatrixClient(); const space = useSpace(); @@ -84,13 +91,6 @@ export function ChannelsList({ scrollRef }: ChannelsListProps) { ) ); - const virtualizer = useVirtualizer({ - count: hierarchy.length, - getScrollElement: () => scrollRef.current, - estimateSize: () => 0, - overscan: 10, - }); - const handleCategoryClick = useCategoryHandler(setClosedCategories, (categoryId) => closedCategories.has(categoryId) ); @@ -100,59 +100,42 @@ export function ChannelsList({ scrollRef }: ChannelsListProps) { return ( - - {virtualizer.getVirtualItems().map((vItem) => { - const { roomId } = hierarchy[vItem.index] ?? {}; + + {hierarchy.map((item, index) => { + const { roomId } = item; const room = mx.getRoom(roomId); if (!room) return null; if (room.isSpaceRoom()) { const categoryId = makeNavCategoryId(space.roomId, roomId); return ( - -
- - - {roomId === space.roomId ? t('Channels.root_category') : room?.name} - - -
-
+
+ + + {roomId === space.roomId ? t('Channels.root_category') : room?.name} + + +
); } return ( - - - + ); })}
); } -