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

@@ -1,4 +1,4 @@
import React from 'react';
import React, { useCallback } from 'react';
import {
Box,
config,
@@ -14,6 +14,7 @@ import {
Text,
} from 'folds';
import FocusTrap from 'focus-trap-react';
import { useAtomValue } from 'jotai';
import { useAllJoinedRoomsSet, useGetRoom } from '../../hooks/useGetRoom';
import { SpaceProvider } from '../../hooks/useSpace';
import { CreateRoomForm } from './CreateRoom';
@@ -23,17 +24,96 @@ import {
} from '../../state/hooks/createRoomModal';
import { CreateRoomModalState } from '../../state/createRoomModal';
import { stopPropagation } from '../../utils/keyboard';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { StateEvent } from '../../../types/matrix/room';
import { PaarrotSubRoomsContent } from '../../hooks/useRoomSubRooms';
import { roomToParentsAtom } from '../../state/room/roomToParents';
import { isSpace } from '../../utils/room';
type CreateRoomModalProps = {
state: CreateRoomModalState;
};
function CreateRoomModal({ state }: CreateRoomModalProps) {
const { spaceId } = state;
const { spaceId, parentSubRoomId } = state;
const mx = useMatrixClient();
const closeDialog = useCloseCreateRoomModal();
const roomToParents = useAtomValue(roomToParentsAtom);
const allJoinedRooms = useAllJoinedRoomsSet();
const getRoom = useGetRoom(allJoinedRooms);
const space = spaceId ? getRoom(spaceId) : undefined;
const parentRoom = parentSubRoomId ? getRoom(parentSubRoomId) : undefined;
const handleCreate = useCallback(
async (newRoomId: string) => {
// If creating a sub-room, add it to the parent's sub-rooms and set parent relationship
if (parentSubRoomId && parentRoom) {
try {
// Extract server from parent room ID for via servers
const serverName = parentSubRoomId.split(':')[1] || '';
const viaServers = serverName ? [serverName] : [];
// Set m.space.parent on the new room pointing to parent
await mx.sendStateEvent(
newRoomId,
StateEvent.SpaceParent,
{
canonical: true,
via: viaServers,
},
parentSubRoomId
);
// Add new room to parent's sub-rooms list
const stateEvent = parentRoom.currentState.getStateEvents(StateEvent.PaarrotSubRooms, '');
const currentContent = stateEvent?.getContent() as PaarrotSubRoomsContent | undefined;
const currentChildren = currentContent?.children ?? [];
const newContent: PaarrotSubRoomsContent = {
children: [...currentChildren, newRoomId],
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
await mx.sendStateEvent(parentSubRoomId, StateEvent.PaarrotSubRooms as any, newContent, '');
// Also add sub-room to parent's space(s) for joining/permissions
// The UI will hide it from the top-level display and only show it nested
const parentSpaces = roomToParents.get(parentSubRoomId);
if (parentSpaces) {
const spaceIds = Array.from(parentSpaces);
await Promise.all(
spaceIds.map(async (parentSpaceId) => {
const parentSpaceRoom = mx.getRoom(parentSpaceId);
// Only add to actual spaces (not other sub-room parents)
if (parentSpaceRoom && isSpace(parentSpaceRoom)) {
try {
const spaceServerName = parentSpaceId.split(':')[1] || '';
const spaceViaServers = spaceServerName ? [spaceServerName] : [];
// Add as m.space.child to the space
await mx.sendStateEvent(
parentSpaceId,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
StateEvent.SpaceChild as any,
{ via: spaceViaServers },
newRoomId
);
} catch (spaceError) {
// eslint-disable-next-line no-console
console.warn(`Failed to add sub-room to space ${parentSpaceId}:`, spaceError);
}
}
})
);
}
} catch (error) {
// eslint-disable-next-line no-console
console.error('Failed to set up sub-room relationship:', error);
}
}
closeDialog();
},
[mx, parentSubRoomId, parentRoom, roomToParents, closeDialog]
);
const modalTitle = parentSubRoomId ? 'New Sub-Room' : 'New Room';
return (
<SpaceProvider value={space ?? null}>
@@ -57,7 +137,7 @@ function CreateRoomModal({ state }: CreateRoomModalProps) {
}}
>
<Box grow="Yes">
<Text size="H4">New Room</Text>
<Text size="H4">{modalTitle}</Text>
</Box>
<Box shrink="No">
<IconButton size="300" radii="300" onClick={closeDialog}>
@@ -74,7 +154,7 @@ function CreateRoomModal({ state }: CreateRoomModalProps) {
direction="Column"
gap="500"
>
<CreateRoomForm space={space} onCreate={closeDialog} />
<CreateRoomForm space={space} onCreate={handleCreate} />
</Box>
</Scroll>
</Box>