import React, { useState, useEffect, useMemo } from 'react'; import { Modal } from '../ui/Modal'; import { useUIStore } from '../../stores/uiStore'; import { useSpaceStore, NotConnectedError } from '../../stores/spaceStore'; import { useInstanceStore, DifferentPasswordError } from '../../stores/instanceStore'; import { useAuthStore } from '../../stores/authStore'; import { useExploreStore } from '../../stores/exploreStore'; import { useNavigate } from 'react-router-dom'; import { parseInviteInput } from '../../utils/inviteParser'; import { ExploreSpacePreviewCard } from './ExploreSpacePreviewCard'; type JoinPhase = 'input' | 'connect' | 'fallback'; export function JoinSpaceModal() { const [inviteCode, setInviteCode] = useState(''); const [error, setError] = useState(''); const [isLoading, setIsLoading] = useState(false); const [phase, setPhase] = useState('input'); const [parsedCode, setParsedCode] = useState(''); const [parsedOrigin, setParsedOrigin] = useState(''); const [password, setPassword] = useState(''); const [fallbackUsername, setFallbackUsername] = useState(''); const [fallbackPassword, setFallbackPassword] = useState(''); const activeModal = useUIStore((s) => s.activeModal); const closeModal = useUIStore((s) => s.closeModal); const joinByCode = useSpaceStore((s) => s.joinByCode); const connectToRemote = useInstanceStore((s) => s.connectToRemote); const loginToRemote = useInstanceStore((s) => s.loginToRemote); const user = useAuthStore((s) => s.user); const navigate = useNavigate(); const isMobile = useUIStore((s) => s.isMobile); const pushMobileScreen = useUIStore((s) => s.pushMobileScreen); const discoverySpaces = useExploreStore((s) => s.spaces); const discoveryLoading = useExploreStore((s) => s.isLoading); const discoveryEnabled = useExploreStore((s) => s.discoveryEnabled); const discoveryError = useExploreStore((s) => s.error); const fetchSpaces = useExploreStore((s) => s.fetchSpaces); const fetchMyRequests = useExploreStore((s) => s.fetchMyRequests); const isOpen = activeModal === 'joinSpace'; // Fetch discoverable spaces when the modal opens. Fire-and-forget; the invite // section never depends on this resolving. useEffect(() => { if (isOpen) { void fetchSpaces(); void fetchMyRequests(); } }, [isOpen, fetchSpaces, fetchMyRequests]); const previewSpaces = useMemo( () => discoverySpaces.filter((s) => !s.joined).slice(0, 6), [discoverySpaces], ); const handleBrowseExplore = () => { closeModal(); if (isMobile) pushMobileScreen('explore'); else navigate('/explore'); }; const handlePreviewJoinSuccess = (spaceId: string) => { closeModal(); navigate(`/channels/${spaceId}`); }; // Reset state on close useEffect(() => { if (!isOpen) { setInviteCode(''); setError(''); setPhase('input'); setParsedCode(''); setParsedOrigin(''); setPassword(''); setFallbackUsername(''); setFallbackPassword(''); } }, [isOpen]); const joinAndNavigate = async (code: string, origin?: string) => { const space = await joinByCode(code, origin || undefined); closeModal(); navigate(`/channels/${space.id}`); }; // Phase 1: Submit invite code/URL const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setError(''); let parsed: { code: string; origin?: string }; try { parsed = parseInviteInput(inviteCode); } catch (err) { setError((err as Error).message); return; } setParsedCode(parsed.code); setParsedOrigin(parsed.origin || ''); setIsLoading(true); try { await joinAndNavigate(parsed.code, parsed.origin); } catch (err) { if (err instanceof NotConnectedError) { setPhase('connect'); setError(''); } else { setError(err instanceof Error ? err.message : 'Failed to join space'); } } finally { setIsLoading(false); } }; // Phase 2: Connect to remote instance with password, then join const handleConnect = async (e: React.FormEvent) => { e.preventDefault(); setError(''); setIsLoading(true); try { await connectToRemote(parsedOrigin, password, user?.displayName || undefined); await joinAndNavigate(parsedCode, parsedOrigin); } catch (err) { if (err instanceof DifferentPasswordError) { setPhase('fallback'); setFallbackUsername(err.remoteUsername); setFallbackPassword(''); setError(''); } else { setError((err as Error).message); } } finally { setIsLoading(false); } }; // Phase 3: Fallback login with different credentials, then join const handleFallbackLogin = async (e: React.FormEvent) => { e.preventDefault(); setError(''); setIsLoading(true); try { await loginToRemote(parsedOrigin, fallbackUsername, fallbackPassword); await joinAndNavigate(parsedCode, parsedOrigin); } catch (err) { setError((err as Error).message); } finally { setIsLoading(false); } }; let hostDisplay = ''; try { if (parsedOrigin) hostDisplay = new URL(parsedOrigin).host; } catch { /* ignore */ } return ( {/* Error display (shared across all phases) */} {error && (
{error}
)} {/* Phase: input — discovery-first, with invite code as a secondary path */} {phase === 'input' && (
{/* ── Discovery section ── */} {discoveryEnabled ? (

Discover spaces to join, or browse them all in Explore.

{discoveryLoading && previewSpaces.length === 0 ? (
{[0, 1, 2].map((i) => (
))}
) : discoveryError ? (
Couldn’t load spaces to discover right now. You can still join with an invite code below.
) : previewSpaces.length === 0 ? (
No spaces to discover yet — try an invite code below, or check back later.
) : (
{previewSpaces.map((space) => ( ))}
)}
) : (
Space discovery is turned off on this instance. You can still join with an invite code.
)} {/* Divider */}
Have an invite code?
{/* ── Invite-code section (secondary) ── */}
setInviteCode(e.target.value)} className="input-standard w-full" placeholder="e.g. abc123 or https://instance.com/join/abc123" />
)} {/* Phase: connect — password prompt to connect to remote instance */} {phase === 'connect' && (

Connect to {hostDisplay} to join this space.

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.
)} {/* Phase: fallback — different password on remote instance */} {phase === 'fallback' && (
An account already exists on {hostDisplay} with a different password. Enter the credentials you used on that instance.
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" />
)} ); }