import React, { useEffect, useState } from 'react'; import ReactDOM from 'react-dom'; import { useNavigate } from 'react-router-dom'; import type { InstanceInfoResponse, FederationRegistryEntry, PeeringSubscription, PeeringNotification, PeeringTriggerReason, } from '@backspace/shared'; import { useInstanceStore, DifferentPasswordError, isSelfOrigin } from '../../stores/instanceStore'; import { useAuthStore } from '../../stores/authStore'; import { useUIStore } from '../../stores/uiStore'; import { useFederationStore } from '../../stores/federationStore'; import { isElectron } from '../../platform/platform'; import { ConfirmDialog } from '../ui/ConfirmDialog'; // ─── URL helpers ───────────────────────────────────────────────────────────── function safeHost(origin: string): string { try { return new URL(origin).host; } catch { return origin; } } // ─── Status indicator ──────────────────────────────────────────────────────── function StatusDot({ status }: { status: string }) { const colorClass = status === 'connected' ? 'bg-status-online' : status === 'connecting' ? 'bg-accent-amber' : 'bg-txt-tertiary'; return
; } // ─── Registry status helpers ──────────────────────────────────────────────── function registryStatusColor(status: string): string { switch (status) { case 'connected': return 'bg-status-online/15 text-status-online'; case 'disconnected': return 'bg-white/5 text-txt-tertiary'; case 'unreachable': return 'bg-accent-amber/15 text-accent-amber'; case 'auth_expired': return 'bg-accent-rose/15 text-accent-rose'; default: return 'bg-white/5 text-txt-tertiary'; } } function registryStatusDotColor(status: string): string { switch (status) { case 'connected': return 'bg-status-online'; case 'unreachable': return 'bg-accent-amber'; case 'auth_expired': return 'bg-accent-rose'; default: return 'bg-txt-tertiary'; } } function registryStatusLabel(status: string): string { switch (status) { case 'connected': return 'Connected'; case 'disconnected': return 'Disconnected'; case 'unreachable': return 'Unreachable'; case 'auth_expired': return 'Auth Expired'; default: return status; } } function formatRelativeTime(timestamp: number | null): string { if (!timestamp) return 'Never'; const seconds = Math.floor((Date.now() - timestamp) / 1000); if (seconds < 60) return 'Just now'; const minutes = Math.floor(seconds / 60); if (minutes < 60) return `${minutes}m ago`; const hours = Math.floor(minutes / 60); if (hours < 24) return `${hours}h ago`; const days = Math.floor(hours / 24); return `${days}d ago`; } function formatAbsoluteDate(timestamp: number): string { return new Date(timestamp).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); } // ─── Add Instance flow ─────────────────────────────────────────────────────── type AddStep = 'url' | 'auth' | 'done'; type AuthPhase = 'password' | 'fallback-login'; function AddInstanceFlow({ onDone }: { onDone: () => void }) { const user = useAuthStore((s) => s.user); const connectToRemote = useInstanceStore((s) => s.connectToRemote); const loginToRemote = useInstanceStore((s) => s.loginToRemote); const probeInstance = useInstanceStore((s) => s.probeInstance); const [step, setStep] = useState('url'); const [url, setUrl] = useState(''); const [probeResult, setProbeResult] = useState<(InstanceInfoResponse & { origin: string }) | null>(null); const [authPhase, setAuthPhase] = useState('password'); const [password, setPassword] = useState(''); const [fallbackUsername, setFallbackUsername] = useState(''); const [fallbackPassword, setFallbackPassword] = useState(''); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(''); const handleProbe = async () => { setError(''); setIsLoading(true); try { const result = await probeInstance(url); setProbeResult(result); setAuthPhase('password'); setStep('auth'); } catch (err) { setError((err as Error).message); } finally { setIsLoading(false); } }; const handleConnect = async () => { if (!probeResult) return; setError(''); setIsLoading(true); try { await connectToRemote( probeResult.origin, password, user?.displayName || undefined, ); setStep('done'); onDone(); } catch (err) { if (err instanceof DifferentPasswordError) { setAuthPhase('fallback-login'); setFallbackUsername(err.remoteUsername); setFallbackPassword(''); setError(''); } else { setError((err as Error).message); } } finally { setIsLoading(false); } }; const handleFallbackLogin = async () => { if (!probeResult) return; setError(''); setIsLoading(true); try { await loginToRemote(probeResult.origin, fallbackUsername, fallbackPassword); setStep('done'); onDone(); } catch (err) { setError((err as Error).message); } finally { setIsLoading(false); } }; if (step === 'done') return null; return (
{/* Step 1: Enter URL */} {step === 'url' && ( <>
Add Remote Instance
setUrl(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && !isLoading && url.trim() && handleProbe()} placeholder="https://instance.example.com" className="input-standard flex-1" disabled={isLoading} />
)} {/* Step 2: Auth — single password */} {step === 'auth' && probeResult && authPhase === 'password' && ( <> {/* Instance info card */}
{probeResult.name}
{probeResult.origin}
{!probeResult.federatedRegistrationOpen && (
This instance has disabled new federated registrations. Existing accounts can still sign in.
)}
{ e.preventDefault(); handleConnect(); }} className="space-y-2">
setPassword(e.target.value)} placeholder="Your account password" className="input-standard w-full" disabled={isLoading} autoFocus autoComplete="current-password" />
Your password is verified locally, then used to create or access your account on the remote instance.
)} {/* Step 2b: Fallback login — different password on remote */} {step === 'auth' && probeResult && authPhase === 'fallback-login' && ( <> {/* Instance info card */}
{probeResult.name}
{probeResult.origin}
An account already exists on this instance with a different password. Enter the credentials you used on that instance.
{ e.preventDefault(); handleFallbackLogin(); }} className="space-y-2">
setFallbackUsername(e.target.value)} placeholder="Your username on this instance" className="input-standard w-full" disabled={isLoading} autoComplete="username" />
setFallbackPassword(e.target.value)} placeholder="Password on the remote instance" className="input-standard w-full" disabled={isLoading} autoFocus autoComplete="current-password" />
)} {/* Error display */} {error && (
{error}
)}
); } // ─── Filter / Sort types ──────────────────────────────────────────────────── type StatusFilter = 'all' | 'connected' | 'disconnected' | 'issues'; type SortBy = 'name' | 'dateAdded' | 'lastConnected'; // ─── RegistryFilterBar ────────────────────────────────────────────────────── function RegistryFilterBar({ filter, setFilter, sortBy, setSortBy, counts, }: { filter: StatusFilter; setFilter: (f: StatusFilter) => void; sortBy: SortBy; setSortBy: (s: SortBy) => void; counts: { all: number; connected: number; disconnected: number; issues: number }; }) { const [sortOpen, setSortOpen] = useState(false); const tabs: Array<{ key: StatusFilter; label: string; count: number }> = [ { key: 'all', label: 'All', count: counts.all }, { key: 'connected', label: 'Connected', count: counts.connected }, { key: 'disconnected', label: 'Disconnected', count: counts.disconnected }, { key: 'issues', label: 'Issues', count: counts.issues }, ]; const sortOptions: Array<{ key: SortBy; label: string }> = [ { key: 'name', label: 'Name (A-Z)' }, { key: 'dateAdded', label: 'Date Added' }, { key: 'lastConnected', label: 'Last Connected' }, ]; return (
{tabs.map((tab) => ( ))}
{sortOpen && ( <>
setSortOpen(false)} />
Sort by
{sortOptions.map((opt) => ( ))}
)}
); } // ─── DeleteIdentityDialog ─────────────────────────────────────────────────── type DeletionMode = 'leave' | 'soft' | 'full'; type DeletionScope = 'this' | 'select' | 'all'; function DeleteIdentityDialog({ origin, label, onClose, }: { origin: string; label: string; onClose: () => void; }) { const deleteIdentity = useInstanceStore((s) => s.deleteIdentity); const registry = useInstanceStore((s) => s.registry); const [mode, setMode] = useState('leave'); const [scope, setScope] = useState('this'); const [selectedOrigins, setSelectedOrigins] = useState>(new Set()); const [loading, setLoading] = useState(false); const handleConfirm = async () => { // Resolve target origins based on scope let targetOrigins: string[]; if (scope === 'all') { targetOrigins = Array.from(registry.keys()); } else if (scope === 'select') { targetOrigins = Array.from(selectedOrigins); } else { targetOrigins = [origin]; } if (targetOrigins.length === 0) { onClose(); return; } if (mode !== 'leave') { setLoading(true); } const results = await deleteIdentity(targetOrigins, mode); // Check results const failed = Object.entries(results).filter(([, r]) => !r.success); if (failed.length === 0) { useUIStore.getState().addToast( mode === 'leave' ? 'Disconnected successfully' : targetOrigins.length === 1 ? 'Identity deleted successfully' : `Identity deleted on ${targetOrigins.length} instances`, 'success', 3000, ); onClose(); } else { for (const [failOrigin, result] of failed) { let host: string; try { host = new URL(failOrigin).hostname; } catch { host = failOrigin; } if (result.error === 'owns_spaces') { useUIStore.getState().addToast( `${host}: Transfer space ownership first`, 'warning', 5000, ); } else { useUIStore.getState().addToast( `${host}: ${result.error || 'Failed'}`, 'warning', 5000, ); } } // Close if some succeeded, keep open if all failed const succeeded = Object.values(results).filter(r => r.success).length; if (succeeded > 0) { onClose(); } else { setLoading(false); } } }; return ReactDOM.createPortal(

Delete Identity

Remove your federated identity on {label}. Choose how your data should be handled.

{/* Deletion mode selection */}
{/* Leave quietly */} {/* Delete User (soft) */} {/* Nuke everything (full) */}
{/* Scope selector */}
Scope
{([ { key: 'this' as DeletionScope, label: 'This instance only', disabled: false }, { key: 'select' as DeletionScope, label: 'Select instances...', disabled: false }, { key: 'all' as DeletionScope, label: 'All remote instances', disabled: false }, ]).map((opt) => ( ))}
{/* Instance picker for 'select' scope */} {scope === 'select' && (
{Array.from(registry.values()).map((entry) => { const checked = selectedOrigins.has(entry.origin); return ( ); })}
)} {/* Actions */}
, document.body, ); } // ─── RegistryRow ──────────────────────────────────────────────────────────── function RegistryRow({ entry, expanded, onToggleExpand, }: { entry: FederationRegistryEntry; expanded: boolean; onToggleExpand: () => void; }) { const instances = useInstanceStore((s) => s.instances); const disconnectInstance = useInstanceStore((s) => s.disconnectInstance); const reconnectInstance = useInstanceStore((s) => s.reconnectInstance); const forceRemoveEntry = useInstanceStore((s) => s.forceRemoveEntry); const reauthenticateInstance = useInstanceStore((s) => s.reauthenticateInstance); const [showForceRemoveConfirm, setShowForceRemoveConfirm] = useState(false); const [showDeleteIdentity, setShowDeleteIdentity] = useState(false); const [showReauth, setShowReauth] = useState(false); const [reauthPassword, setReauthPassword] = useState(''); const [reauthLoading, setReauthLoading] = useState(false); const [reauthError, setReauthError] = useState(''); const name = entry.label || safeHost(entry.origin); const isDisconnected = entry.status === 'disconnected'; const isConnected = entry.status === 'connected'; const hasIssue = entry.status === 'unreachable' || entry.status === 'auth_expired'; // Check if there's a live ConnectedInstance for reconnect actions const liveInstance = instances.find((i) => i.origin === entry.origin); // Build context-dependent metadata line let metadataText = ''; if (isConnected) { metadataText = `Added ${formatAbsoluteDate(entry.addedAt)}`; if (entry.lastConnectedAt) { metadataText += ` · Connected ${formatRelativeTime(entry.lastConnectedAt)}`; } } else if (isDisconnected) { metadataText = `Added ${formatAbsoluteDate(entry.addedAt)}`; if (entry.disconnectedAt) { metadataText += ` · Disconnected ${formatRelativeTime(entry.disconnectedAt)}`; } } else { metadataText = `Added ${formatAbsoluteDate(entry.addedAt)}`; if (entry.lastConnectedAt) { metadataText += ` · Last connected ${formatRelativeTime(entry.lastConnectedAt)}`; } } const handleDisconnect = () => { disconnectInstance(entry.origin); }; const handleReconnect = () => { if (liveInstance) { reconnectInstance(entry.origin); } }; const handleForceRemove = () => { forceRemoveEntry(entry.origin); setShowForceRemoveConfirm(false); }; const handleReauth = async () => { if (!reauthPassword) return; setReauthError(''); setReauthLoading(true); try { await reauthenticateInstance(entry.origin, reauthPassword); setShowReauth(false); setReauthPassword(''); } catch (err) { setReauthError((err as Error).message); } finally { setReauthLoading(false); } }; return ( <>
{/* Compact row */}
{name} {registryStatusLabel(entry.status)}
{safeHost(entry.origin)} {entry.username && ( as {entry.username} )}
{metadataText}
{expanded ? '\u25BE' : '\u25B8'}
{/* Expanded details */} {expanded && (
{/* Stats grid */}
Remote User ID
{entry.remoteUserId ? entry.remoteUserId.slice(0, 12) + '...' : 'Unknown'}
Added
{formatAbsoluteDate(entry.addedAt)}
{isDisconnected && entry.disconnectedAt ? ( <>
Disconnected
{formatAbsoluteDate(entry.disconnectedAt)}
) : ( <>
Last Connected
{entry.lastConnectedAt ? formatAbsoluteDate(entry.lastConnectedAt) : 'Never'}
)}
{/* Error message */} {entry.errorMessage && (
{entry.errorMessage}
)} {/* Re-auth inline form */} {showReauth && (
{ e.preventDefault(); handleReauth(); }} className="mt-3 space-y-2">
setReauthPassword(e.target.value)} placeholder="Your account password" className="input-standard flex-1 py-1.5" disabled={reauthLoading} autoFocus autoComplete="current-password" />
{reauthError && (
{reauthError}
)}
)} {/* Actions */}
{isConnected && ( <> )} {isDisconnected && ( <> {liveInstance && ( )} )} {hasIssue && ( <> {entry.status === 'unreachable' && liveInstance && ( )} {entry.status === 'auth_expired' && ( )} )}
)}
{/* Force Remove confirmation dialog */} setShowForceRemoveConfirm(false)} onConfirm={handleForceRemove} title="Force Remove Entry" description={`This will remove the registry entry for ${name}. The remote instance will not be notified. Use this only if the instance is permanently unreachable.`} confirmLabel="Force Remove" variant="warning" /> {/* Delete Identity dialog */} {showDeleteIdentity && ( setShowDeleteIdentity(false)} /> )} ); } // ─── Sorting ──────────────────────────────────────────────────────────────── function sortEntries(entries: FederationRegistryEntry[], sortBy: SortBy): FederationRegistryEntry[] { return [...entries].sort((a, b) => { switch (sortBy) { case 'name': { const nameA = (a.label || safeHost(a.origin)).toLowerCase(); const nameB = (b.label || safeHost(b.origin)).toLowerCase(); return nameA.localeCompare(nameB); } case 'dateAdded': return b.addedAt - a.addedAt; case 'lastConnected': return (b.lastConnectedAt ?? 0) - (a.lastConnectedAt ?? 0); default: return 0; } }); } // ─── Outbound peering gate helpers ────────────────────────────────────────── function actionLabel(reason: PeeringTriggerReason): string { switch (reason) { case 'friend_add': return 'friend request'; case 'space_join': return 'space join'; case 'direct_message': return 'direct message'; } } function actionVerbPhrase(reason: PeeringTriggerReason, target: string): string { switch (reason) { case 'friend_add': return `Friend request to ${target}`; case 'space_join': return `Join ${target}`; case 'direct_message': return `Direct message to ${target}`; } } // ─── Pending peering subscriptions section ────────────────────────────────── function PendingSubscriptionRow({ subscription }: { subscription: PeeringSubscription }) { const cancelPeeringSubscription = useFederationStore((s) => s.cancelPeeringSubscription); const addToast = useUIStore((s) => s.addToast); const [busy, setBusy] = useState(false); const host = safeHost(subscription.peerOrigin); const peerLabel = subscription.peerInstanceName || host; const handleCancel = async () => { setBusy(true); try { await cancelPeeringSubscription(subscription.id); addToast('Peering request cancelled', 'success', 3000); } catch (err) { addToast( `Failed to cancel: ${err instanceof Error ? err.message : 'Unknown error'}`, 'warning', 5000, ); setBusy(false); } }; return (
{actionVerbPhrase(subscription.triggerReason, subscription.triggerTarget)}
on {peerLabel} {subscription.peerInstanceName && ( ({host}) )}
); } function PendingPeeringSubscriptionsSection() { const subscriptions = useFederationStore((s) => s.peeringSubscriptions); if (subscriptions.length === 0) return null; return (
Pending Peering Approvals

Your admin must approve before these requests can proceed.

{subscriptions.map((s) => ( ))}
); } // ─── Recent peering outcomes section ──────────────────────────────────────── function notificationAccentClasses(kind: PeeringNotification['kind']): { surface: string; iconBg: string; iconColor: string; } { switch (kind) { case 'approved': return { surface: 'bg-status-online/[0.06] border border-status-online/15', iconBg: 'bg-status-online/15', iconColor: 'text-status-online', }; case 'denied': return { surface: 'bg-accent-rose/[0.06] border border-accent-rose/15', iconBg: 'bg-accent-rose/15', iconColor: 'text-txt-danger', }; case 'expired': return { surface: 'bg-accent-amber/[0.06] border border-accent-amber/15', iconBg: 'bg-accent-amber/15', iconColor: 'text-accent-amber', }; } } function NotificationIcon({ kind, className }: { kind: PeeringNotification['kind']; className: string }) { // approved: check, denied: cross, expired: clock if (kind === 'approved') { return ( ); } if (kind === 'denied') { return ( ); } return ( ); } function PeeringNotificationCard({ notification, onRetry, }: { notification: PeeringNotification; onRetry: (notification: PeeringNotification) => void; }) { const markPeeringNotificationRead = useFederationStore((s) => s.markPeeringNotificationRead); const addToast = useUIStore((s) => s.addToast); const [busy, setBusy] = useState(false); const host = safeHost(notification.peerOrigin); const accent = notificationAccentClasses(notification.kind); const handleDismiss = async () => { setBusy(true); try { await markPeeringNotificationRead(notification.id); } catch (err) { addToast( `Failed to dismiss: ${err instanceof Error ? err.message : 'Unknown error'}`, 'warning', 5000, ); setBusy(false); } }; // Retry is only meaningful on approved notifications, and the gate currently // only wires friend_add. space_join and direct_message reach the gate via // backend paths that aren't user-initiated end-to-end yet, so we hide Retry // for those rather than promise an action we cannot deliver. const showRetry = notification.kind === 'approved' && notification.triggerReason === 'friend_add'; let primaryText: string; if (notification.kind === 'approved') { primaryText = `Your peering request to ${host} was approved by your admin.`; } else if (notification.kind === 'denied') { primaryText = `Your peering request to ${host} was denied by your admin.`; } else { primaryText = `Your peering request to ${host} expired without admin action.`; } const contextText = notification.kind === 'approved' && notification.triggerReason !== 'friend_add' ? `Original action: ${actionVerbPhrase(notification.triggerReason, notification.triggerTarget)}.` : `Original action: ${actionVerbPhrase(notification.triggerReason, notification.triggerTarget)}`; return (
{primaryText}
{contextText}
{showRetry && ( )}
); } function RecentPeeringOutcomesSection() { const notifications = useFederationStore((s) => s.peeringNotifications); const markAllPeeringNotificationsRead = useFederationStore((s) => s.markAllPeeringNotificationsRead); const setPendingFriendAddPrefill = useFederationStore((s) => s.setPendingFriendAddPrefill); const closeModal = useUIStore((s) => s.closeModal); const setShowDms = useUIStore((s) => s.setShowDms); const setMobileTab = useUIStore((s) => s.setMobileTab); const isMobile = useUIStore((s) => s.isMobile); const addToast = useUIStore((s) => s.addToast); const navigate = useNavigate(); const [bulkBusy, setBulkBusy] = useState(false); if (notifications.length === 0) return null; const handleRetry = (notification: PeeringNotification) => { if (notification.triggerReason !== 'friend_add') { // Defensive — Retry button is only rendered for friend_add. Bail // silently if a future change widens this without updating the handler. return; } // Set the prefill side-channel before navigating so AddFriendTab finds // it on its initial render. setPendingFriendAddPrefill(notification.triggerTarget); // Mark this notification read in the background — the user has acted on // it. Use the per-id endpoint so other unread notifications stay visible. void useFederationStore.getState().markPeeringNotificationRead(notification.id); // Close the settings modal that hosts this panel. closeModal(); if (isMobile) { // Mobile: jump to the DMs/Friends tab so MobileShell renders FriendsPage. setMobileTab('dms'); } else { // Desktop: route to /channels/@me — AppLayout's effect calls // setShowDms(true) for the @me path, and MainContent renders FriendsPage // when no DM channel is selected. setShowDms(true); navigate('/channels/@me'); } }; const handleDismissAll = async () => { setBulkBusy(true); try { await markAllPeeringNotificationsRead(); } catch (err) { addToast( `Failed to dismiss all: ${err instanceof Error ? err.message : 'Unknown error'}`, 'warning', 5000, ); setBulkBusy(false); } }; return (
Recent Peering Outcomes
{notifications.length > 1 && ( )}
{notifications.map((n) => ( ))}
); } // ─── Main component ────────────────────────────────────────────────────────── export function ConnectedInstances() { const instances = useInstanceStore((s) => s.instances); const registry = useInstanceStore((s) => s.registry); const user = useAuthStore((s) => s.user); const refetchPeeringSubscriptions = useFederationStore((s) => s.refetchPeeringSubscriptions); const refetchPeeringNotifications = useFederationStore((s) => s.refetchPeeringNotifications); // Hydrate the outbound-peering-gate user surfaces on mount. WS events // (peering_subscription_changed / peering_notification_received) will keep // them fresh while the panel stays mounted. useEffect(() => { void refetchPeeringSubscriptions(); void refetchPeeringNotifications(); }, [refetchPeeringSubscriptions, refetchPeeringNotifications]); const [showAddForm, setShowAddForm] = useState(false); const [filter, setFilter] = useState('all'); const [sortBy, setSortBy] = useState('dateAdded'); const [expandedOrigins, setExpandedOrigins] = useState>(new Set()); const toggleExpand = (origin: string) => { setExpandedOrigins((prev) => { const next = new Set(prev); if (next.has(origin)) { next.delete(origin); } else { next.add(origin); } return next; }); }; // Convert registry Map to array (hide self-referencing entry — shown as Home Instance above) const registryEntries = Array.from(registry.values()) .filter(entry => !isSelfOrigin(entry.origin)); // Compute filter counts const counts = { all: registryEntries.length, connected: registryEntries.filter((e) => e.status === 'connected').length, disconnected: registryEntries.filter((e) => e.status === 'disconnected').length, issues: registryEntries.filter((e) => e.status === 'unreachable' || e.status === 'auth_expired').length, }; // Filter entries const filteredEntries = registryEntries.filter((entry) => { switch (filter) { case 'all': return true; case 'connected': return entry.status === 'connected'; case 'disconnected': return entry.status === 'disconnected'; case 'issues': return entry.status === 'unreachable' || entry.status === 'auth_expired'; default: return true; } }); // Sort entries const sortedEntries = sortEntries(filteredEntries, sortBy); // Empty state message const emptyMessage = registryEntries.length === 0 ? null : filteredEntries.length === 0 ? 'No instances match the current filter.' : null; return (
{/* Terminal-state outcomes first — newly resolved requests warrant the user's attention (especially approvals they can now retry). */} {/* Active waiting state. */}
Connected Instances

Link accounts across federated Backspace instances.

{/* Home instance (always pinned, non-filterable) */}
Home Instance
{window.location.host} {user?.username && ( as {user.username} )}
Local {isElectron() && ( )}
{/* Filter bar (only if registry has entries) */} {registryEntries.length > 0 && ( )} {/* Registry rows */} {sortedEntries.length > 0 && (
{sortedEntries.map((entry) => ( toggleExpand(entry.origin)} /> ))}
)} {/* Empty state */} {emptyMessage && (
{emptyMessage}
)} {registryEntries.length === 0 && !showAddForm && (
No remote instances connected. Add one to start federating.
)} {/* Add instance button / flow */} {showAddForm ? ( setShowAddForm(false)} /> ) : ( )}
); }