import { useCallback } from 'react'; import { useQuery } from '@tanstack/react-query'; import { useAppContext } from '@/hooks/useAppContext'; import { CITRINE_RELAY_URL, citrinePreview, isCitrinePossible, probeCitrineEndpoints, } from '@/lib/citrine'; /** * useCitrine — availability + opt-in state for the local on-device Citrine relay. * * Exposes everything the app needs to decide whether the local relay should be * folded into reads/writes: * - `possible`: cheap sync gate — are we on native Android at all? * - `reachable`: async probe result — is a Citrine relay actually running? * - `enabled`: the user's opt-in setting (config.useCitrineRelay). * - `active`: enabled AND reachable — the single boolean callers should key on. * - `url`: the effective relay URL to use when `active`, else null. * - `setEnabled`: toggle the opt-in setting. * * The reachability probe is cached via react-query so the whole app shares one * result and we don't spam localhost connection attempts. On non-Android the * probe short-circuits to false, so this hook is a safe no-op everywhere else. */ export function useCitrine() { const { config, updateConfig } = useAppContext(); const possible = isCitrinePossible(); const enabled = config.useCitrineRelay ?? false; const { data: probe, refetch, isFetching, isFetched } = useQuery({ queryKey: ['citrine-reachable'], // Only bother probing when the platform could host Citrine AND the user has // opted in — no point connecting to localhost otherwise. enabled: possible && enabled, // Try each candidate endpoint (127.0.0.1 / localhost) and use whichever // actually answers as a Nostr relay. queryFn: () => probeCitrineEndpoints(), staleTime: 60_000, // Poll only while we're still WAITING for Citrine (not yet reachable), so a // freshly-started relay is picked up automatically. Once connected, stop the // background churn — a per-session localhost relay doesn't vanish silently, // and any failed read will naturally re-probe. This avoids opening a fresh // ws:// probe every few minutes for the whole session. refetchInterval: (query) => (query.state.data?.ok ? false : 30_000), // Re-verify on focus only until connected (e.g. user returns after starting // Citrine). No need to re-poke localhost on every Amber round-trip once up. refetchOnWindowFocus: (query) => !query.state.data?.ok, retry: false, }); const reachable = probe?.ok ?? false; const active = possible && enabled && reachable; // DEV PREVIEW ONLY: a forced visual state for browser design review. Purely // cosmetic — it never affects `active`/`url`, so no traffic is ever rerouted. const preview = citrinePreview(); const setEnabled = useCallback( (next: boolean) => { updateConfig((current) => ({ ...current, useCitrineRelay: next })); }, [updateConfig], ); return { possible, enabled, reachable, active, /** True while a reachability probe is in flight (for spinner UI). */ probing: isFetching, /** * True once a probe has completed at least once. Used to avoid flashing the * "not detected" state on the very first render after enabling, before the * initial probe has had a chance to run. */ settled: isFetched, /** Diagnostic reason from the last probe (shown on-device for debugging). */ reason: probe?.reason ?? null, /** The relay URL we probe/connect to — shown in the debug readout. */ probeUrl: probe?.url ?? CITRINE_RELAY_URL, // Route to the endpoint that actually answered (127.0.0.1 or localhost), // falling back to the default when active but somehow missing a url. url: active ? (probe?.url ?? CITRINE_RELAY_URL) : null, /** * DEV PREVIEW ONLY — a forced UI state ('off'|'searching'|'live'|'waiting') * when the citrine:preview flag is set, else null. Cosmetic; does not affect * `active`/`url`. The Settings card uses it to override its computed state. */ preview, setEnabled, /** Re-run the reachability probe (e.g. after the user starts Citrine). */ recheck: refetch, }; }