import { isTauri } from "@tauri-apps/api/core"; import { emit } from "@tauri-apps/api/event"; import { QueryClientProvider } from "@tanstack/react-query"; import { RouterProvider } from "@tanstack/react-router"; import { type ReactNode, useCallback, useEffect, useLayoutEffect, useRef, useState, } from "react"; import { router } from "@/app/router"; import { ThemeGrainientBackground } from "@/app/ThemeGrainientBackground"; import { useReloadShortcut } from "@/app/useReloadShortcut"; import { KnownAgentPubkeysProvider } from "@/features/agents/useKnownAgentPubkeys"; import { useAppOnboardingState } from "@/features/onboarding/hooks"; import { useMachineOnboardingState } from "@/features/onboarding/machineOnboarding"; import { type FirstCommunityPage, useCommunityOnboarding, markCommunityOnboardingComplete, resolveProfileCheckAction, isTransactionStillConnecting, } from "@/features/onboarding/communityOnboarding"; import { CommunityOnboardingFlow } from "@/features/onboarding/ui/CommunityOnboardingFlow"; import { MachineOnboardingFlow, type MachineOnboardingPage, } from "@/features/onboarding/ui/MachineOnboardingFlow"; import { OnboardingFlow } from "@/features/onboarding/ui/OnboardingFlow"; import { PendingInviteGate } from "@/features/onboarding/ui/PendingInviteGate"; import { KeyringLockedScreen } from "@/features/onboarding/ui/KeyringLockedScreen"; import { RelaunchRequiredScreen } from "@/features/onboarding/ui/RelaunchRequiredScreen"; import { ResetFailedScreen } from "@/features/onboarding/ui/ResetFailedScreen"; import { useCommunityInit } from "@/features/communities/useCommunityInit"; import { useNestNotifications } from "@/features/communities/useNestNotifications"; import { useCommunities } from "@/features/communities/useCommunities"; import { onAddCommunityPrefillAvailable, requestAddCommunityPrefill, } from "@/features/communities/addCommunityPrefill"; import { WelcomeSetup } from "@/features/communities/ui/WelcomeSetup"; import { CommunityApplyErrorScreen } from "@/features/communities/ui/CommunityApplyErrorScreen"; import { CommunityChangeOverlay } from "@/features/communities/ui/CommunityChangeOverlay"; import { setAvatarProfileSyncQueryClient } from "@/features/profile/avatarProfileSync"; import { createBuzzQueryClient } from "@/shared/api/queryClient"; import { isSharedIdentity as isSharedIdentityCmd } from "@/shared/api/tauri"; import { getProfile } from "@/shared/api/tauriProfiles"; import { type AddCommunityDeepLinkPayload, listenForDeepLinks, } from "@/shared/deep-link"; import { cn } from "@/shared/lib/cn"; import { BuzzMark } from "@/shared/ui/buzz-logo/BuzzMark"; import { FlappingBee } from "@/shared/ui/buzz-logo/FlappingBee"; import { FuzzyLogo } from "@/shared/ui/buzz-logo/FuzzyLogo"; import { StartupWindowDragRegion } from "@/shared/ui/StartupWindowDragRegion"; const LOADING_TEXT = "Setting up your community..."; // Minimum time the cold-boot splash stays on screen. A real boot resolves the // community in well under 100ms, and the native window setup plus first paint // can take longer than that — without a hold, the bee is unmounted before it is // ever visible. The hold runs as an overlay above the already-mounted app, so // time-to-interactive is unchanged; only the reveal waits. const BOOT_SPLASH_MIN_VISIBLE_MS = 1_200; const BOOT_SPLASH_FADE_MS = 200; const INITIAL_RENDER_READY_EVENT = "initial-render-ready"; type BootSplashPhase = "holding" | "fading" | "done"; function useInitialRenderReady() { useLayoutEffect(() => { if (!isTauri()) { return; } void emit(INITIAL_RENDER_READY_EVENT); }, []); } // E2E runs skip the hold (it would slow every spec's boot and block pointer // actionability); a spec can opt back in via __BUZZ_E2E__.bootSplashHoldMs. function bootSplashHoldMs(): number { const e2e = ( window as Window & { __BUZZ_E2E__?: { bootSplashHoldMs?: number }; } ).__BUZZ_E2E__; if (e2e) { return e2e.bootSplashHoldMs ?? 0; } return BOOT_SPLASH_MIN_VISIBLE_MS; } function useBootSplashHold(): BootSplashPhase { const [phase, setPhase] = useState(() => bootSplashHoldMs() > 0 ? "holding" : "done", ); useEffect(() => { const holdMs = bootSplashHoldMs(); if (holdMs <= 0) { return; } const fadeTimer = window.setTimeout(() => setPhase("fading"), holdMs); const doneTimer = window.setTimeout( () => setPhase("done"), holdMs + BOOT_SPLASH_FADE_MS, ); return () => { window.clearTimeout(fadeTimer); window.clearTimeout(doneTimer); }; }, []); return phase; } // Animated Buzz mark for the loading gates. The static BuzzMark renders in // normal flow and sizes the box — it's plain SVG (no JS/SMIL), so it paints on // the very first frame even before scripting starts, avoiding a blank flash on // hard reload. The animated FuzzyLogo is layered on top and takes over once it // begins playing. function BeeLoader({ ariaLabel, className, tintClassName = "text-foreground", }: { ariaLabel: string; className?: string; tintClassName?: string; }) { return (
); } // Cold boot gate: the theme-adaptive grainient background with a single // centered Buzz bee flying over it — the same static mark as before, now with // its wings flapping (ported from the Buzz website's wing-flap). Replaces the // old "Setting up your community" text, which stays as an sr-only caption. function AppLoadingGate() { return (
{LOADING_TEXT}
); } // Quiet gate for switching between already-set-up communities: visually empty // unless the switch takes long, so fast switches don't flash the boot splash. function CommunitySwitchGate() { const [showSpinner, setShowSpinner] = useState(false); useEffect(() => { const timer = window.setTimeout(() => setShowSpinner(true), 300); return () => window.clearTimeout(timer); }, []); return (
Switching community… {showSpinner ? ( ) : null}
); } function CommunityQueryProvider({ children }: { children: ReactNode }) { const [queryClient] = useState(createBuzzQueryClient); useEffect(() => setAvatarProfileSyncQueryClient(queryClient), [queryClient]); useEffect(() => { const e2eWindow = window as Window & { __BUZZ_E2E__?: unknown; __BUZZ_E2E_QUERY_CLIENT__?: typeof queryClient; }; if (!e2eWindow.__BUZZ_E2E__) { return; } e2eWindow.__BUZZ_E2E_QUERY_CLIENT__ = queryClient; return () => { if (e2eWindow.__BUZZ_E2E_QUERY_CLIENT__ === queryClient) { delete e2eWindow.__BUZZ_E2E_QUERY_CLIENT__; } }; }, [queryClient]); return ( {children} ); } function AppReady({ isSharedIdentity, isCommunitySwitch, }: { isSharedIdentity: boolean; isCommunitySwitch: boolean; }) { const onboarding = useAppOnboardingState(isSharedIdentity); if (onboarding.stage === "reset-failed") { return ; } if (onboarding.stage === "keyring-locked") { return ; } if (onboarding.stage === "relaunch-required") { return ; } if (onboarding.stage === "onboarding") { return ( ); } if (onboarding.stage === "blocking") { return isCommunitySwitch ? : ; } return ( ); } function CommunityApp({ currentPubkey, onBackToMachineConfig, sharedIdentity, }: { currentPubkey: string | null; onBackToMachineConfig: () => void; sharedIdentity: boolean; }) { const { activeCommunity, communities, reinitKey, addCommunity, clearCommunities, removeCommunity, switchCommunity, reconnectCommunity, } = useCommunities(); const communityOnboarding = useCommunityOnboarding(); const connectingTransactionRef = useRef(null); // Tracks the ID of the profile-check request that has been launched for the // current connecting transaction. Prevents the effect from launching a // second request if it re-runs while a fetch is in flight. const profileCheckTransactionRef = useRef(null); // Always reflects the live transaction object so async callbacks can perform // an atomic check of both ID and stage before mutating state. const transactionRef = useRef(communityOnboarding.transaction); transactionRef.current = communityOnboarding.transaction; const [isCommunityChangeOpen, setIsCommunityChangeOpen] = useState(false); const [resumeFirstCommunityPage, setResumeFirstCommunityPage] = useState(null); // Surface nest-related backend events (repos-dir errors, legacy migration) // as toasts. Mounted before useCommunityInit so the listeners are registered // ahead of the first apply_workspace call. useNestNotifications(); // Composite key: changes when community ID changes OR when // the active community's config is updated (relayUrl/token). const communityKey = `${activeCommunity?.id ?? "none"}-${reinitKey}`; // Latch once the community key deviates from its cold-boot value: from then // on, loading phases are in-app switches and get the quiet gate instead of // the full "Setting up your community" splash. const initialCommunityKeyRef = useRef(communityKey); const hasSwitchedCommunityRef = useRef(false); if (communityKey !== initialCommunityKeyRef.current) { hasSwitchedCommunityRef.current = true; } const isCommunitySwitch = hasSwitchedCommunityRef.current; const community = useCommunityInit( activeCommunity, communityKey, sharedIdentity, ); const handleCommunityOnboardingConnect = useCallback(() => { const transaction = communityOnboarding.transaction; if (transaction?.stage !== "connecting") return; if (connectingTransactionRef.current === transaction.id) return; connectingTransactionRef.current = transaction.id; if (transaction.communityId) { switchCommunity(transaction.communityId); return; } const previousCommunityId = activeCommunity?.id; const relayAlreadyExists = communities.some( (community) => community.relayUrl === transaction.relayUrl, ); const id = addCommunity({ id: crypto.randomUUID(), name: transaction.communityName, relayUrl: transaction.relayUrl, token: transaction.token, reposDir: transaction.reposDir, pubkey: currentPubkey ?? undefined, addedAt: new Date().toISOString(), }); communityOnboarding.update({ communityId: id, previousCommunityId, addedCommunity: !relayAlreadyExists, error: undefined, }); switchCommunity(id); reconnectCommunity(); }, [ activeCommunity?.id, addCommunity, communities, communityOnboarding, currentPubkey, reconnectCommunity, switchCommunity, ]); const handleCommunityOnboardingCancel = useCallback(() => { const transaction = communityOnboarding.transaction; communityOnboarding.clear(); if (!transaction?.communityId) return; if (!transaction.addedCommunity) { if (transaction.previousCommunityId) { switchCommunity(transaction.previousCommunityId); } return; } if (communities.length === 1) { if (transaction.source === "first-community") { setResumeFirstCommunityPage(transaction.firstCommunityPage ?? "join"); } clearCommunities(); return; } removeCommunity(transaction.communityId); if (transaction.previousCommunityId) { switchCommunity(transaction.previousCommunityId); } }, [ clearCommunities, communities.length, communityOnboarding, removeCommunity, switchCommunity, ]); const bootSplashPhase = useBootSplashHold(); const transaction = communityOnboarding.transaction; useEffect(() => { if (transaction?.stage !== "connecting") { connectingTransactionRef.current = null; profileCheckTransactionRef.current = null; } }, [transaction?.stage]); const targetIsReady = transaction?.communityId === activeCommunity?.id && community.isReady && community.appliedKey === communityKey; useEffect(() => { if (transaction?.stage !== "connecting" || !targetIsReady) return; const transactionId = transaction.id; const relayUrl = transaction.relayUrl; if (profileCheckTransactionRef.current === transactionId) return; profileCheckTransactionRef.current = transactionId; // resolveProfileCheckAction resolves exactly once (Promise.race + timer // cleared on settle), so no settled flag is needed here. void resolveProfileCheckAction(getProfile, 10_000).then((result) => { // Atomic staleness guard via isTransactionStillConnecting: the // transaction must still be the same one that launched this request // AND still be in connecting. Covers cancel+replacement (B's ID !== A's) // and cancel-without-replacement (transactionRef.current is null). if (!isTransactionStillConnecting(transactionRef.current, transactionId)) return; if (result.action === "skip") { markCommunityOnboardingComplete(result.profile.pubkey, relayUrl); communityOnboarding.clear(); } else { communityOnboarding.update( { stage: "profile", error: undefined }, transactionId, ); } }); }, [ communityOnboarding, targetIsReady, transaction?.stage, transaction?.id, transaction?.relayUrl, ]); // During "entering" the transaction stays alive as a curtain: the app mounts // underneath (already pointed at the Welcome channel route) while the // onboarding screen covers it, then fades once Welcome reports ready. // // The flow must keep ONE stable position in the element tree across every // stage. Rendering it from a different slot when the stage flips to // "entering" would remount it — React state resets and the "Meet your // starter team" screen visibly restarts mid-handoff. const isEnteringCurtain = transaction?.stage === "entering"; // The app mounts (and starts loading data) beneath the splash overlay; the // overlay just keeps the bee on screen long enough to be seen, then fades. // Community switches keep their quiet gate. const showBootSplashOverlay = bootSplashPhase !== "done" && !isCommunitySwitch; let appContent: ReactNode = null; if (!transaction) { if (community.needsSetup) { // Show welcome setup for first-run users with no communities appContent = ( ); } else if ("error" in community && community.error) { // Surface apply failures so the user can retry or change community. appContent = ( <> setIsCommunityChangeOpen(true)} onRetry={reconnectCommunity} /> {isCommunityChangeOpen ? ( setIsCommunityChangeOpen(false)} /> ) : null} ); } } // Wait for this exact community config to be applied to the backend before // rendering anything that connects to the relay. The appliedKey check avoids // a one-render race where React sees the new active community while the // Tauri backend is still configured for the previous one. const communityApplied = community.isReady && community.appliedKey === communityKey; if (appContent === null && (!transaction || isEnteringCurtain)) { appContent = communityApplied ? ( {showBootSplashOverlay ? ( ) : null} ) : isCommunitySwitch ? ( ) : ( ); } return ( <> {appContent} {transaction ? (
) : null} ); } function MachineBootstrap({ sharedIdentity }: { sharedIdentity: boolean }) { const { activeCommunity } = useCommunities(); const communityOnboarding = useCommunityOnboarding(); const machine = useMachineOnboardingState({ activeCommunityPubkey: activeCommunity ? (activeCommunity.pubkey ?? null) : undefined, isSharedIdentity: sharedIdentity, }); const [machineInitialPage, setMachineInitialPage] = useState(); const reopenMachineConfig = useCallback(() => { setMachineInitialPage("config"); machine.reopen(); }, [machine.reopen]); const completeMachineOnboarding = useCallback( (pubkey?: string) => { setMachineInitialPage(undefined); machine.complete(pubkey); }, [machine.complete], ); const openAddCommunity = useCallback( (payload: AddCommunityDeepLinkPayload & { requestId: string }) => activeCommunity ? requestAddCommunityPrefill(payload) : communityOnboarding.start({ source: "add-community", relayUrl: payload.relayUrl, communityName: payload.name, }), [activeCommunity, communityOnboarding.start], ); // Deep links are captured here — above the machine-onboarding gate — not in // CommunityApp. The Rust side queues them; draining into the persisted // community-onboarding transaction immediately means an invite opened on a // fresh install is acknowledged on screen while the identity steps are // still pending, and survives a relaunch in between. useEffect(() => { const unlisten = listenForDeepLinks({ startCommunityOnboarding: communityOnboarding.start, openAddCommunity, onAddCommunityAvailable: onAddCommunityPrefillAvailable, }); return () => { void unlisten.then((fn) => fn()); }; }, [communityOnboarding.start, openAddCommunity]); if (machine.stage === "reset-failed") return ; if (machine.stage === "keyring-locked") return ; if (machine.stage === "relaunch-required") return ; if (machine.stage === "blocking") return ; if (machine.stage === "ready") { return ( ); } // A community deep link that arrived before machine onboarding finished is // persisted immediately and acknowledged here. Invite claiming waits until // setup completes so it is signed only by the user's final identity. const transaction = communityOnboarding.transaction; const isDeepLink = transaction?.source === "deep-link-join" || transaction?.source === "deep-link-connect"; const shouldAcknowledgeDeepLink = isDeepLink && !transaction.acknowledged; return ( <> {shouldAcknowledgeDeepLink ? : null} ); } export function App() { useReloadShortcut(); useInitialRenderReady(); const [sharedIdentity, setSharedIdentity] = useState(null); const [queryClient] = useState(createBuzzQueryClient); useEffect(() => { isSharedIdentityCmd() .then(setSharedIdentity) .catch((err) => { console.warn("is_shared_identity command failed:", err); setSharedIdentity(false); }); }, []); if (sharedIdentity === null) return ; return ( ); }