fix(channels): render rooms list non-virtualized so re-picking a workspace on native doesn't strand the list empty after a remount
This commit is contained in:
parent
136aacded1
commit
fda6c7bd7e
1 changed files with 41 additions and 58 deletions
|
|
@ -3,14 +3,12 @@ import { useTranslation } from 'react-i18next';
|
||||||
import { useAtom, useAtomValue } from 'jotai';
|
import { useAtom, useAtomValue } from 'jotai';
|
||||||
import { Box, config } from 'folds';
|
import { Box, config } from 'folds';
|
||||||
import { Room } from 'matrix-js-sdk';
|
import { Room } from 'matrix-js-sdk';
|
||||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
|
||||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||||
import { mDirectAtom } from '../../../state/mDirectList';
|
import { mDirectAtom } from '../../../state/mDirectList';
|
||||||
import { NavCategory, NavCategoryHeader } from '../../../components/nav';
|
import { NavCategory, NavCategoryHeader } from '../../../components/nav';
|
||||||
import { getCanonicalAliasOrRoomId } from '../../../utils/matrix';
|
import { getCanonicalAliasOrRoomId } from '../../../utils/matrix';
|
||||||
import { useSelectedRoom } from '../../../hooks/router/useSelectedRoom';
|
import { useSelectedRoom } from '../../../hooks/router/useSelectedRoom';
|
||||||
import { useSpace } from '../../../hooks/useSpace';
|
import { useSpace } from '../../../hooks/useSpace';
|
||||||
import { VirtualTile } from '../../../components/virtualizer';
|
|
||||||
import { RoomNavCategoryButton, RoomNavItem } from '../../../features/room-nav';
|
import { RoomNavCategoryButton, RoomNavItem } from '../../../features/room-nav';
|
||||||
import { makeNavCategoryId } from '../../../state/closedNavCategories';
|
import { makeNavCategoryId } from '../../../state/closedNavCategories';
|
||||||
import { roomToUnreadAtom } from '../../../state/room/roomToUnread';
|
import { roomToUnreadAtom } from '../../../state/room/roomToUnread';
|
||||||
|
|
@ -26,19 +24,28 @@ import {
|
||||||
import { getChannelsRoomPath } from '../../pathUtils';
|
import { getChannelsRoomPath } from '../../pathUtils';
|
||||||
|
|
||||||
type ChannelsListProps = {
|
type ChannelsListProps = {
|
||||||
// Scroll container ref — owned by the parent `PageNavContent` so virtualizer
|
// Kept for API compatibility with PageNavContent's scrollRef wiring even
|
||||||
// measures the actual scrollable element instead of an inner div nested
|
// though the list itself no longer virtualizes — parent still passes the
|
||||||
// inside `<Scroll>`. Wrong scroll target → clientHeight reads as 0 →
|
// ref so other consumers (StreamHeader curtain gesture) can read it.
|
||||||
// virtualizer renders no rows. Match `Space.tsx` pattern.
|
|
||||||
scrollRef: MutableRefObject<HTMLDivElement | null>;
|
scrollRef: MutableRefObject<HTMLDivElement | null>;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Joined-hierarchy list rendered inside the channels left pane. Mirrors the
|
// Joined-hierarchy list rendered inside the channels left pane. Mirrors the
|
||||||
// scaffold of `Space.tsx` (virtualized hierarchy of joined rooms grouped by
|
// scaffold of `Space.tsx` (hierarchy of joined rooms grouped by sub-spaces)
|
||||||
// sub-spaces) but routes selections through the /channels/<space>/<room>/
|
// but routes selections through the /channels/<space>/<room>/ path so the
|
||||||
// path so the room timeline opens inside the channels surface, not the
|
// room timeline opens inside the channels surface, not the legacy
|
||||||
// legacy /<spaceId>/<room>/ tree.
|
// /<spaceId>/<room>/ tree.
|
||||||
export function ChannelsList({ scrollRef }: ChannelsListProps) {
|
//
|
||||||
|
// 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 `<Scroll>` 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 { t } = useTranslation();
|
||||||
const mx = useMatrixClient();
|
const mx = useMatrixClient();
|
||||||
const space = useSpace();
|
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) =>
|
const handleCategoryClick = useCategoryHandler(setClosedCategories, (categoryId) =>
|
||||||
closedCategories.has(categoryId)
|
closedCategories.has(categoryId)
|
||||||
);
|
);
|
||||||
|
|
@ -100,26 +100,16 @@ export function ChannelsList({ scrollRef }: ChannelsListProps) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box direction="Column" gap="300">
|
<Box direction="Column" gap="300">
|
||||||
<NavCategory
|
<NavCategory>
|
||||||
style={{
|
{hierarchy.map((item, index) => {
|
||||||
height: virtualizer.getTotalSize(),
|
const { roomId } = item;
|
||||||
position: 'relative',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{virtualizer.getVirtualItems().map((vItem) => {
|
|
||||||
const { roomId } = hierarchy[vItem.index] ?? {};
|
|
||||||
const room = mx.getRoom(roomId);
|
const room = mx.getRoom(roomId);
|
||||||
if (!room) return null;
|
if (!room) return null;
|
||||||
|
|
||||||
if (room.isSpaceRoom()) {
|
if (room.isSpaceRoom()) {
|
||||||
const categoryId = makeNavCategoryId(space.roomId, roomId);
|
const categoryId = makeNavCategoryId(space.roomId, roomId);
|
||||||
return (
|
return (
|
||||||
<VirtualTile
|
<div key={roomId} style={{ paddingTop: index === 0 ? undefined : config.space.S400 }}>
|
||||||
virtualItem={vItem}
|
|
||||||
key={vItem.index}
|
|
||||||
ref={virtualizer.measureElement}
|
|
||||||
>
|
|
||||||
<div style={{ paddingTop: vItem.index === 0 ? undefined : config.space.S400 }}>
|
|
||||||
<NavCategoryHeader>
|
<NavCategoryHeader>
|
||||||
<RoomNavCategoryButton
|
<RoomNavCategoryButton
|
||||||
data-category-id={categoryId}
|
data-category-id={categoryId}
|
||||||
|
|
@ -130,17 +120,12 @@ export function ChannelsList({ scrollRef }: ChannelsListProps) {
|
||||||
</RoomNavCategoryButton>
|
</RoomNavCategoryButton>
|
||||||
</NavCategoryHeader>
|
</NavCategoryHeader>
|
||||||
</div>
|
</div>
|
||||||
</VirtualTile>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VirtualTile
|
|
||||||
virtualItem={vItem}
|
|
||||||
key={vItem.index}
|
|
||||||
ref={virtualizer.measureElement}
|
|
||||||
>
|
|
||||||
<RoomNavItem
|
<RoomNavItem
|
||||||
|
key={roomId}
|
||||||
room={room}
|
room={room}
|
||||||
selected={selectedRoomId === roomId}
|
selected={selectedRoomId === roomId}
|
||||||
showAvatar={mDirects.has(roomId)}
|
showAvatar={mDirects.has(roomId)}
|
||||||
|
|
@ -148,11 +133,9 @@ export function ChannelsList({ scrollRef }: ChannelsListProps) {
|
||||||
linkPath={getToLink(roomId)}
|
linkPath={getToLink(roomId)}
|
||||||
notificationMode={getRoomNotificationMode(notificationPreferences, room.roomId)}
|
notificationMode={getRoomNotificationMode(notificationPreferences, room.roomId)}
|
||||||
/>
|
/>
|
||||||
</VirtualTile>
|
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</NavCategory>
|
</NavCategory>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue