- Implemented DockedCallPanel component to render the docked call interface when an active call is present. - Created RemoteCursorOverlay component to display remote cursor positions during screen sharing. - Added hooks for managing docked call state and remote cursor functionality. - Introduced pending drag state management for seamless transitions between docked and floating states. - Developed embed filter management components for personal and room-wide settings, allowing users to customize URL embed visibility. - Implemented auto-joining functionality for rooms within spaces, enhancing user experience during space navigation. - Added state management for tracking joining progress of rooms in spaces.
205 lines
5.7 KiB
TypeScript
205 lines
5.7 KiB
TypeScript
import React, { useCallback, useEffect, useState } from 'react';
|
|
import FocusTrap from 'focus-trap-react';
|
|
import {
|
|
Dialog,
|
|
Overlay,
|
|
OverlayCenter,
|
|
OverlayBackdrop,
|
|
Header,
|
|
config,
|
|
Box,
|
|
Text,
|
|
IconButton,
|
|
Icon,
|
|
Icons,
|
|
color,
|
|
Button,
|
|
Spinner,
|
|
} from 'folds';
|
|
import { MatrixError, Room } from 'matrix-js-sdk';
|
|
import { useAtomValue } from 'jotai';
|
|
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
|
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
|
|
import { stopPropagation } from '../../utils/keyboard';
|
|
import { getSpaceChildren, isSpace } from '../../utils/room';
|
|
import { mDirectAtom } from '../../state/mDirectList';
|
|
import { Membership } from '../../../types/matrix/room';
|
|
|
|
const LEAVE_DELAY_MS = 300;
|
|
|
|
/**
|
|
* Delays execution for a specified time
|
|
*/
|
|
function delay(ms: number): Promise<void> {
|
|
return new Promise<void>((resolve) => {
|
|
setTimeout(resolve, ms);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Leaves multiple rooms sequentially with rate limiting
|
|
*/
|
|
async function leaveRoomsSequentially(
|
|
mx: ReturnType<typeof useMatrixClient>,
|
|
roomIds: string[],
|
|
onProgress?: (current: number, total: number) => void
|
|
): Promise<void> {
|
|
const total = roomIds.length;
|
|
|
|
for (let i = 0; i < roomIds.length; i++) {
|
|
const roomId = roomIds[i];
|
|
onProgress?.(i, total);
|
|
|
|
try {
|
|
await mx.leave(roomId);
|
|
await delay(LEAVE_DELAY_MS);
|
|
} catch {
|
|
// Continue leaving other rooms even if one fails
|
|
}
|
|
}
|
|
|
|
onProgress?.(total, total);
|
|
}
|
|
|
|
/**
|
|
* Gets direct child room IDs from a space that can be left (excluding DMs and subspaces)
|
|
*/
|
|
function getDirectChildRoomsToLeave(
|
|
mx: ReturnType<typeof useMatrixClient>,
|
|
spaceRoom: Room,
|
|
mDirects: Set<string>
|
|
): string[] {
|
|
const childIds = getSpaceChildren(spaceRoom);
|
|
|
|
return childIds.filter((childId) => {
|
|
// Skip DMs
|
|
if (mDirects.has(childId)) {
|
|
return false;
|
|
}
|
|
|
|
const childRoom = mx.getRoom(childId);
|
|
if (!childRoom) {
|
|
return false;
|
|
}
|
|
|
|
// Skip subspaces (only leave rooms, not nested spaces)
|
|
if (isSpace(childRoom)) {
|
|
return false;
|
|
}
|
|
|
|
// Only include rooms we're actually joined to
|
|
if (childRoom.getMyMembership() !== Membership.Join) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
});
|
|
}
|
|
|
|
type LeaveSpacePromptProps = {
|
|
roomId: string;
|
|
onDone: () => void;
|
|
onCancel: () => void;
|
|
};
|
|
export function LeaveSpacePrompt({ roomId, onDone, onCancel }: LeaveSpacePromptProps) {
|
|
const mx = useMatrixClient();
|
|
const mDirects = useAtomValue(mDirectAtom);
|
|
const [progress, setProgress] = useState<{ current: number; total: number } | null>(null);
|
|
|
|
const [leaveState, leaveRoom] = useAsyncCallback<undefined, MatrixError, []>(
|
|
useCallback(async () => {
|
|
const spaceRoom = mx.getRoom(roomId);
|
|
|
|
if (spaceRoom) {
|
|
// Get direct child rooms to leave (excluding DMs and subspaces)
|
|
const childRoomsToLeave = getDirectChildRoomsToLeave(mx, spaceRoom, mDirects);
|
|
|
|
if (childRoomsToLeave.length > 0) {
|
|
// Leave child rooms first
|
|
await leaveRoomsSequentially(mx, childRoomsToLeave, (current, total) => {
|
|
setProgress({ current, total });
|
|
});
|
|
}
|
|
}
|
|
|
|
// Clear progress and leave the space itself
|
|
setProgress(null);
|
|
await mx.leave(roomId);
|
|
}, [mx, roomId, mDirects])
|
|
);
|
|
|
|
const handleLeave = () => {
|
|
leaveRoom();
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (leaveState.status === AsyncStatus.Success) {
|
|
onDone();
|
|
}
|
|
}, [leaveState, onDone]);
|
|
|
|
const isLoading = leaveState.status === AsyncStatus.Loading;
|
|
const getButtonText = () => {
|
|
if (!isLoading) return 'Leave';
|
|
if (progress) return `Leaving Rooms: ${progress.current}/${progress.total}`;
|
|
return 'Leaving Space...';
|
|
};
|
|
|
|
return (
|
|
<Overlay open backdrop={<OverlayBackdrop />}>
|
|
<OverlayCenter>
|
|
<FocusTrap
|
|
focusTrapOptions={{
|
|
initialFocus: false,
|
|
onDeactivate: onCancel,
|
|
clickOutsideDeactivates: true,
|
|
escapeDeactivates: stopPropagation,
|
|
}}
|
|
>
|
|
<Dialog variant="Surface">
|
|
<Header
|
|
style={{
|
|
padding: `0 ${config.space.S200} 0 ${config.space.S400}`,
|
|
borderBottomWidth: config.borderWidth.B300,
|
|
}}
|
|
variant="Surface"
|
|
size="500"
|
|
>
|
|
<Box grow="Yes">
|
|
<Text size="H4">Leave Space</Text>
|
|
</Box>
|
|
<IconButton size="300" onClick={onCancel} radii="300">
|
|
<Icon src={Icons.Cross} />
|
|
</IconButton>
|
|
</Header>
|
|
<Box style={{ padding: config.space.S400 }} direction="Column" gap="400">
|
|
<Box direction="Column" gap="200">
|
|
<Text priority="400">
|
|
Are you sure you want to leave this space? You will also leave all rooms directly
|
|
in this space (except DMs).
|
|
</Text>
|
|
{leaveState.status === AsyncStatus.Error && (
|
|
<Text style={{ color: color.Critical.Main }} size="T300">
|
|
Failed to leave space! {leaveState.error.message}
|
|
</Text>
|
|
)}
|
|
</Box>
|
|
<Button
|
|
type="submit"
|
|
variant="Critical"
|
|
onClick={handleLeave}
|
|
before={
|
|
isLoading ? <Spinner fill="Solid" variant="Critical" size="200" /> : undefined
|
|
}
|
|
aria-disabled={isLoading || leaveState.status === AsyncStatus.Success}
|
|
>
|
|
<Text size="B400">{getButtonText()}</Text>
|
|
</Button>
|
|
</Box>
|
|
</Dialog>
|
|
</FocusTrap>
|
|
</OverlayCenter>
|
|
</Overlay>
|
|
);
|
|
}
|