feat: Enhance space hierarchy management by implementing asynchronous room fetching and improving room handling logic

This commit is contained in:
2026-07-06 16:06:50 +10:00
parent 3ee95a295d
commit f67f08f1db
5 changed files with 238 additions and 56 deletions

View File

@@ -1,4 +1,4 @@
import React, { MouseEventHandler, useCallback, useMemo, useRef, useState } from 'react';
import React, { MouseEventHandler, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Box, Chip, IconButton, Line, Scroll, Spinner, Text, config } from 'folds';
import { Icon, Icons } from '../../components/icons';
import { useVirtualizer } from '@tanstack/react-virtual';
@@ -13,6 +13,8 @@ import { Page, PageContent, PageContentCenter, PageHeroSection } from '../../com
import {
HierarchyItem,
HierarchyItemSpace,
fetchAllHierarchyRooms,
isHierarchySpaceRoom,
useSpaceHierarchy,
} from '../../hooks/useSpaceHierarchy';
import { VirtualTile } from '../../components/virtualizer';
@@ -41,7 +43,7 @@ import { getSpaceRoomPath } from '../../pages/pathUtils';
import { StateEvent } from '../../../types/matrix/room';
import { CanDropCallback, useDnDMonitor } from './DnD';
import { ASCIILexicalTable, orderKeys } from '../../utils/ASCIILexicalTable';
import { getStateEvent } from '../../utils/room';
import { getStateEvent, shouldShowForumLobby } from '../../utils/room';
import { useClosedLobbyCategoriesAtom } from '../../state/hooks/closedLobbyCategories';
import {
makeCinnySpacesContent,
@@ -161,6 +163,7 @@ function SpaceCardLobby() {
const allRooms = useAtomValue(allRoomsAtom);
const allJoinedRooms = useMemo(() => new Set(allRooms), [allRooms]);
const space = useSpace();
const isForum = shouldShowForumLobby(space);
const spacePowerLevels = usePowerLevels(space);
const lex = useMemo(() => new ASCIILexicalTable(' '.charCodeAt(0), '~'.charCodeAt(0), 6), []);
const members = useRoomMembers(mx, space.roomId);
@@ -189,6 +192,30 @@ function SpaceCardLobby() {
const [spacesItems, setSpacesItem] = useState<Map<string, IHierarchyRoom>>(() => new Map());
useEffect(() => {
let cancelled = false;
void (async () => {
try {
const all = await fetchAllHierarchyRooms(mx, space.roomId, 3);
if (cancelled) return;
setSpaceRooms({
type: 'PUT',
roomIds: all.filter(isHierarchySpaceRoom).map((r) => r.room_id),
});
setSpacesItem((current) =>
produce(current, (draft) => {
all.forEach((r) => draft.set(r.room_id, r));
})
);
} catch {
// hierarchy may be unavailable for some memberships
}
})();
return () => {
cancelled = true;
};
}, [mx, space.roomId, setSpaceRooms]);
useElementSizeObserver(
useCallback(() => heroSectionRef.current, []),
useCallback((w, height) => setHeroSectionHeight(height), [])
@@ -206,7 +233,8 @@ function SpaceCardLobby() {
closedCategories.has(makeLobbyCategoryId(space.roomId, childId)) ||
(draggingItem ? 'space' in draggingItem : false),
[closedCategories, space.roomId, draggingItem]
)
),
spacesItems
);
const virtualizer = useVirtualizer({
@@ -400,14 +428,16 @@ function SpaceCardLobby() {
);
const handleSpacesFound = useCallback(
(sItems: IHierarchyRoom[]) => {
setSpaceRooms({ type: 'PUT', roomIds: sItems.map((i) => i.room_id) });
setSpacesItem((current) => {
const newItems = produce(current, (draft) => {
sItems.forEach((item) => draft.set(item.room_id, item));
});
return current.size === newItems.size ? current : newItems;
(items: IHierarchyRoom[]) => {
setSpaceRooms({
type: 'PUT',
roomIds: items.filter(isHierarchySpaceRoom).map((i) => i.room_id),
});
setSpacesItem((current) =>
produce(current, (draft) => {
items.forEach((item) => draft.set(item.room_id, item));
})
);
},
[setSpaceRooms]
);
@@ -491,6 +521,7 @@ function SpaceCardLobby() {
<SpaceHierarchy
spaceItem={item.space}
summary={spacesItems.get(item.space.roomId)}
hierarchySummaries={spacesItems}
roomItems={item.rooms}
allJoinedRooms={allJoinedRooms}
mDirects={mDirects}
@@ -510,7 +541,7 @@ function SpaceCardLobby() {
pinned={sidebarSpaces.has(item.space.roomId)}
togglePinToSidebar={togglePinToSidebar}
onSpacesFound={handleSpacesFound}
onOpenRoom={handleOpenRoom}
onOpenRoom={isForum ? undefined : handleOpenRoom}
/>
</VirtualTile>
);

View File

@@ -267,7 +267,7 @@ type RoomItemCardProps = {
dm?: boolean;
firstChild?: boolean;
lastChild?: boolean;
onOpen: MouseEventHandler<HTMLButtonElement>;
onOpen?: MouseEventHandler<HTMLButtonElement>;
options?: ReactNode;
before?: ReactNode;
after?: ReactNode;
@@ -332,7 +332,7 @@ export const RoomItemCard = as<'div', RoomItemCardProps>(
suggested={content.suggested}
joinRule={localSummary.joinRule}
options={
joined ? (
joined && onOpen ? (
<Box shrink="No" gap="100" alignItems="Center">
<Chip
data-room-id={roomId}

View File

@@ -6,6 +6,7 @@ import {
HierarchyItem,
HierarchyItemRoom,
HierarchyItemSpace,
isHierarchySpaceRoom,
useFetchSpaceHierarchyLevel,
} from '../../hooks/useSpaceHierarchy';
import { IPowerLevels } from '../../hooks/usePowerLevels';
@@ -14,7 +15,7 @@ import { SpaceItemCard } from './SpaceItem';
import { AfterItemDropTarget, CanDropCallback } from './DnD';
import { HierarchyItemMenu } from './HierarchyItemMenu';
import { RoomItemCard } from './RoomItem';
import { RoomType, StateEvent } from '../../../types/matrix/room';
import { StateEvent } from '../../../types/matrix/room';
import { SequenceCard } from '../../components/sequence-card';
import { getRoomCreatorsForRoomId } from '../../hooks/useRoomCreators';
import { getRoomPermissionsAPI } from '../../hooks/useRoomPermissions';
@@ -22,6 +23,7 @@ import { PaarrotSubRoomsContent } from '../../hooks/useRoomSubRooms';
type SpaceHierarchyProps = {
summary: IHierarchyRoom | undefined;
hierarchySummaries: Map<string, IHierarchyRoom>;
spaceItem: HierarchyItemSpace;
roomItems?: HierarchyItemRoom[];
allJoinedRooms: Set<string>;
@@ -39,12 +41,13 @@ type SpaceHierarchyProps = {
pinned: boolean;
togglePinToSidebar: (roomId: string) => void;
onSpacesFound: (spaceItems: IHierarchyRoom[]) => void;
onOpenRoom: MouseEventHandler<HTMLButtonElement>;
onOpenRoom?: MouseEventHandler<HTMLButtonElement>;
};
export const SpaceHierarchy = forwardRef<HTMLDivElement, SpaceHierarchyProps>(
(
{
summary,
hierarchySummaries,
spaceItem,
roomItems,
allJoinedRooms,
@@ -73,7 +76,7 @@ export const SpaceHierarchy = forwardRef<HTMLDivElement, SpaceHierarchyProps>(
const subspaces = useMemo(() => {
const s: Map<string, IHierarchyRoom> = new Map();
rooms.forEach((r) => {
if (r.room_type === RoomType.Space || r.room_type === RoomType.Forum) {
if (isHierarchySpaceRoom(r)) {
s.set(r.room_id, r);
}
});
@@ -97,8 +100,8 @@ export const SpaceHierarchy = forwardRef<HTMLDivElement, SpaceHierarchyProps>(
getRoomPermissionsAPI(parentCreators, parentPowerLevels);
useEffect(() => {
onSpacesFound(Array.from(subspaces.values()));
}, [subspaces, onSpacesFound]);
onSpacesFound(Array.from(rooms.values()));
}, [rooms, onSpacesFound]);
// Build a global set of all sub-room IDs by checking ALL joined rooms
// This ensures sub-rooms are hidden even if their parent isn't in this space
@@ -134,7 +137,11 @@ export const SpaceHierarchy = forwardRef<HTMLDivElement, SpaceHierarchyProps>(
// hide unknown rooms for normal user
childItems = childItems?.filter((i) => {
const forbidden = error instanceof MatrixError ? error.errcode === 'M_FORBIDDEN' : false;
const inaccessibleRoom = !rooms.get(i.roomId) && !fetching && (error ? forbidden : true);
const inaccessibleRoom =
!rooms.get(i.roomId) &&
!hierarchySummaries.has(i.roomId) &&
!fetching &&
(error ? forbidden : true);
return !inaccessibleRoom;
});
}
@@ -185,7 +192,7 @@ export const SpaceHierarchy = forwardRef<HTMLDivElement, SpaceHierarchyProps>(
{childItems && childItems.length > 0 ? (
<Box direction="Column" gap="100">
{childItems.map((roomItem, index) => {
const roomSummary = rooms.get(roomItem.roomId);
const roomSummary = rooms.get(roomItem.roomId) ?? hierarchySummaries.get(roomItem.roomId);
const roomPowerLevels = roomsPowerLevels.get(roomItem.roomId) ?? {};
@@ -238,7 +245,7 @@ export const SpaceHierarchy = forwardRef<HTMLDivElement, SpaceHierarchyProps>(
<Box direction="Column" gap="100" style={{ marginLeft: config.space.S400 }}>
{subRoomIds.map((subRoomId) => {
const subRoom = mx.getRoom(subRoomId);
const subRoomSummary = rooms.get(subRoomId);
const subRoomSummary = rooms.get(subRoomId) ?? hierarchySummaries.get(subRoomId);
const subRoomPowerLevels = roomsPowerLevels.get(subRoomId) ?? {};
const subRoomItem: HierarchyItemRoom = {
roomId: subRoomId,