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 { return new Promise((resolve) => { setTimeout(resolve, ms); }); } /** * Leaves multiple rooms sequentially with rate limiting */ async function leaveRoomsSequentially( mx: ReturnType, roomIds: string[], onProgress?: (current: number, total: number) => void ): Promise { 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, spaceRoom: Room, mDirects: Set ): 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( 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 ( }>
Leave Space
Are you sure you want to leave this space? You will also leave all rooms directly in this space (except DMs). {leaveState.status === AsyncStatus.Error && ( Failed to leave space! {leaveState.error.message} )}
); }