feat: Enhance space hierarchy management by implementing asynchronous room fetching and improving room handling logic
This commit is contained in:
@@ -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 { Box, Chip, IconButton, Line, Scroll, Spinner, Text, config } from 'folds';
|
||||||
import { Icon, Icons } from '../../components/icons';
|
import { Icon, Icons } from '../../components/icons';
|
||||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||||
@@ -13,6 +13,8 @@ import { Page, PageContent, PageContentCenter, PageHeroSection } from '../../com
|
|||||||
import {
|
import {
|
||||||
HierarchyItem,
|
HierarchyItem,
|
||||||
HierarchyItemSpace,
|
HierarchyItemSpace,
|
||||||
|
fetchAllHierarchyRooms,
|
||||||
|
isHierarchySpaceRoom,
|
||||||
useSpaceHierarchy,
|
useSpaceHierarchy,
|
||||||
} from '../../hooks/useSpaceHierarchy';
|
} from '../../hooks/useSpaceHierarchy';
|
||||||
import { VirtualTile } from '../../components/virtualizer';
|
import { VirtualTile } from '../../components/virtualizer';
|
||||||
@@ -41,7 +43,7 @@ import { getSpaceRoomPath } from '../../pages/pathUtils';
|
|||||||
import { StateEvent } from '../../../types/matrix/room';
|
import { StateEvent } from '../../../types/matrix/room';
|
||||||
import { CanDropCallback, useDnDMonitor } from './DnD';
|
import { CanDropCallback, useDnDMonitor } from './DnD';
|
||||||
import { ASCIILexicalTable, orderKeys } from '../../utils/ASCIILexicalTable';
|
import { ASCIILexicalTable, orderKeys } from '../../utils/ASCIILexicalTable';
|
||||||
import { getStateEvent } from '../../utils/room';
|
import { getStateEvent, shouldShowForumLobby } from '../../utils/room';
|
||||||
import { useClosedLobbyCategoriesAtom } from '../../state/hooks/closedLobbyCategories';
|
import { useClosedLobbyCategoriesAtom } from '../../state/hooks/closedLobbyCategories';
|
||||||
import {
|
import {
|
||||||
makeCinnySpacesContent,
|
makeCinnySpacesContent,
|
||||||
@@ -161,6 +163,7 @@ function SpaceCardLobby() {
|
|||||||
const allRooms = useAtomValue(allRoomsAtom);
|
const allRooms = useAtomValue(allRoomsAtom);
|
||||||
const allJoinedRooms = useMemo(() => new Set(allRooms), [allRooms]);
|
const allJoinedRooms = useMemo(() => new Set(allRooms), [allRooms]);
|
||||||
const space = useSpace();
|
const space = useSpace();
|
||||||
|
const isForum = shouldShowForumLobby(space);
|
||||||
const spacePowerLevels = usePowerLevels(space);
|
const spacePowerLevels = usePowerLevels(space);
|
||||||
const lex = useMemo(() => new ASCIILexicalTable(' '.charCodeAt(0), '~'.charCodeAt(0), 6), []);
|
const lex = useMemo(() => new ASCIILexicalTable(' '.charCodeAt(0), '~'.charCodeAt(0), 6), []);
|
||||||
const members = useRoomMembers(mx, space.roomId);
|
const members = useRoomMembers(mx, space.roomId);
|
||||||
@@ -189,6 +192,30 @@ function SpaceCardLobby() {
|
|||||||
|
|
||||||
const [spacesItems, setSpacesItem] = useState<Map<string, IHierarchyRoom>>(() => new Map());
|
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(
|
useElementSizeObserver(
|
||||||
useCallback(() => heroSectionRef.current, []),
|
useCallback(() => heroSectionRef.current, []),
|
||||||
useCallback((w, height) => setHeroSectionHeight(height), [])
|
useCallback((w, height) => setHeroSectionHeight(height), [])
|
||||||
@@ -206,7 +233,8 @@ function SpaceCardLobby() {
|
|||||||
closedCategories.has(makeLobbyCategoryId(space.roomId, childId)) ||
|
closedCategories.has(makeLobbyCategoryId(space.roomId, childId)) ||
|
||||||
(draggingItem ? 'space' in draggingItem : false),
|
(draggingItem ? 'space' in draggingItem : false),
|
||||||
[closedCategories, space.roomId, draggingItem]
|
[closedCategories, space.roomId, draggingItem]
|
||||||
)
|
),
|
||||||
|
spacesItems
|
||||||
);
|
);
|
||||||
|
|
||||||
const virtualizer = useVirtualizer({
|
const virtualizer = useVirtualizer({
|
||||||
@@ -400,14 +428,16 @@ function SpaceCardLobby() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleSpacesFound = useCallback(
|
const handleSpacesFound = useCallback(
|
||||||
(sItems: IHierarchyRoom[]) => {
|
(items: IHierarchyRoom[]) => {
|
||||||
setSpaceRooms({ type: 'PUT', roomIds: sItems.map((i) => i.room_id) });
|
setSpaceRooms({
|
||||||
setSpacesItem((current) => {
|
type: 'PUT',
|
||||||
const newItems = produce(current, (draft) => {
|
roomIds: items.filter(isHierarchySpaceRoom).map((i) => i.room_id),
|
||||||
sItems.forEach((item) => draft.set(item.room_id, item));
|
|
||||||
});
|
|
||||||
return current.size === newItems.size ? current : newItems;
|
|
||||||
});
|
});
|
||||||
|
setSpacesItem((current) =>
|
||||||
|
produce(current, (draft) => {
|
||||||
|
items.forEach((item) => draft.set(item.room_id, item));
|
||||||
|
})
|
||||||
|
);
|
||||||
},
|
},
|
||||||
[setSpaceRooms]
|
[setSpaceRooms]
|
||||||
);
|
);
|
||||||
@@ -491,6 +521,7 @@ function SpaceCardLobby() {
|
|||||||
<SpaceHierarchy
|
<SpaceHierarchy
|
||||||
spaceItem={item.space}
|
spaceItem={item.space}
|
||||||
summary={spacesItems.get(item.space.roomId)}
|
summary={spacesItems.get(item.space.roomId)}
|
||||||
|
hierarchySummaries={spacesItems}
|
||||||
roomItems={item.rooms}
|
roomItems={item.rooms}
|
||||||
allJoinedRooms={allJoinedRooms}
|
allJoinedRooms={allJoinedRooms}
|
||||||
mDirects={mDirects}
|
mDirects={mDirects}
|
||||||
@@ -510,7 +541,7 @@ function SpaceCardLobby() {
|
|||||||
pinned={sidebarSpaces.has(item.space.roomId)}
|
pinned={sidebarSpaces.has(item.space.roomId)}
|
||||||
togglePinToSidebar={togglePinToSidebar}
|
togglePinToSidebar={togglePinToSidebar}
|
||||||
onSpacesFound={handleSpacesFound}
|
onSpacesFound={handleSpacesFound}
|
||||||
onOpenRoom={handleOpenRoom}
|
onOpenRoom={isForum ? undefined : handleOpenRoom}
|
||||||
/>
|
/>
|
||||||
</VirtualTile>
|
</VirtualTile>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -267,7 +267,7 @@ type RoomItemCardProps = {
|
|||||||
dm?: boolean;
|
dm?: boolean;
|
||||||
firstChild?: boolean;
|
firstChild?: boolean;
|
||||||
lastChild?: boolean;
|
lastChild?: boolean;
|
||||||
onOpen: MouseEventHandler<HTMLButtonElement>;
|
onOpen?: MouseEventHandler<HTMLButtonElement>;
|
||||||
options?: ReactNode;
|
options?: ReactNode;
|
||||||
before?: ReactNode;
|
before?: ReactNode;
|
||||||
after?: ReactNode;
|
after?: ReactNode;
|
||||||
@@ -332,7 +332,7 @@ export const RoomItemCard = as<'div', RoomItemCardProps>(
|
|||||||
suggested={content.suggested}
|
suggested={content.suggested}
|
||||||
joinRule={localSummary.joinRule}
|
joinRule={localSummary.joinRule}
|
||||||
options={
|
options={
|
||||||
joined ? (
|
joined && onOpen ? (
|
||||||
<Box shrink="No" gap="100" alignItems="Center">
|
<Box shrink="No" gap="100" alignItems="Center">
|
||||||
<Chip
|
<Chip
|
||||||
data-room-id={roomId}
|
data-room-id={roomId}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
HierarchyItem,
|
HierarchyItem,
|
||||||
HierarchyItemRoom,
|
HierarchyItemRoom,
|
||||||
HierarchyItemSpace,
|
HierarchyItemSpace,
|
||||||
|
isHierarchySpaceRoom,
|
||||||
useFetchSpaceHierarchyLevel,
|
useFetchSpaceHierarchyLevel,
|
||||||
} from '../../hooks/useSpaceHierarchy';
|
} from '../../hooks/useSpaceHierarchy';
|
||||||
import { IPowerLevels } from '../../hooks/usePowerLevels';
|
import { IPowerLevels } from '../../hooks/usePowerLevels';
|
||||||
@@ -14,7 +15,7 @@ import { SpaceItemCard } from './SpaceItem';
|
|||||||
import { AfterItemDropTarget, CanDropCallback } from './DnD';
|
import { AfterItemDropTarget, CanDropCallback } from './DnD';
|
||||||
import { HierarchyItemMenu } from './HierarchyItemMenu';
|
import { HierarchyItemMenu } from './HierarchyItemMenu';
|
||||||
import { RoomItemCard } from './RoomItem';
|
import { RoomItemCard } from './RoomItem';
|
||||||
import { RoomType, StateEvent } from '../../../types/matrix/room';
|
import { StateEvent } from '../../../types/matrix/room';
|
||||||
import { SequenceCard } from '../../components/sequence-card';
|
import { SequenceCard } from '../../components/sequence-card';
|
||||||
import { getRoomCreatorsForRoomId } from '../../hooks/useRoomCreators';
|
import { getRoomCreatorsForRoomId } from '../../hooks/useRoomCreators';
|
||||||
import { getRoomPermissionsAPI } from '../../hooks/useRoomPermissions';
|
import { getRoomPermissionsAPI } from '../../hooks/useRoomPermissions';
|
||||||
@@ -22,6 +23,7 @@ import { PaarrotSubRoomsContent } from '../../hooks/useRoomSubRooms';
|
|||||||
|
|
||||||
type SpaceHierarchyProps = {
|
type SpaceHierarchyProps = {
|
||||||
summary: IHierarchyRoom | undefined;
|
summary: IHierarchyRoom | undefined;
|
||||||
|
hierarchySummaries: Map<string, IHierarchyRoom>;
|
||||||
spaceItem: HierarchyItemSpace;
|
spaceItem: HierarchyItemSpace;
|
||||||
roomItems?: HierarchyItemRoom[];
|
roomItems?: HierarchyItemRoom[];
|
||||||
allJoinedRooms: Set<string>;
|
allJoinedRooms: Set<string>;
|
||||||
@@ -39,12 +41,13 @@ type SpaceHierarchyProps = {
|
|||||||
pinned: boolean;
|
pinned: boolean;
|
||||||
togglePinToSidebar: (roomId: string) => void;
|
togglePinToSidebar: (roomId: string) => void;
|
||||||
onSpacesFound: (spaceItems: IHierarchyRoom[]) => void;
|
onSpacesFound: (spaceItems: IHierarchyRoom[]) => void;
|
||||||
onOpenRoom: MouseEventHandler<HTMLButtonElement>;
|
onOpenRoom?: MouseEventHandler<HTMLButtonElement>;
|
||||||
};
|
};
|
||||||
export const SpaceHierarchy = forwardRef<HTMLDivElement, SpaceHierarchyProps>(
|
export const SpaceHierarchy = forwardRef<HTMLDivElement, SpaceHierarchyProps>(
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
summary,
|
summary,
|
||||||
|
hierarchySummaries,
|
||||||
spaceItem,
|
spaceItem,
|
||||||
roomItems,
|
roomItems,
|
||||||
allJoinedRooms,
|
allJoinedRooms,
|
||||||
@@ -73,7 +76,7 @@ export const SpaceHierarchy = forwardRef<HTMLDivElement, SpaceHierarchyProps>(
|
|||||||
const subspaces = useMemo(() => {
|
const subspaces = useMemo(() => {
|
||||||
const s: Map<string, IHierarchyRoom> = new Map();
|
const s: Map<string, IHierarchyRoom> = new Map();
|
||||||
rooms.forEach((r) => {
|
rooms.forEach((r) => {
|
||||||
if (r.room_type === RoomType.Space || r.room_type === RoomType.Forum) {
|
if (isHierarchySpaceRoom(r)) {
|
||||||
s.set(r.room_id, r);
|
s.set(r.room_id, r);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -97,8 +100,8 @@ export const SpaceHierarchy = forwardRef<HTMLDivElement, SpaceHierarchyProps>(
|
|||||||
getRoomPermissionsAPI(parentCreators, parentPowerLevels);
|
getRoomPermissionsAPI(parentCreators, parentPowerLevels);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onSpacesFound(Array.from(subspaces.values()));
|
onSpacesFound(Array.from(rooms.values()));
|
||||||
}, [subspaces, onSpacesFound]);
|
}, [rooms, onSpacesFound]);
|
||||||
|
|
||||||
// Build a global set of all sub-room IDs by checking ALL joined rooms
|
// 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
|
// 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
|
// hide unknown rooms for normal user
|
||||||
childItems = childItems?.filter((i) => {
|
childItems = childItems?.filter((i) => {
|
||||||
const forbidden = error instanceof MatrixError ? error.errcode === 'M_FORBIDDEN' : false;
|
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;
|
return !inaccessibleRoom;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -185,7 +192,7 @@ export const SpaceHierarchy = forwardRef<HTMLDivElement, SpaceHierarchyProps>(
|
|||||||
{childItems && childItems.length > 0 ? (
|
{childItems && childItems.length > 0 ? (
|
||||||
<Box direction="Column" gap="100">
|
<Box direction="Column" gap="100">
|
||||||
{childItems.map((roomItem, index) => {
|
{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) ?? {};
|
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 }}>
|
<Box direction="Column" gap="100" style={{ marginLeft: config.space.S400 }}>
|
||||||
{subRoomIds.map((subRoomId) => {
|
{subRoomIds.map((subRoomId) => {
|
||||||
const subRoom = mx.getRoom(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 subRoomPowerLevels = roomsPowerLevels.get(subRoomId) ?? {};
|
||||||
const subRoomItem: HierarchyItemRoom = {
|
const subRoomItem: HierarchyItemRoom = {
|
||||||
roomId: subRoomId,
|
roomId: subRoomId,
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { atom, useAtom, useAtomValue } from 'jotai';
|
import { atom, useAtom, useAtomValue } from 'jotai';
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { MatrixError, Room } from 'matrix-js-sdk';
|
import { MatrixClient, MatrixError, Room } from 'matrix-js-sdk';
|
||||||
import { IHierarchyRoom } from 'matrix-js-sdk/lib/@types/spaces';
|
import { IHierarchyRoom } from 'matrix-js-sdk/lib/@types/spaces';
|
||||||
import { QueryFunction, useInfiniteQuery } from '@tanstack/react-query';
|
import { QueryFunction, useInfiniteQuery } from '@tanstack/react-query';
|
||||||
import { useMatrixClient } from './useMatrixClient';
|
import { useMatrixClient } from './useMatrixClient';
|
||||||
import { roomToParentsAtom } from '../state/room/roomToParents';
|
import { roomToParentsAtom } from '../state/room/roomToParents';
|
||||||
import { MSpaceChildContent, StateEvent } from '../../types/matrix/room';
|
import { MSpaceChildContent, RoomType, StateEvent } from '../../types/matrix/room';
|
||||||
import { getAllParents, getStateEvents, isSpace, isValidChild } from '../utils/room';
|
import { getAllParents, getStateEvents, isSpace, isValidChild } from '../utils/room';
|
||||||
import { isRoomId } from '../utils/matrix';
|
import { isRoomId } from '../utils/matrix';
|
||||||
import { SortFunc, byOrderKey, byTsOldToNew, factoryRoomIdByActivity } from '../utils/sort';
|
import { SortFunc, byOrderKey, byTsOldToNew, factoryRoomIdByActivity } from '../utils/sort';
|
||||||
@@ -35,10 +35,88 @@ const hierarchyItemTs: SortFunc<HierarchyItem> = (a, b) => byTsOldToNew(a.ts, b.
|
|||||||
const hierarchyItemByOrder: SortFunc<HierarchyItem> = (a, b) =>
|
const hierarchyItemByOrder: SortFunc<HierarchyItem> = (a, b) =>
|
||||||
byOrderKey(a.content.order, b.content.order);
|
byOrderKey(a.content.order, b.content.order);
|
||||||
|
|
||||||
|
// we will paginate until 5000 items
|
||||||
|
const PER_PAGE_COUNT = 100;
|
||||||
|
const MAX_AUTO_PAGE_COUNT = 50;
|
||||||
|
|
||||||
|
export function buildParentByRoomId(rooms: Iterable<IHierarchyRoom>): Map<string, string> {
|
||||||
|
const parentByRoomId = new Map<string, string>();
|
||||||
|
for (const room of rooms) {
|
||||||
|
if (!room?.room_id || !Array.isArray(room.children_state)) continue;
|
||||||
|
for (const childState of room.children_state) {
|
||||||
|
if (childState?.type !== StateEvent.SpaceChild) continue;
|
||||||
|
if (typeof childState.state_key !== 'string') continue;
|
||||||
|
parentByRoomId.set(childState.state_key, room.room_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return parentByRoomId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Space or forum container in hierarchy API (room_type may be absent on some servers). */
|
||||||
|
export function isHierarchySpaceRoom(room: IHierarchyRoom | undefined): boolean {
|
||||||
|
if (!room) return false;
|
||||||
|
if (room.room_type === RoomType.Space || room.room_type === RoomType.Forum) return true;
|
||||||
|
if (Array.isArray(room.children_state)) {
|
||||||
|
return room.children_state.some((c) => c?.type === StateEvent.SpaceChild);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchAllHierarchyRooms(
|
||||||
|
mx: MatrixClient,
|
||||||
|
spaceId: string,
|
||||||
|
maxDepth = 3
|
||||||
|
): Promise<IHierarchyRoom[]> {
|
||||||
|
const allRooms: IHierarchyRoom[] = [];
|
||||||
|
let nextBatch: string | undefined;
|
||||||
|
|
||||||
|
for (let page = 0; page < MAX_AUTO_PAGE_COUNT; page += 1) {
|
||||||
|
const result = await mx.getRoomHierarchy(spaceId, PER_PAGE_COUNT, maxDepth, false, nextBatch);
|
||||||
|
allRooms.push(...result.rooms);
|
||||||
|
nextBatch = result.next_batch;
|
||||||
|
if (!nextBatch) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return allRooms;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isChildSpace = (
|
||||||
|
childId: string,
|
||||||
|
getRoom: GetRoomCallback,
|
||||||
|
spaceRooms: Set<string>,
|
||||||
|
hierarchySummaries: Map<string, IHierarchyRoom>
|
||||||
|
): boolean =>
|
||||||
|
isSpace(getRoom(childId) ?? null) ||
|
||||||
|
spaceRooms.has(childId) ||
|
||||||
|
isHierarchySpaceRoom(hierarchySummaries.get(childId));
|
||||||
|
|
||||||
|
const getChildRoomsFromSummaries = (
|
||||||
|
spaceItemId: string,
|
||||||
|
hierarchySummaries: Map<string, IHierarchyRoom>,
|
||||||
|
parentByRoomId: Map<string, string>,
|
||||||
|
spaceRooms: Set<string>,
|
||||||
|
getRoom: GetRoomCallback
|
||||||
|
): HierarchyItemRoom[] => {
|
||||||
|
const childItems: HierarchyItemRoom[] = [];
|
||||||
|
hierarchySummaries.forEach((summary, roomId) => {
|
||||||
|
if (parentByRoomId.get(roomId) !== spaceItemId) return;
|
||||||
|
if (isChildSpace(roomId, getRoom, spaceRooms, hierarchySummaries)) return;
|
||||||
|
|
||||||
|
childItems.push({
|
||||||
|
roomId,
|
||||||
|
content: { via: summary.via ?? [] },
|
||||||
|
ts: 0,
|
||||||
|
parentId: spaceItemId,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return childItems;
|
||||||
|
};
|
||||||
|
|
||||||
const getHierarchySpaces = (
|
const getHierarchySpaces = (
|
||||||
rootSpaceId: string,
|
rootSpaceId: string,
|
||||||
getRoom: GetRoomCallback,
|
getRoom: GetRoomCallback,
|
||||||
spaceRooms: Set<string>
|
spaceRooms: Set<string>,
|
||||||
|
hierarchySummaries: Map<string, IHierarchyRoom>
|
||||||
): HierarchyItemSpace[] => {
|
): HierarchyItemSpace[] => {
|
||||||
const rootSpaceItem: HierarchyItemSpace = {
|
const rootSpaceItem: HierarchyItemSpace = {
|
||||||
roomId: rootSpaceId,
|
roomId: rootSpaceId,
|
||||||
@@ -47,33 +125,47 @@ const getHierarchySpaces = (
|
|||||||
space: true,
|
space: true,
|
||||||
};
|
};
|
||||||
let spaceItems: HierarchyItemSpace[] = [];
|
let spaceItems: HierarchyItemSpace[] = [];
|
||||||
|
const parentByRoomId = buildParentByRoomId(hierarchySummaries.values());
|
||||||
|
|
||||||
const findAndCollectHierarchySpaces = (spaceItem: HierarchyItemSpace) => {
|
const findAndCollectHierarchySpaces = (spaceItem: HierarchyItemSpace) => {
|
||||||
if (spaceItems.find((item) => item.roomId === spaceItem.roomId)) return;
|
if (spaceItems.find((item) => item.roomId === spaceItem.roomId)) return;
|
||||||
const space = getRoom(spaceItem.roomId);
|
const space = getRoom(spaceItem.roomId);
|
||||||
spaceItems.push(spaceItem);
|
spaceItems.push(spaceItem);
|
||||||
|
|
||||||
if (!space) return;
|
if (space) {
|
||||||
const childEvents = getStateEvents(space, StateEvent.SpaceChild);
|
const childEvents = getStateEvents(space, StateEvent.SpaceChild);
|
||||||
|
|
||||||
childEvents.forEach((childEvent) => {
|
childEvents.forEach((childEvent) => {
|
||||||
if (!isValidChild(childEvent)) return;
|
if (!isValidChild(childEvent)) return;
|
||||||
const childId = childEvent.getStateKey();
|
const childId = childEvent.getStateKey();
|
||||||
if (!childId || !isRoomId(childId)) return;
|
if (!childId || !isRoomId(childId)) return;
|
||||||
|
|
||||||
// because we can not find if a childId is space without joining
|
if (isChildSpace(childId, getRoom, spaceRooms, hierarchySummaries)) {
|
||||||
// or requesting room summary, we will look it into spaceRooms local
|
const childItem: HierarchyItemSpace = {
|
||||||
// cache which we maintain as we load summary in UI.
|
roomId: childId,
|
||||||
if (isSpace(getRoom(childId) ?? null) || spaceRooms.has(childId)) {
|
content: childEvent.getContent<MSpaceChildContent>(),
|
||||||
const childItem: HierarchyItemSpace = {
|
ts: childEvent.getTs(),
|
||||||
roomId: childId,
|
space: true,
|
||||||
content: childEvent.getContent<MSpaceChildContent>(),
|
parentId: spaceItem.roomId,
|
||||||
ts: childEvent.getTs(),
|
};
|
||||||
space: true,
|
findAndCollectHierarchySpaces(childItem);
|
||||||
parentId: spaceItem.roomId,
|
}
|
||||||
};
|
});
|
||||||
findAndCollectHierarchySpaces(childItem);
|
}
|
||||||
}
|
|
||||||
|
// Unjoined sub-spaces: discover children from hierarchy API summaries.
|
||||||
|
hierarchySummaries.forEach((summary, roomId) => {
|
||||||
|
if (parentByRoomId.get(roomId) !== spaceItem.roomId) return;
|
||||||
|
if (!isChildSpace(roomId, getRoom, spaceRooms, hierarchySummaries)) return;
|
||||||
|
if (spaceItems.find((item) => item.roomId === roomId)) return;
|
||||||
|
|
||||||
|
findAndCollectHierarchySpaces({
|
||||||
|
roomId,
|
||||||
|
content: { via: summary.via ?? [] },
|
||||||
|
ts: 0,
|
||||||
|
space: true,
|
||||||
|
parentId: spaceItem.roomId,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
findAndCollectHierarchySpaces(rootSpaceItem);
|
findAndCollectHierarchySpaces(rootSpaceItem);
|
||||||
@@ -97,13 +189,35 @@ const getSpaceHierarchy = (
|
|||||||
rootSpaceId: string,
|
rootSpaceId: string,
|
||||||
spaceRooms: Set<string>,
|
spaceRooms: Set<string>,
|
||||||
getRoom: (roomId: string) => Room | undefined,
|
getRoom: (roomId: string) => Room | undefined,
|
||||||
closedCategory: (spaceId: string) => boolean
|
closedCategory: (spaceId: string) => boolean,
|
||||||
|
hierarchySummaries: Map<string, IHierarchyRoom>
|
||||||
): SpaceHierarchy[] => {
|
): SpaceHierarchy[] => {
|
||||||
const spaceItems: HierarchyItemSpace[] = getHierarchySpaces(rootSpaceId, getRoom, spaceRooms);
|
const spaceItems: HierarchyItemSpace[] = getHierarchySpaces(
|
||||||
|
rootSpaceId,
|
||||||
|
getRoom,
|
||||||
|
spaceRooms,
|
||||||
|
hierarchySummaries
|
||||||
|
);
|
||||||
|
const parentByRoomId = buildParentByRoomId(hierarchySummaries.values());
|
||||||
|
|
||||||
const hierarchy: SpaceHierarchy[] = spaceItems.map((spaceItem) => {
|
const hierarchy: SpaceHierarchy[] = spaceItems.map((spaceItem) => {
|
||||||
const space = getRoom(spaceItem.roomId);
|
const space = getRoom(spaceItem.roomId);
|
||||||
if (!space || closedCategory(spaceItem.roomId)) {
|
if (!space || closedCategory(spaceItem.roomId)) {
|
||||||
|
if (!space && !closedCategory(spaceItem.roomId) && hierarchySummaries.size > 0) {
|
||||||
|
const summaryRooms = getChildRoomsFromSummaries(
|
||||||
|
spaceItem.roomId,
|
||||||
|
hierarchySummaries,
|
||||||
|
parentByRoomId,
|
||||||
|
spaceRooms,
|
||||||
|
getRoom
|
||||||
|
);
|
||||||
|
if (summaryRooms.length > 0) {
|
||||||
|
return {
|
||||||
|
space: spaceItem,
|
||||||
|
rooms: summaryRooms.sort(hierarchyItemTs).sort(hierarchyItemByOrder),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
space: spaceItem,
|
space: spaceItem,
|
||||||
};
|
};
|
||||||
@@ -114,7 +228,7 @@ const getSpaceHierarchy = (
|
|||||||
if (!isValidChild(childEvent)) return;
|
if (!isValidChild(childEvent)) return;
|
||||||
const childId = childEvent.getStateKey();
|
const childId = childEvent.getStateKey();
|
||||||
if (!childId || !isRoomId(childId)) return;
|
if (!childId || !isRoomId(childId)) return;
|
||||||
if (isSpace(getRoom(childId) ?? null) || spaceRooms.has(childId)) return;
|
if (isChildSpace(childId, getRoom, spaceRooms, hierarchySummaries)) return;
|
||||||
|
|
||||||
const childItem: HierarchyItemRoom = {
|
const childItem: HierarchyItemRoom = {
|
||||||
roomId: childId,
|
roomId: childId,
|
||||||
@@ -125,6 +239,20 @@ const getSpaceHierarchy = (
|
|||||||
childItems.push(childItem);
|
childItems.push(childItem);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const summaryRooms = getChildRoomsFromSummaries(
|
||||||
|
spaceItem.roomId,
|
||||||
|
hierarchySummaries,
|
||||||
|
parentByRoomId,
|
||||||
|
spaceRooms,
|
||||||
|
getRoom
|
||||||
|
);
|
||||||
|
const roomIds = new Set(childItems.map((i) => i.roomId));
|
||||||
|
summaryRooms.forEach((item) => {
|
||||||
|
if (!roomIds.has(item.roomId)) {
|
||||||
|
childItems.push(item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
space: spaceItem,
|
space: spaceItem,
|
||||||
rooms: childItems.sort(hierarchyItemTs).sort(hierarchyItemByOrder),
|
rooms: childItems.sort(hierarchyItemTs).sort(hierarchyItemByOrder),
|
||||||
@@ -138,19 +266,22 @@ export const useSpaceHierarchy = (
|
|||||||
spaceId: string,
|
spaceId: string,
|
||||||
spaceRooms: Set<string>,
|
spaceRooms: Set<string>,
|
||||||
getRoom: (roomId: string) => Room | undefined,
|
getRoom: (roomId: string) => Room | undefined,
|
||||||
closedCategory: (spaceId: string) => boolean
|
closedCategory: (spaceId: string) => boolean,
|
||||||
|
hierarchySummaries: Map<string, IHierarchyRoom>
|
||||||
): SpaceHierarchy[] => {
|
): SpaceHierarchy[] => {
|
||||||
const mx = useMatrixClient();
|
const mx = useMatrixClient();
|
||||||
const roomToParents = useAtomValue(roomToParentsAtom);
|
const roomToParents = useAtomValue(roomToParentsAtom);
|
||||||
|
|
||||||
const [hierarchyAtom] = useState(() =>
|
const [hierarchyAtom] = useState(() =>
|
||||||
atom(getSpaceHierarchy(spaceId, spaceRooms, getRoom, closedCategory))
|
atom(getSpaceHierarchy(spaceId, spaceRooms, getRoom, closedCategory, hierarchySummaries))
|
||||||
);
|
);
|
||||||
const [hierarchy, setHierarchy] = useAtom(hierarchyAtom);
|
const [hierarchy, setHierarchy] = useAtom(hierarchyAtom);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setHierarchy(getSpaceHierarchy(spaceId, spaceRooms, getRoom, closedCategory));
|
setHierarchy(
|
||||||
}, [mx, spaceId, spaceRooms, setHierarchy, getRoom, closedCategory]);
|
getSpaceHierarchy(spaceId, spaceRooms, getRoom, closedCategory, hierarchySummaries)
|
||||||
|
);
|
||||||
|
}, [mx, spaceId, spaceRooms, setHierarchy, getRoom, closedCategory, hierarchySummaries]);
|
||||||
|
|
||||||
useStateEventCallback(
|
useStateEventCallback(
|
||||||
mx,
|
mx,
|
||||||
@@ -161,10 +292,12 @@ export const useSpaceHierarchy = (
|
|||||||
if (!eventRoomId) return;
|
if (!eventRoomId) return;
|
||||||
|
|
||||||
if (spaceId === eventRoomId || getAllParents(roomToParents, eventRoomId).has(spaceId)) {
|
if (spaceId === eventRoomId || getAllParents(roomToParents, eventRoomId).has(spaceId)) {
|
||||||
setHierarchy(getSpaceHierarchy(spaceId, spaceRooms, getRoom, closedCategory));
|
setHierarchy(
|
||||||
|
getSpaceHierarchy(spaceId, spaceRooms, getRoom, closedCategory, hierarchySummaries)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[spaceId, roomToParents, setHierarchy, spaceRooms, getRoom, closedCategory]
|
[spaceId, roomToParents, setHierarchy, spaceRooms, getRoom, closedCategory, hierarchySummaries]
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -177,7 +310,12 @@ const getSpaceJoinedHierarchy = (
|
|||||||
excludeRoom: (parentId: string, roomId: string) => boolean,
|
excludeRoom: (parentId: string, roomId: string) => boolean,
|
||||||
sortRoomItems: (parentId: string, items: HierarchyItem[]) => HierarchyItem[]
|
sortRoomItems: (parentId: string, items: HierarchyItem[]) => HierarchyItem[]
|
||||||
): HierarchyItem[] => {
|
): HierarchyItem[] => {
|
||||||
const spaceItems: HierarchyItemSpace[] = getHierarchySpaces(rootSpaceId, getRoom, new Set());
|
const spaceItems: HierarchyItemSpace[] = getHierarchySpaces(
|
||||||
|
rootSpaceId,
|
||||||
|
getRoom,
|
||||||
|
new Set(),
|
||||||
|
new Map()
|
||||||
|
);
|
||||||
|
|
||||||
const hierarchy: HierarchyItem[] = spaceItems.flatMap((spaceItem) => {
|
const hierarchy: HierarchyItem[] = spaceItems.flatMap((spaceItem) => {
|
||||||
const space = getRoom(spaceItem.roomId);
|
const space = getRoom(spaceItem.roomId);
|
||||||
@@ -266,9 +404,6 @@ export const useSpaceJoinedHierarchy = (
|
|||||||
return hierarchy;
|
return hierarchy;
|
||||||
};
|
};
|
||||||
|
|
||||||
// we will paginate until 5000 items
|
|
||||||
const PER_PAGE_COUNT = 100;
|
|
||||||
const MAX_AUTO_PAGE_COUNT = 50;
|
|
||||||
export type FetchSpaceHierarchyLevelData = {
|
export type FetchSpaceHierarchyLevelData = {
|
||||||
fetching: boolean;
|
fetching: boolean;
|
||||||
error: Error | null;
|
error: Error | null;
|
||||||
|
|||||||
@@ -339,6 +339,7 @@ export function Space() {
|
|||||||
// Forum feed unless user opened manage, search, or a room (not tied to URL alias encoding).
|
// Forum feed unless user opened manage, search, or a room (not tied to URL alias encoding).
|
||||||
const feedActive = isForum && !lobbySelected && !searchSelected && !selectedRoomId;
|
const feedActive = isForum && !lobbySelected && !searchSelected && !selectedRoomId;
|
||||||
const showForumFeedSidebar = feedActive;
|
const showForumFeedSidebar = feedActive;
|
||||||
|
const forumManageMode = isForum && lobbySelected;
|
||||||
|
|
||||||
if (showForumFeedSidebar) {
|
if (showForumFeedSidebar) {
|
||||||
return (
|
return (
|
||||||
@@ -351,6 +352,14 @@ export function Space() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (forumManageMode) {
|
||||||
|
return (
|
||||||
|
<PageNav>
|
||||||
|
<SpaceHeader />
|
||||||
|
</PageNav>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const spacePath = getCanonicalAliasOrRoomId(mx, space.roomId);
|
const spacePath = getCanonicalAliasOrRoomId(mx, space.roomId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
Reference in New Issue
Block a user