feat: Enhance space hierarchy management by implementing asynchronous room fetching and improving room handling logic
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
import { atom, useAtom, useAtomValue } from 'jotai';
|
||||
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 { QueryFunction, useInfiniteQuery } from '@tanstack/react-query';
|
||||
import { useMatrixClient } from './useMatrixClient';
|
||||
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 { isRoomId } from '../utils/matrix';
|
||||
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) =>
|
||||
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 = (
|
||||
rootSpaceId: string,
|
||||
getRoom: GetRoomCallback,
|
||||
spaceRooms: Set<string>
|
||||
spaceRooms: Set<string>,
|
||||
hierarchySummaries: Map<string, IHierarchyRoom>
|
||||
): HierarchyItemSpace[] => {
|
||||
const rootSpaceItem: HierarchyItemSpace = {
|
||||
roomId: rootSpaceId,
|
||||
@@ -47,33 +125,47 @@ const getHierarchySpaces = (
|
||||
space: true,
|
||||
};
|
||||
let spaceItems: HierarchyItemSpace[] = [];
|
||||
const parentByRoomId = buildParentByRoomId(hierarchySummaries.values());
|
||||
|
||||
const findAndCollectHierarchySpaces = (spaceItem: HierarchyItemSpace) => {
|
||||
if (spaceItems.find((item) => item.roomId === spaceItem.roomId)) return;
|
||||
const space = getRoom(spaceItem.roomId);
|
||||
spaceItems.push(spaceItem);
|
||||
|
||||
if (!space) return;
|
||||
const childEvents = getStateEvents(space, StateEvent.SpaceChild);
|
||||
if (space) {
|
||||
const childEvents = getStateEvents(space, StateEvent.SpaceChild);
|
||||
|
||||
childEvents.forEach((childEvent) => {
|
||||
if (!isValidChild(childEvent)) return;
|
||||
const childId = childEvent.getStateKey();
|
||||
if (!childId || !isRoomId(childId)) return;
|
||||
childEvents.forEach((childEvent) => {
|
||||
if (!isValidChild(childEvent)) return;
|
||||
const childId = childEvent.getStateKey();
|
||||
if (!childId || !isRoomId(childId)) return;
|
||||
|
||||
// because we can not find if a childId is space without joining
|
||||
// or requesting room summary, we will look it into spaceRooms local
|
||||
// cache which we maintain as we load summary in UI.
|
||||
if (isSpace(getRoom(childId) ?? null) || spaceRooms.has(childId)) {
|
||||
const childItem: HierarchyItemSpace = {
|
||||
roomId: childId,
|
||||
content: childEvent.getContent<MSpaceChildContent>(),
|
||||
ts: childEvent.getTs(),
|
||||
space: true,
|
||||
parentId: spaceItem.roomId,
|
||||
};
|
||||
findAndCollectHierarchySpaces(childItem);
|
||||
}
|
||||
if (isChildSpace(childId, getRoom, spaceRooms, hierarchySummaries)) {
|
||||
const childItem: HierarchyItemSpace = {
|
||||
roomId: childId,
|
||||
content: childEvent.getContent<MSpaceChildContent>(),
|
||||
ts: childEvent.getTs(),
|
||||
space: true,
|
||||
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);
|
||||
@@ -97,13 +189,35 @@ const getSpaceHierarchy = (
|
||||
rootSpaceId: string,
|
||||
spaceRooms: Set<string>,
|
||||
getRoom: (roomId: string) => Room | undefined,
|
||||
closedCategory: (spaceId: string) => boolean
|
||||
closedCategory: (spaceId: string) => boolean,
|
||||
hierarchySummaries: Map<string, IHierarchyRoom>
|
||||
): 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 space = getRoom(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 {
|
||||
space: spaceItem,
|
||||
};
|
||||
@@ -114,7 +228,7 @@ const getSpaceHierarchy = (
|
||||
if (!isValidChild(childEvent)) return;
|
||||
const childId = childEvent.getStateKey();
|
||||
if (!childId || !isRoomId(childId)) return;
|
||||
if (isSpace(getRoom(childId) ?? null) || spaceRooms.has(childId)) return;
|
||||
if (isChildSpace(childId, getRoom, spaceRooms, hierarchySummaries)) return;
|
||||
|
||||
const childItem: HierarchyItemRoom = {
|
||||
roomId: childId,
|
||||
@@ -125,6 +239,20 @@ const getSpaceHierarchy = (
|
||||
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 {
|
||||
space: spaceItem,
|
||||
rooms: childItems.sort(hierarchyItemTs).sort(hierarchyItemByOrder),
|
||||
@@ -138,19 +266,22 @@ export const useSpaceHierarchy = (
|
||||
spaceId: string,
|
||||
spaceRooms: Set<string>,
|
||||
getRoom: (roomId: string) => Room | undefined,
|
||||
closedCategory: (spaceId: string) => boolean
|
||||
closedCategory: (spaceId: string) => boolean,
|
||||
hierarchySummaries: Map<string, IHierarchyRoom>
|
||||
): SpaceHierarchy[] => {
|
||||
const mx = useMatrixClient();
|
||||
const roomToParents = useAtomValue(roomToParentsAtom);
|
||||
|
||||
const [hierarchyAtom] = useState(() =>
|
||||
atom(getSpaceHierarchy(spaceId, spaceRooms, getRoom, closedCategory))
|
||||
atom(getSpaceHierarchy(spaceId, spaceRooms, getRoom, closedCategory, hierarchySummaries))
|
||||
);
|
||||
const [hierarchy, setHierarchy] = useAtom(hierarchyAtom);
|
||||
|
||||
useEffect(() => {
|
||||
setHierarchy(getSpaceHierarchy(spaceId, spaceRooms, getRoom, closedCategory));
|
||||
}, [mx, spaceId, spaceRooms, setHierarchy, getRoom, closedCategory]);
|
||||
setHierarchy(
|
||||
getSpaceHierarchy(spaceId, spaceRooms, getRoom, closedCategory, hierarchySummaries)
|
||||
);
|
||||
}, [mx, spaceId, spaceRooms, setHierarchy, getRoom, closedCategory, hierarchySummaries]);
|
||||
|
||||
useStateEventCallback(
|
||||
mx,
|
||||
@@ -161,10 +292,12 @@ export const useSpaceHierarchy = (
|
||||
if (!eventRoomId) return;
|
||||
|
||||
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,
|
||||
sortRoomItems: (parentId: string, items: 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 space = getRoom(spaceItem.roomId);
|
||||
@@ -266,9 +404,6 @@ export const useSpaceJoinedHierarchy = (
|
||||
return hierarchy;
|
||||
};
|
||||
|
||||
// we will paginate until 5000 items
|
||||
const PER_PAGE_COUNT = 100;
|
||||
const MAX_AUTO_PAGE_COUNT = 50;
|
||||
export type FetchSpaceHierarchyLevelData = {
|
||||
fetching: boolean;
|
||||
error: Error | null;
|
||||
|
||||
Reference in New Issue
Block a user