import { useState, useRef, useEffect, useMemo } from 'react'; import ReactDOM from 'react-dom'; import type { MemberWithUser } from '@backspace/shared'; import { useSpaceStore, getApiForOrigin, type TaggedSpace } from '../../stores/spaceStore'; import { useAuthStore } from '../../stores/authStore'; import { useUIStore } from '../../stores/uiStore'; import { normalizeUserAssets } from '../../utils/assetUrls'; import { useCanonicalUserView } from '../../utils/userViewLookup'; import { Avatar } from '../ui/Avatar'; function TransferMemberRow({ member, onSelect, }: { member: MemberWithUser; onSelect: (userId: string) => void; }) { const canonical = useCanonicalUserView(member.user); const displayName = canonical.displayName || canonical.username; return ( ); } export function TransferOwnershipModal({ spaceId, onClose }: { spaceId: string; onClose: () => void }) { const modalRef = useRef(null); const space = useSpaceStore((s) => s.spaces.find(sp => sp.id === spaceId)); const currentUserId = useAuthStore((s) => s.user?.id); const transferOwnership = useSpaceStore((s) => s.transferOwnership); const addToast = useUIStore((s) => s.addToast); const [search, setSearch] = useState(''); const [selectedUserId, setSelectedUserId] = useState(null); const [transferring, setTransferring] = useState(false); const [members, setMembers] = useState([]); const [loading, setLoading] = useState(true); // Fetch members for the target space on mount (federation-aware) useEffect(() => { let cancelled = false; (async () => { try { const origin = (space as TaggedSpace)?._instanceOrigin ?? ''; const client = getApiForOrigin(origin); const fetched = await client.spaces.members(spaceId); if (cancelled) return; for (const m of fetched) { if (origin) normalizeUserAssets(m.user, origin); useSpaceStore.getState().upsertUserView(m.user, origin); } setMembers(fetched); } catch { // Space may have been deleted or we lost access } finally { if (!cancelled) setLoading(false); } })(); return () => { cancelled = true; }; }, [spaceId, space]); // Filter members: exclude self, filter by search const filteredMembers = useMemo(() => { const spaceMembers = members.filter(m => m.userId !== currentUserId); if (!search.trim()) return spaceMembers; const q = search.toLowerCase(); return spaceMembers.filter(m => m.user.displayName?.toLowerCase().includes(q) || m.user.username.toLowerCase().includes(q) ); }, [members, currentUserId, search]); const selectedMember = selectedUserId ? members.find(m => m.userId === selectedUserId) : null; // Close on click-outside and escape useEffect(() => { const handleClickOutside = (e: MouseEvent) => { if (modalRef.current && !modalRef.current.contains(e.target as Node)) { onClose(); } }; const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape') { if (selectedUserId) { setSelectedUserId(null); } else { onClose(); } } }; document.addEventListener('mousedown', handleClickOutside); document.addEventListener('keydown', handleKeyDown); return () => { document.removeEventListener('mousedown', handleClickOutside); document.removeEventListener('keydown', handleKeyDown); }; }, [onClose, selectedUserId]); if (!space) return null; const handleTransfer = async () => { if (!selectedUserId) return; setTransferring(true); try { await transferOwnership(spaceId, selectedUserId); addToast(`Ownership transferred to ${selectedMember?.user.displayName || selectedMember?.user.username}`, 'success', 3000); onClose(); } catch (err) { addToast(err instanceof Error ? err.message : 'Failed to transfer ownership', 'warning', 3000); } finally { setTransferring(false); } }; return ReactDOM.createPortal(
{/* Header */}

Transfer Ownership

Choose a member to become the new owner of {space.name}

{selectedUserId && selectedMember ? ( /* Confirm step */

Transfer ownership of {space.name} to{' '} {selectedMember.user.displayName || selectedMember.user.username}?

You will become a regular member.

) : ( /* Member list */ <>
setSearch(e.target.value)} placeholder="Search members..." className="input-search w-full" autoFocus />
{loading ? (

Loading members...

) : filteredMembers.length === 0 ? (

No members found

) : ( filteredMembers.map((member) => ( )) )}
)}
, document.body, ); }