323 lines
12 KiB
TypeScript
323 lines
12 KiB
TypeScript
import React, { forwardRef, MouseEventHandler, useEffect, useMemo } from 'react';
|
|
import { MatrixError, Room } from 'matrix-js-sdk';
|
|
import { IHierarchyRoom } from 'matrix-js-sdk/lib/@types/spaces';
|
|
import { Box, config, Text } from 'folds';
|
|
import {
|
|
HierarchyItem,
|
|
HierarchyItemRoom,
|
|
HierarchyItemSpace,
|
|
isHierarchySpaceRoom,
|
|
useFetchSpaceHierarchyLevel,
|
|
} from '../../hooks/useSpaceHierarchy';
|
|
import { IPowerLevels } from '../../hooks/usePowerLevels';
|
|
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
|
import { SpaceItemCard } from './SpaceItem';
|
|
import { AfterItemDropTarget, CanDropCallback } from './DnD';
|
|
import { HierarchyItemMenu } from './HierarchyItemMenu';
|
|
import { RoomItemCard } from './RoomItem';
|
|
import { StateEvent } from '../../../types/matrix/room';
|
|
import { SequenceCard } from '../../components/sequence-card';
|
|
import { getRoomCreatorsForRoomId } from '../../hooks/useRoomCreators';
|
|
import { getRoomPermissionsAPI } from '../../hooks/useRoomPermissions';
|
|
import { PaarrotSubRoomsContent } from '../../hooks/useRoomSubRooms';
|
|
|
|
type SpaceHierarchyProps = {
|
|
summary: IHierarchyRoom | undefined;
|
|
hierarchySummaries: Map<string, IHierarchyRoom>;
|
|
spaceItem: HierarchyItemSpace;
|
|
roomItems?: HierarchyItemRoom[];
|
|
allJoinedRooms: Set<string>;
|
|
mDirects: Set<string>;
|
|
roomsPowerLevels: Map<string, IPowerLevels>;
|
|
categoryId: string;
|
|
closed: boolean;
|
|
handleClose: MouseEventHandler<HTMLButtonElement>;
|
|
draggingItem?: HierarchyItem;
|
|
onDragging: (item?: HierarchyItem) => void;
|
|
canDrop: CanDropCallback;
|
|
disabledReorder?: boolean;
|
|
nextSpaceId?: string;
|
|
getRoom: (roomId: string) => Room | undefined;
|
|
pinned: boolean;
|
|
togglePinToSidebar: (roomId: string) => void;
|
|
onSpacesFound: (spaceItems: IHierarchyRoom[]) => void;
|
|
onOpenRoom?: MouseEventHandler<HTMLButtonElement>;
|
|
};
|
|
export const SpaceHierarchy = forwardRef<HTMLDivElement, SpaceHierarchyProps>(
|
|
(
|
|
{
|
|
summary,
|
|
hierarchySummaries,
|
|
spaceItem,
|
|
roomItems,
|
|
allJoinedRooms,
|
|
mDirects,
|
|
roomsPowerLevels,
|
|
categoryId,
|
|
closed,
|
|
handleClose,
|
|
draggingItem,
|
|
onDragging,
|
|
canDrop,
|
|
disabledReorder,
|
|
nextSpaceId,
|
|
getRoom,
|
|
pinned,
|
|
togglePinToSidebar,
|
|
onOpenRoom,
|
|
onSpacesFound,
|
|
},
|
|
ref
|
|
) => {
|
|
const mx = useMatrixClient();
|
|
|
|
const { fetching, error, rooms } = useFetchSpaceHierarchyLevel(spaceItem.roomId, true);
|
|
|
|
const subspaces = useMemo(() => {
|
|
const s: Map<string, IHierarchyRoom> = new Map();
|
|
rooms.forEach((r) => {
|
|
if (isHierarchySpaceRoom(r)) {
|
|
s.set(r.room_id, r);
|
|
}
|
|
});
|
|
return s;
|
|
}, [rooms]);
|
|
|
|
const spacePowerLevels = roomsPowerLevels.get(spaceItem.roomId);
|
|
const spaceCreators = getRoomCreatorsForRoomId(mx, spaceItem.roomId);
|
|
const spacePermissions =
|
|
spacePowerLevels && getRoomPermissionsAPI(spaceCreators, spacePowerLevels);
|
|
|
|
const draggingSpace =
|
|
draggingItem?.roomId === spaceItem.roomId && draggingItem.parentId === spaceItem.parentId;
|
|
|
|
const { parentId } = spaceItem;
|
|
const parentPowerLevels = parentId ? roomsPowerLevels.get(parentId) : undefined;
|
|
const parentCreators = parentId ? getRoomCreatorsForRoomId(mx, parentId) : undefined;
|
|
const parentPermissions =
|
|
parentCreators &&
|
|
parentPowerLevels &&
|
|
getRoomPermissionsAPI(parentCreators, parentPowerLevels);
|
|
|
|
useEffect(() => {
|
|
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
|
|
const globalSubRoomIds = useMemo(() => {
|
|
const subRoomIds = new Set<string>();
|
|
mx.getRooms().forEach((room) => {
|
|
const subRoomsEvent = room.currentState.getStateEvents(StateEvent.PaarrotSubRooms, '');
|
|
const content = subRoomsEvent?.getContent<PaarrotSubRoomsContent>();
|
|
content?.children?.forEach((childId: string) => subRoomIds.add(childId));
|
|
});
|
|
return subRoomIds;
|
|
}, [mx, allJoinedRooms]);
|
|
|
|
// Build a map of parent room -> sub-room IDs for nested rendering (only for rooms in this space)
|
|
const subRoomsMap = useMemo(() => {
|
|
const parentToSubRooms = new Map<string, string[]>();
|
|
roomItems?.forEach((item) => {
|
|
const room = mx.getRoom(item.roomId);
|
|
if (room) {
|
|
const subRoomsEvent = room.currentState.getStateEvents(StateEvent.PaarrotSubRooms, '');
|
|
const content = subRoomsEvent?.getContent<PaarrotSubRoomsContent>();
|
|
const children = content?.children ?? [];
|
|
if (children.length > 0) {
|
|
parentToSubRooms.set(item.roomId, children);
|
|
}
|
|
}
|
|
});
|
|
return parentToSubRooms;
|
|
}, [mx, roomItems]);
|
|
|
|
let childItems = roomItems?.filter((i) => !subspaces.has(i.roomId) && !globalSubRoomIds.has(i.roomId));
|
|
// Only hide when we know the user cannot edit children. If power levels are
|
|
// still loading (spacePermissions undefined), keep inaccessible rows visible
|
|
// so admins can clean them up.
|
|
const canEditChild = spacePermissions?.stateEvent(StateEvent.SpaceChild, mx.getSafeUserId());
|
|
if (spacePermissions && !canEditChild) {
|
|
// 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) &&
|
|
!hierarchySummaries.has(i.roomId) &&
|
|
!fetching &&
|
|
(error ? forbidden : true);
|
|
return !inaccessibleRoom;
|
|
});
|
|
}
|
|
|
|
return (
|
|
<Box direction="Column" gap="100" ref={ref}>
|
|
<SpaceItemCard
|
|
summary={rooms.get(spaceItem.roomId) ?? summary}
|
|
loading={fetching}
|
|
item={spaceItem}
|
|
joined={allJoinedRooms.has(spaceItem.roomId)}
|
|
categoryId={categoryId}
|
|
closed={closed}
|
|
handleClose={handleClose}
|
|
getRoom={getRoom}
|
|
canEditChild={!!spacePermissions?.stateEvent(StateEvent.SpaceChild, mx.getSafeUserId())}
|
|
canReorder={
|
|
parentPowerLevels && !disabledReorder && parentPermissions
|
|
? parentPermissions.stateEvent(StateEvent.SpaceChild, mx.getSafeUserId())
|
|
: false
|
|
}
|
|
options={
|
|
parentId &&
|
|
parentPowerLevels && (
|
|
<HierarchyItemMenu
|
|
item={{ ...spaceItem, parentId }}
|
|
powerLevels={spacePowerLevels}
|
|
joined={allJoinedRooms.has(spaceItem.roomId)}
|
|
canEditChild={
|
|
!!parentPermissions?.stateEvent(StateEvent.SpaceChild, mx.getSafeUserId())
|
|
}
|
|
pinned={pinned}
|
|
onTogglePin={togglePinToSidebar}
|
|
/>
|
|
)
|
|
}
|
|
after={
|
|
<AfterItemDropTarget
|
|
item={spaceItem}
|
|
nextRoomId={closed ? nextSpaceId : childItems?.[0]?.roomId}
|
|
afterSpace
|
|
canDrop={canDrop}
|
|
/>
|
|
}
|
|
onDragging={onDragging}
|
|
data-dragging={draggingSpace}
|
|
/>
|
|
{childItems && childItems.length > 0 ? (
|
|
<Box direction="Column" gap="100">
|
|
{childItems.map((roomItem, index) => {
|
|
const roomSummary = rooms.get(roomItem.roomId) ?? hierarchySummaries.get(roomItem.roomId);
|
|
|
|
const roomPowerLevels = roomsPowerLevels.get(roomItem.roomId) ?? {};
|
|
|
|
const lastItem = index === childItems.length;
|
|
const nextRoomId = lastItem ? nextSpaceId : childItems[index + 1]?.roomId;
|
|
|
|
const roomDragging =
|
|
draggingItem?.roomId === roomItem.roomId &&
|
|
draggingItem.parentId === roomItem.parentId;
|
|
|
|
// Get sub-rooms for this room
|
|
const subRoomIds = subRoomsMap.get(roomItem.roomId) ?? [];
|
|
|
|
return (
|
|
<React.Fragment key={roomItem.roomId}>
|
|
<RoomItemCard
|
|
item={roomItem}
|
|
loading={fetching}
|
|
error={error}
|
|
summary={roomSummary}
|
|
dm={mDirects.has(roomItem.roomId)}
|
|
onOpen={onOpenRoom}
|
|
getRoom={getRoom}
|
|
canReorder={
|
|
!!spacePermissions?.stateEvent(StateEvent.SpaceChild, mx.getSafeUserId()) &&
|
|
!disabledReorder
|
|
}
|
|
options={
|
|
<HierarchyItemMenu
|
|
item={roomItem}
|
|
powerLevels={roomPowerLevels}
|
|
joined={allJoinedRooms.has(roomItem.roomId)}
|
|
canEditChild={
|
|
!!spacePermissions?.stateEvent(StateEvent.SpaceChild, mx.getSafeUserId())
|
|
}
|
|
/>
|
|
}
|
|
after={
|
|
<AfterItemDropTarget
|
|
item={roomItem}
|
|
nextRoomId={nextRoomId}
|
|
canDrop={canDrop}
|
|
/>
|
|
}
|
|
data-dragging={roomDragging}
|
|
onDragging={onDragging}
|
|
/>
|
|
{/* Render sub-rooms nested under this room */}
|
|
{subRoomIds.length > 0 && (
|
|
<Box direction="Column" gap="100" style={{ marginLeft: config.space.S400 }}>
|
|
{subRoomIds.map((subRoomId) => {
|
|
const subRoom = mx.getRoom(subRoomId);
|
|
const subRoomSummary = rooms.get(subRoomId) ?? hierarchySummaries.get(subRoomId);
|
|
const subRoomPowerLevels = roomsPowerLevels.get(subRoomId) ?? {};
|
|
const subRoomItem: HierarchyItemRoom = {
|
|
roomId: subRoomId,
|
|
parentId: roomItem.roomId,
|
|
content: {},
|
|
};
|
|
const subRoomDragging =
|
|
draggingItem?.roomId === subRoomId &&
|
|
draggingItem.parentId === roomItem.roomId;
|
|
|
|
// Skip sub-rooms the user hasn't joined yet if we have no summary
|
|
if (!subRoom && !subRoomSummary) return null;
|
|
|
|
return (
|
|
<RoomItemCard
|
|
key={subRoomId}
|
|
item={subRoomItem}
|
|
loading={fetching}
|
|
error={error}
|
|
summary={subRoomSummary}
|
|
dm={mDirects.has(subRoomId)}
|
|
onOpen={onOpenRoom}
|
|
getRoom={getRoom}
|
|
canReorder={false}
|
|
options={
|
|
<HierarchyItemMenu
|
|
item={subRoomItem}
|
|
powerLevels={subRoomPowerLevels}
|
|
joined={allJoinedRooms.has(subRoomId)}
|
|
canEditChild={false}
|
|
/>
|
|
}
|
|
data-dragging={subRoomDragging}
|
|
onDragging={onDragging}
|
|
/>
|
|
);
|
|
})}
|
|
</Box>
|
|
)}
|
|
</React.Fragment>
|
|
);
|
|
})}
|
|
</Box>
|
|
) : (
|
|
childItems && (
|
|
<SequenceCard variant="SurfaceVariant" gap="300" alignItems="Center">
|
|
<Box
|
|
grow="Yes"
|
|
style={{
|
|
padding: config.space.S700,
|
|
}}
|
|
direction="Column"
|
|
alignItems="Center"
|
|
justifyContent="Center"
|
|
gap="100"
|
|
>
|
|
<Text size="H5" align="Center">
|
|
No Rooms
|
|
</Text>
|
|
<Text align="Center" size="T300" priority="300">
|
|
This space does not contains rooms yet.
|
|
</Text>
|
|
</Box>
|
|
</SequenceCard>
|
|
)
|
|
)}
|
|
</Box>
|
|
);
|
|
}
|
|
);
|