feat: Implement sub-room functionality

- Added support for sub-rooms in the SpaceHierarchy component, allowing rooms to have nested child rooms.
- Introduced UnjoinedSubRoomItem component to display unjoined sub-rooms with a join button.
- Created SubRooms settings page for managing sub-rooms, including adding and removing sub-rooms.
- Updated RoomNavItem to include options for creating sub-rooms.
- Enhanced room fetching logic to filter out sub-rooms from the main room list.
- Added hooks for managing sub-rooms state and permissions.
- Updated relevant state management to handle sub-room creation and association with parent rooms.
This commit is contained in:
2026-03-15 16:25:53 +11:00
parent a0c0068997
commit c7bd376292
21 changed files with 917 additions and 111 deletions

View File

@@ -18,6 +18,7 @@ import { RoomType, 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;
@@ -99,7 +100,36 @@ export const SpaceHierarchy = forwardRef<HTMLDivElement, SpaceHierarchyProps>(
onSpacesFound(Array.from(subspaces.values()));
}, [subspaces, onSpacesFound]);
let childItems = roomItems?.filter((i) => !subspaces.has(i.roomId));
// 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));
if (!spacePermissions?.stateEvent(StateEvent.SpaceChild, mx.getSafeUserId())) {
// hide unknown rooms for normal user
childItems = childItems?.filter((i) => {
@@ -166,40 +196,89 @@ export const SpaceHierarchy = forwardRef<HTMLDivElement, SpaceHierarchyProps>(
draggingItem?.roomId === roomItem.roomId &&
draggingItem.parentId === roomItem.parentId;
// Get sub-rooms for this room
const subRoomIds = subRoomsMap.get(roomItem.roomId) ?? [];
return (
<RoomItemCard
key={roomItem.roomId}
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}
/>
<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);
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>