Full self-audit of the Citrine feature. Fixes found: Robustness - Probe now tries BOTH ws://127.0.0.1 and ws://localhost and routes to whichever actually answers (probeCitrineEndpoints + CitrineProbeResult.url). We no longer bet the whole feature on one loopback address form; useCitrine.url reflects the winning endpoint so reads/writes go to the right place. Correctness - Preview toggle was mutating the REAL setting while in dev-preview; handleToggle is now a no-op under preview (forced state owns the visuals). Accessibility / "flawless" bar - Respect prefers-reduced-motion: status pips, chip pop-in and the expand slide now motion-reduce:animate-none (no looping glow/pulse for reduced-motion users). - Recheck button gets an aria-label; status text is role="status" aria-live so screen readers announce searching→detected transitions; decorative pips are aria-hidden. UX copy - "Waiting" state no longer presumes Citrine isn't installed. Clearer two-step guidance (Open Citrine → Start; come back → Recheck) covering the common "installed but not started/detected yet" case. Honest note: MIXED_CONTENT_ALWAYS_ALLOW (1.3.5) is the high-probability fix for the 0-connections block but is not device-verified from here; the dual-endpoint probe hedges the second most likely cause (address form). Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
99 lines
4.1 KiB
TypeScript
99 lines
4.1 KiB
TypeScript
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,
|
|
};
|
|
}
|