import { useState, useEffect } from 'react' import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faSpinner } from '@fortawesome/free-solid-svg-icons' import { EventStoreProvider, AccountsProvider, Hooks } from 'applesauce-react' import { EventStore } from 'applesauce-core' import { AccountManager, Accounts } from 'applesauce-accounts' import { registerCommonAccountTypes } from 'applesauce-accounts/accounts' import { RelayPool } from 'applesauce-relay' import { NostrConnectSigner } from 'applesauce-signers' import { createAddressLoader } from 'applesauce-loaders/loaders' import Bookmarks from './components/Bookmarks' import RouteDebug from './components/RouteDebug' import Toast from './components/Toast' import { useToast } from './hooks/useToast' import { useOnlineStatus } from './hooks/useOnlineStatus' import { RELAYS } from './config/relays' import { SkeletonThemeProvider } from './components/Skeletons' const DEFAULT_ARTICLE = import.meta.env.VITE_DEFAULT_ARTICLE_NADDR || 'naddr1qvzqqqr4gupzqmjxss3dld622uu8q25gywum9qtg4w4cv4064jmg20xsac2aam5nqqxnzd3cxqmrzv3exgmr2wfesgsmew' // AppRoutes component that has access to hooks function AppRoutes({ relayPool, showToast }: { relayPool: RelayPool showToast: (message: string) => void }) { const accountManager = Hooks.useAccountManager() const handleLogout = () => { accountManager.clearActive() showToast('Logged out successfully') } return ( } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> ) } function App() { const [eventStore, setEventStore] = useState(null) const [accountManager, setAccountManager] = useState(null) const [relayPool, setRelayPool] = useState(null) const { toastMessage, toastType, showToast, clearToast } = useToast() const isOnline = useOnlineStatus() useEffect(() => { const initializeApp = async () => { // Initialize event store, account manager, and relay pool const store = new EventStore() const accounts = new AccountManager() // Register common account types (needed for deserialization) registerCommonAccountTypes(accounts) // Create relay pool and set it up BEFORE loading accounts // NostrConnectAccount.fromJSON needs this to restore the signer const pool = new RelayPool() NostrConnectSigner.pool = pool console.log('[bunker] ✅ Pool assigned to NostrConnectSigner (before account load)') // Create a relay group for better event deduplication and management pool.group(RELAYS) console.log('[bunker] Created relay group with', RELAYS.length, 'relays (including local)') // Load persisted accounts from localStorage try { const accountsJson = localStorage.getItem('accounts') console.log('[bunker] Raw accounts from localStorage:', accountsJson) const json = JSON.parse(accountsJson || '[]') console.log('[bunker] Parsed accounts:', json.length, 'accounts') await accounts.fromJSON(json) console.log('[bunker] Loaded', accounts.accounts.length, 'accounts from storage') console.log('[bunker] Account types:', accounts.accounts.map(a => ({ id: a.id, type: a.type }))) // Load active account from storage const activeId = localStorage.getItem('active') console.log('[bunker] Active ID from localStorage:', activeId) if (activeId) { const account = accounts.getAccount(activeId) console.log('[bunker] Found account for ID?', !!account, account?.type) if (account) { accounts.setActive(activeId) console.log('[bunker] ✅ Restored active account:', activeId, 'type:', account.type) } else { console.warn('[bunker] ⚠️ Active ID found but account not in list') } } else { console.log('[bunker] No active account ID in localStorage') } } catch (err) { console.error('[bunker] ❌ Failed to load accounts from storage:', err) } // Subscribe to accounts changes and persist to localStorage const accountsSub = accounts.accounts$.subscribe(() => { localStorage.setItem('accounts', JSON.stringify(accounts.toJSON())) }) // Subscribe to active account changes and persist to localStorage const activeSub = accounts.active$.subscribe((account) => { if (account) { localStorage.setItem('active', account.id) } else { localStorage.removeItem('active') } }) // Reconnect bunker signers when active account changes // Keep track of which accounts we've already reconnected to avoid double-connecting const reconnectedAccounts = new Set() const bunkerReconnectSub = accounts.active$.subscribe(async (account) => { console.log('[bunker] Active account changed:', { hasAccount: !!account, type: account?.type, id: account?.id }) if (account && account.type === 'nostr-connect') { const nostrConnectAccount = account as Accounts.NostrConnectAccount // Skip if we've already reconnected this account if (reconnectedAccounts.has(account.id)) { console.log('[bunker] ⏭️ Already reconnected this account, skipping') return } console.log('[bunker] Account detected. Status:', { listening: nostrConnectAccount.signer.listening, isConnected: nostrConnectAccount.signer.isConnected, hasRemote: !!nostrConnectAccount.signer.remote, bunkerRelays: nostrConnectAccount.signer.relays }) try { // Add bunker's relays to the pool so signing requests can be sent/received const bunkerRelays = nostrConnectAccount.signer.relays || [] console.log('[bunker] Adding bunker relays to pool:', bunkerRelays) pool.group(bunkerRelays) // Just ensure the signer is listening for responses - don't call connect() again // The fromBunkerURI already connected with permissions during login if (!nostrConnectAccount.signer.listening) { console.log('[bunker] Opening signer subscription...') await nostrConnectAccount.signer.open() console.log('[bunker] ✅ Signer subscription opened') } else { console.log('[bunker] ✅ Signer already listening') } // Mark as connected so requireConnection() doesn't call connect() again // The bunker remembers the permissions from the initial connection nostrConnectAccount.signer.isConnected = true console.log('[bunker] Final signer status:', { listening: nostrConnectAccount.signer.listening, isConnected: nostrConnectAccount.signer.isConnected, remote: nostrConnectAccount.signer.remote, relays: nostrConnectAccount.signer.relays }) // Mark this account as reconnected reconnectedAccounts.add(account.id) console.log('[bunker] 🎉 Signer ready for signing') } catch (error) { console.error('[bunker] ❌ Failed to open signer:', error) } } }) // Keep all relay connections alive indefinitely by creating a persistent subscription // This prevents disconnection when no other subscriptions are active // Create a minimal subscription that never completes to keep connections alive const keepAliveSub = pool.subscription(RELAYS, { kinds: [0], limit: 0 }).subscribe({ next: () => {}, // No-op, we don't care about events error: (err) => console.warn('Keep-alive subscription error:', err) }) console.log('🔗 Created keep-alive subscription for', RELAYS.length, 'relay(s)') // Store subscription for cleanup ;(pool as unknown as { _keepAliveSubscription: typeof keepAliveSub })._keepAliveSubscription = keepAliveSub // Attach address/replaceable loaders so ProfileModel can fetch profiles const addressLoader = createAddressLoader(pool, { eventStore: store, lookupRelays: RELAYS }) store.addressableLoader = addressLoader store.replaceableLoader = addressLoader setEventStore(store) setAccountManager(accounts) setRelayPool(pool) // Cleanup function return () => { accountsSub.unsubscribe() activeSub.unsubscribe() bunkerReconnectSub.unsubscribe() // Clean up keep-alive subscription if it exists const poolWithSub = pool as unknown as { _keepAliveSubscription?: { unsubscribe: () => void } } if (poolWithSub._keepAliveSubscription) { poolWithSub._keepAliveSubscription.unsubscribe() } } } let cleanup: (() => void) | undefined initializeApp().then((fn) => { cleanup = fn }) return () => { if (cleanup) cleanup() } }, []) // Monitor online/offline status useEffect(() => { if (!isOnline) { showToast('You are offline. Some features may be limited.') } }, [isOnline, showToast]) // Listen for service worker updates useEffect(() => { const handleSWUpdate = () => { showToast('New version available! Refresh to update.') } window.addEventListener('sw-update-available', handleSWUpdate) return () => { window.removeEventListener('sw-update-available', handleSWUpdate) } }, [showToast]) if (!eventStore || !accountManager || !relayPool) { return (
) } return (
{toastMessage && ( )}
) } export default App