From 4caad49dfb9a91eaa91641a6e69ed0eab66a4cd0 Mon Sep 17 00:00:00 2001 From: "shakespeare.diy" Date: Wed, 17 Jun 2026 23:15:54 +0700 Subject: [PATCH] Fix profile/relay loading after Amber login + add Citrine support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug 1 - AppProvider was wiping NIP-65 relay list: - Relay migration check was comparing stored relays vs defaults and resetting them if they differed — overwriting the user's own NIP-65 list - Fix: only reset relays to defaults if updatedAt === 0 (never synced) and relays array is empty Bug 2 - NostrSync not finding profile on mobile: - Add Citrine local relay detection (ws://localhost:4869) - If Citrine is running, query it first — instant local data - Add BOOTSTRAP_RELAYS (purplepag.es, relay.nostr.band, nos.lol) - Always try these for profile+relays before main pool - syncRelays() now accepts optional relay group param - Increase initial delay to 1500ms in PWA mode (800ms in browser) to give WebSocket connections time to establish after Amber redirect Co-authored-by: shakespeare.diy --- src/components/AppProvider.tsx | 22 +++--- src/components/NostrSync.tsx | 124 +++++++++++++++++++++++---------- 2 files changed, 96 insertions(+), 50 deletions(-) diff --git a/src/components/AppProvider.tsx b/src/components/AppProvider.tsx index 0358861..d35c807 100644 --- a/src/components/AppProvider.tsx +++ b/src/components/AppProvider.tsx @@ -55,35 +55,29 @@ export function AppProvider(props: AppProviderProps) { // Merge configs with special handling for relay metadata const config = { ...defaultConfig, ...rawConfig }; - // Force migration to correct relays and dark theme + // Force dark theme and seed default relays only for brand-new users useEffect(() => { let needsUpdate = false; const updates: Partial = {}; - // Check relay configuration - if (config.relayMetadata.relays) { - const currentRelayUrls = config.relayMetadata.relays.map(r => r.url).sort().join(','); - const expectedRelayUrls = defaultConfig.relayMetadata.relays.map(r => r.url).sort().join(','); - - if (currentRelayUrls !== expectedRelayUrls) { - console.log('Migrating relays to:', defaultConfig.relayMetadata.relays.map(r => r.url)); + // Only reset relays to defaults if the user has never synced their own NIP-65 + // relay list (updatedAt === 0 means no NIP-65 event has ever been fetched). + // If updatedAt > 0, the user has their own relay list — never overwrite it. + if (!config.relayMetadata.relays || config.relayMetadata.relays.length === 0) { + if (config.relayMetadata.updatedAt === 0) { updates.relayMetadata = defaultConfig.relayMetadata; needsUpdate = true; } } - // Force dark theme (app is dark-only now) + // Force dark theme (app is dark-only) if (config.theme !== 'dark') { - console.log('Forcing dark theme'); updates.theme = 'dark'; needsUpdate = true; } if (needsUpdate) { - updateConfig((current) => ({ - ...current, - ...updates, - })); + updateConfig((current) => ({ ...current, ...updates })); } }, []); diff --git a/src/components/NostrSync.tsx b/src/components/NostrSync.tsx index 328a2d2..c13b461 100644 --- a/src/components/NostrSync.tsx +++ b/src/components/NostrSync.tsx @@ -7,22 +7,44 @@ import { useCurrentUser } from '@/hooks/useCurrentUser'; import { useAppContext } from '@/hooks/useAppContext'; import { useToast } from '@/hooks/useToast'; +// Citrine local relay (runs on Android device via the Citrine app) +// https://github.com/greenart7c3/Citrine +const CITRINE_URL = 'ws://localhost:4869'; + // Default relays for new users const NEW_USER_RELAYS = [ { url: 'wss://auth.nostr1.com', read: true, write: true }, { url: 'wss://wotr.relatr.xyz', read: true, write: true }, { url: 'wss://relay.ditto.pub', read: true, write: true }, { url: 'wss://goskatespots.nostr1.com', read: true, write: true }, - { url: 'wss://purplepag.es', read: true, write: false }, // Profile aggregator + { url: 'wss://purplepag.es', read: true, write: false }, ]; -// Fallback relays for fetching profile data (aggregators that index many profiles) -const FALLBACK_RELAYS = [ +// Bootstrap relays — always checked for profile even if user has their own relay list. +// purplepag.es indexes kind 0 + kind 10002 for most pubkeys worldwide. +const BOOTSTRAP_RELAYS = [ 'wss://purplepag.es', - 'wss://relay.damus.io', + 'wss://relay.nostr.band', 'wss://nos.lol', ]; +/** + * Try to connect to Citrine local relay. + * Returns true if Citrine is reachable within 1.5 seconds. + */ +async function isCitrineAvailable(): Promise { + return new Promise((resolve) => { + try { + const ws = new WebSocket(CITRINE_URL); + const timer = setTimeout(() => { ws.close(); resolve(false); }, 1500); + ws.onopen = () => { clearTimeout(timer); ws.close(); resolve(true); }; + ws.onerror = () => { clearTimeout(timer); resolve(false); }; + } catch { + resolve(false); + } + }); +} + /** * NostrSync - Syncs user's Nostr data on login * @@ -75,22 +97,28 @@ export function NostrSync() { const syncUserData = async () => { setSyncStatus('syncing'); - // FIX B: Invalidate any stale cached data for this user BEFORE fetching - // This ensures we don't use old cached empty results + // Invalidate stale cache before fetching fresh data queryClient.invalidateQueries({ queryKey: ['author', user.pubkey] }); - - // Also invalidate the logins cache so useLoggedInAccounts refetches const loginsKey = logins.map((l) => l.id).join(';'); queryClient.invalidateQueries({ queryKey: ['nostr', 'logins', loginsKey] }); + // Check if Citrine local relay is running — if so, use it first. + // Citrine stores all the user's events locally, so it's the fastest + // and most reliable source for profile + relay list on Android. + const citrineAvailable = await isCitrineAvailable(); + if (citrineAvailable) { + console.log('[NostrSync] Citrine detected — using local relay first'); + } + let relaysSynced = false; let profileSynced = false; // Sync relay list (NIP-65) - const syncRelays = async (): Promise => { - const events = await nostr.query( + const syncRelays = async (relayGroup?: ReturnType): Promise => { + const source = relayGroup || nostr; + const events = await source.query( [{ kinds: [10002], authors: [user.pubkey], limit: 1 }], - { signal: AbortSignal.timeout(10000) } + { signal: AbortSignal.timeout(8000) } ); if (events.length > 0) { @@ -189,30 +217,51 @@ export function NostrSync() { return false; }; - // Try main relays first with retry - const mainResult = await syncWithRetry(async () => { - const [relayResult, profileResult] = await Promise.all([ - syncRelays(), - syncProfile() - ]); - return { relays: relayResult, profile: profileResult }; - }, 3, 1500); - - if (mainResult) { - relaysSynced = mainResult.relays; - profileSynced = mainResult.profile; + // 1. Try Citrine first if available — instant local data + if (citrineAvailable) { + try { + const citrineGroup = nostr.group([CITRINE_URL]); + const [relayResult, profileResult] = await Promise.all([ + syncRelays(citrineGroup), + syncProfile(citrineGroup), + ]); + relaysSynced = relayResult; + profileSynced = profileResult; + console.log('[NostrSync] Citrine sync:', { relaysSynced, profileSynced }); + } catch (e) { + console.warn('[NostrSync] Citrine sync failed:', e); + } } - // If profile not found on main relays, try fallback relays - if (!profileSynced) { - - const fallbackResult = await syncWithRetry(async () => { - const fallbackGroup = nostr.group(FALLBACK_RELAYS); - return await syncProfile(fallbackGroup); - }, 3, 2000); + // 2. Try bootstrap relays (purplepag.es etc.) — always fast for profile/relays + if (!profileSynced || !relaysSynced) { + try { + const bootstrapGroup = nostr.group(BOOTSTRAP_RELAYS); + const [relayResult, profileResult] = await Promise.all([ + relaysSynced ? Promise.resolve(true) : syncRelays(bootstrapGroup), + profileSynced ? Promise.resolve(true) : syncProfile(bootstrapGroup), + ]); + if (!relaysSynced) relaysSynced = relayResult; + if (!profileSynced) profileSynced = profileResult; + console.log('[NostrSync] Bootstrap sync:', { relaysSynced, profileSynced }); + } catch (e) { + console.warn('[NostrSync] Bootstrap sync failed:', e); + } + } - if (fallbackResult) { - profileSynced = true; + // 3. Try main configured relays with retry + if (!profileSynced || !relaysSynced) { + const mainResult = await syncWithRetry(async () => { + const [relayResult, profileResult] = await Promise.all([ + relaysSynced ? Promise.resolve(true) : syncRelays(), + profileSynced ? Promise.resolve(true) : syncProfile(), + ]); + return { relays: relayResult, profile: profileResult }; + }, 3, 1500); + + if (mainResult) { + if (!relaysSynced) relaysSynced = mainResult.relays; + if (!profileSynced) profileSynced = mainResult.profile; } } @@ -232,12 +281,15 @@ export function NostrSync() { } }; - // FIX C: Longer initial delay to allow relay connections to establish - // This helps with external signers like Amber where PWA context may need time - // to re-establish WebSocket connections after returning from the signer app + // Give relay WebSocket connections time to establish, especially important + // for PWA/Amber where the app context is fresh after returning from another app. + // Also gives Citrine time to be detected. + const isPWA = window.matchMedia('(display-mode: standalone)').matches; + const delay = isPWA ? 1500 : 800; + const timeoutId = setTimeout(() => { syncUserData(); - }, 800); + }, delay); return () => clearTimeout(timeoutId); }, [user, config.relayMetadata.updatedAt, nostr, updateConfig, toast, queryClient, logins]);