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

@@ -0,0 +1,99 @@
import React, { useCallback } from 'react';
import {
Avatar,
Box,
Chip,
Icon,
Icons,
Spinner,
Text,
Tooltip,
TooltipProvider,
color,
config,
toRem,
} from 'folds';
import { MatrixError, Room } from 'matrix-js-sdk';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
/**
* Extract server name from a room ID (!abc:server.com => server.com)
*/
function getServerFromRoomId(roomId: string): string {
const parts = roomId.split(':');
return parts.length > 1 ? parts.slice(1).join(':') : '';
}
type UnjoinedSubRoomItemProps = {
roomId: string;
depth: number;
};
/**
* Displays an unjoined sub-room with a Join button.
* Used when a parent room has sub-rooms that the user hasn't joined yet.
*/
export function UnjoinedSubRoomItem({ roomId, depth }: UnjoinedSubRoomItemProps) {
const mx = useMatrixClient();
const viaServers = [getServerFromRoomId(roomId)].filter(Boolean);
const [joinState, join] = useAsyncCallback<Room, MatrixError, []>(
useCallback(() => mx.joinRoom(roomId, { viaServers }), [mx, roomId, viaServers])
);
const canJoin = joinState.status === AsyncStatus.Idle || joinState.status === AsyncStatus.Error;
const paddingLeft = depth > 0 ? `${depth * 24}px` : undefined;
return (
<Box
style={{ paddingLeft, padding: `${config.space.S100} ${config.space.S200}` }}
alignItems="Center"
gap="200"
>
<Avatar size="200" radii="400">
<Icon src={Icons.Hash} size="100" />
</Avatar>
<Box grow="Yes" alignItems="Center" gap="100">
<Text size="T300" truncate style={{ opacity: 0.7 }}>
{roomId}
</Text>
</Box>
<Box shrink="No" gap="100" alignItems="Center">
{joinState.status === AsyncStatus.Error && (
<TooltipProvider
tooltip={
<Tooltip variant="Critical" style={{ maxWidth: toRem(200) }}>
<Text style={{ wordBreak: 'break-word' }} size="T300">
{joinState.error.data?.error || joinState.error.message}
</Text>
</Tooltip>
}
>
{(triggerRef) => (
<Icon
ref={triggerRef}
style={{ color: color.Critical.Main, cursor: 'pointer' }}
src={Icons.Warning}
size="200"
/>
)}
</TooltipProvider>
)}
<Chip
variant="Secondary"
fill="Soft"
size="400"
radii="Pill"
before={
canJoin ? <Icon src={Icons.Plus} size="50" /> : <Spinner variant="Secondary" size="50" />
}
onClick={join}
disabled={!canJoin}
>
<Text size="B300">Join</Text>
</Chip>
</Box>
</Box>
);
}