feat(call): add docked call panel and remote cursor overlay
- 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.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import FocusTrap from 'focus-trap-react';
|
||||
import {
|
||||
Dialog,
|
||||
@@ -16,10 +16,85 @@ import {
|
||||
Button,
|
||||
Spinner,
|
||||
} from 'folds';
|
||||
import { MatrixError } from 'matrix-js-sdk';
|
||||
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;
|
||||
@@ -28,11 +103,29 @@ type LeaveSpacePromptProps = {
|
||||
};
|
||||
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 () => {
|
||||
mx.leave(roomId);
|
||||
}, [mx, roomId])
|
||||
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 = () => {
|
||||
@@ -45,6 +138,13 @@ export function LeaveSpacePrompt({ roomId, onDone, onCancel }: LeaveSpacePromptP
|
||||
}
|
||||
}, [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>
|
||||
@@ -74,7 +174,10 @@ export function LeaveSpacePrompt({ roomId, onDone, onCancel }: LeaveSpacePromptP
|
||||
</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?</Text>
|
||||
<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}
|
||||
@@ -86,18 +189,11 @@ export function LeaveSpacePrompt({ roomId, onDone, onCancel }: LeaveSpacePromptP
|
||||
variant="Critical"
|
||||
onClick={handleLeave}
|
||||
before={
|
||||
leaveState.status === AsyncStatus.Loading ? (
|
||||
<Spinner fill="Solid" variant="Critical" size="200" />
|
||||
) : undefined
|
||||
}
|
||||
aria-disabled={
|
||||
leaveState.status === AsyncStatus.Loading ||
|
||||
leaveState.status === AsyncStatus.Success
|
||||
isLoading ? <Spinner fill="Solid" variant="Critical" size="200" /> : undefined
|
||||
}
|
||||
aria-disabled={isLoading || leaveState.status === AsyncStatus.Success}
|
||||
>
|
||||
<Text size="B400">
|
||||
{leaveState.status === AsyncStatus.Loading ? 'Leaving...' : 'Leave'}
|
||||
</Text>
|
||||
<Text size="B400">{getButtonText()}</Text>
|
||||
</Button>
|
||||
</Box>
|
||||
</Dialog>
|
||||
|
||||
Reference in New Issue
Block a user