diff --git a/nostr/commit-signatures.jsonl b/nostr/commit-signatures.jsonl index 493f5d0..ec6afd7 100644 --- a/nostr/commit-signatures.jsonl +++ b/nostr/commit-signatures.jsonl @@ -70,3 +70,4 @@ {"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1771924650,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","pass announcement"]],"content":"Signed commit: pass announcement","id":"57e1440848e4b322a9b10a6dff49973f29c8dd20b85f6cc75fd40d32eb04f0e4","sig":"3866152051a42592e83a1850bf9f3fd49af597f7dcdb523ef39374d528f6c46df6118682cac3202c29ce89a90fec8b4284c68a57101c6c590d8d1a184cac9731"} {"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1771949714,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","fallback to API if registered clone unavailble"]],"content":"Signed commit: fallback to API if registered clone unavailble","id":"4921a95aea13f6f72329ff8a278a8ff6321776973e8db327d59ea62b90d363cc","sig":"0efffc826cad23849bd311be582a70cb0a42f3958c742470e8488803c5882955184b9241bf77fcf65fa5ea38feef8bc82de4965de1c783adf53ed05e461dc5de"} {"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1771952814,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","more work on branches"]],"content":"Signed commit: more work on branches","id":"adaaea7f2065a00cfd04c9de9bf82b1b976ac3d20c32389a8bd8aa7ad0a95677","sig":"71ce678d0a0732beab1f49f8318cbfe3d8b33d45eacf13392fdb9553e8b1f4732c28d8ffc33b50c9736a8324cf7604c223bb71ff4cfd32f41d7f3e81e1591fcc"} +{"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1771956701,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","implemented releases and code serach\nadd contributors to private repos\napply/merge buttons for patches and PRs\nhighlgihts and comments on patches and prs\nadded tagged downloads"]],"content":"Signed commit: implemented releases and code serach\nadd contributors to private repos\napply/merge buttons for patches and PRs\nhighlgihts and comments on patches and prs\nadded tagged downloads","id":"e822be2b0fbf3285bbedf9d8f9d1692b5503080af17a4d28941a1dc81c96187c","sig":"70c8b6e499551ce43478116cf694992102a29572d5380cbe3b070a3026bc2c9e35177587712c7414f25d1ca50038c9614479f7758bbdc48f69cc44cd52bf4842"} diff --git a/src/lib/components/NavBar.svelte b/src/lib/components/NavBar.svelte index 62b10b8..b0bb177 100644 --- a/src/lib/components/NavBar.svelte +++ b/src/lib/components/NavBar.svelte @@ -5,102 +5,189 @@ import { nip19 } from 'nostr-tools'; import SettingsButton from './SettingsButton.svelte'; import UserBadge from './UserBadge.svelte'; - import { onMount } from 'svelte'; + import { onMount, onDestroy } from 'svelte'; import { userStore } from '../stores/user-store.js'; import { clearActivity, updateActivity, isSessionExpired } from '../services/activity-tracker.js'; import { determineUserLevel, decodePubkey } from '../services/nostr/user-level-service.js'; let userPubkey = $state(null); let mobileMenuOpen = $state(false); + let nip07Available = $state(false); // Track NIP-07 availability (client-side only) + let isClient = $state(false); // Track if we're on the client + + // Component mount tracking to prevent state updates after destruction + let isMounted = $state(true); + + // Store cleanup references + let expiryCheckInterval: ReturnType | null = null; + let updateActivityOnInteraction: ((event: Event) => void) | null = null; // Sync with userStore changes $effect(() => { - const currentUser = $userStore; - if (currentUser.userPubkey && currentUser.userPubkeyHex) { - // Check if session expired - if (isSessionExpired()) { - userStore.reset(); + if (!isMounted || typeof window === 'undefined') return; + try { + const currentUser = $userStore; + if (!currentUser || !isMounted) return; + + if (currentUser.userPubkey && currentUser.userPubkeyHex && isMounted) { + // Check if session expired + if (isSessionExpired()) { + if (isMounted) { + userStore.reset(); + userPubkey = null; + } + } else if (isMounted) { + userPubkey = currentUser.userPubkey; + updateActivity(); + } + } else if (isMounted) { userPubkey = null; - } else { - userPubkey = currentUser.userPubkey; - updateActivity(); } - } else { - userPubkey = null; + } catch (err) { + // Ignore errors during destruction + if (isMounted) { + console.warn('User store sync error in NavBar:', err); + } } }); onMount(() => { + // Mark as client-side + isClient = true; + if (!isMounted) return; + + // Check NIP-07 availability (client-side only) + nip07Available = isNIP07Available(); + // User store already checks session expiry on initialization // Just restore state from store (which loads from localStorage) - const currentState = $userStore; - if (currentState.userPubkey && currentState.userPubkeyHex) { - // User is logged in - restore state (already synced by $effect, but ensure it's set) - userPubkey = currentState.userPubkey; - // Update activity to extend session - updateActivity(); - } else { - // User not logged in - check auth - checkAuth(); + try { + const currentState = $userStore; + if (currentState && currentState.userPubkey && currentState.userPubkeyHex && isMounted) { + // User is logged in - restore state (already synced by $effect, but ensure it's set) + userPubkey = currentState.userPubkey; + // Update activity to extend session + updateActivity(); + } else if (isMounted) { + // User not logged in - check auth + checkAuth(); + } + } catch (err) { + if (isMounted) { + console.warn('Failed to restore user state in NavBar:', err); + } } // Set up activity tracking for user interactions - const updateActivityOnInteraction = () => { - if (userPubkey) { + updateActivityOnInteraction = () => { + if (isMounted && userPubkey) { updateActivity(); } }; // Track various user interactions - document.addEventListener('click', updateActivityOnInteraction, { passive: true }); - document.addEventListener('keydown', updateActivityOnInteraction, { passive: true }); - document.addEventListener('scroll', updateActivityOnInteraction, { passive: true }); + if (updateActivityOnInteraction) { + document.addEventListener('click', updateActivityOnInteraction, { passive: true }); + document.addEventListener('keydown', updateActivityOnInteraction, { passive: true }); + document.addEventListener('scroll', updateActivityOnInteraction, { passive: true }); + } // Check session expiry periodically (every 5 minutes) - const expiryCheckInterval = setInterval(() => { - if (isSessionExpired()) { - // Session expired - logout user - userStore.reset(); - userPubkey = null; - clearInterval(expiryCheckInterval); + expiryCheckInterval = setInterval(() => { + if (!isMounted) { + if (expiryCheckInterval) { + clearInterval(expiryCheckInterval); + expiryCheckInterval = null; + } + return; + } + + try { + if (isSessionExpired()) { + // Session expired - logout user + if (isMounted) { + userStore.reset(); + userPubkey = null; + } + if (expiryCheckInterval) { + clearInterval(expiryCheckInterval); + expiryCheckInterval = null; + } + } + } catch (err) { + // Ignore errors during destruction + if (isMounted) { + console.warn('Session expiry check error:', err); + } } }, 5 * 60 * 1000); // Check every 5 minutes + }); + + onDestroy(() => { + // Mark component as unmounted first + isMounted = false; - return () => { - document.removeEventListener('click', updateActivityOnInteraction); - document.removeEventListener('keydown', updateActivityOnInteraction); - document.removeEventListener('scroll', updateActivityOnInteraction); - clearInterval(expiryCheckInterval); - }; + // Clean up event listeners + try { + if (updateActivityOnInteraction) { + document.removeEventListener('click', updateActivityOnInteraction); + document.removeEventListener('keydown', updateActivityOnInteraction); + document.removeEventListener('scroll', updateActivityOnInteraction); + updateActivityOnInteraction = null; + } + } catch (err) { + // Ignore errors during cleanup + } + + // Clean up interval + try { + if (expiryCheckInterval) { + clearInterval(expiryCheckInterval); + expiryCheckInterval = null; + } + } catch (err) { + // Ignore errors during cleanup + } }); function toggleMobileMenu() { + if (typeof window === 'undefined') return; mobileMenuOpen = !mobileMenuOpen; } function closeMobileMenu() { + if (typeof window === 'undefined') return; mobileMenuOpen = false; } async function checkAuth() { - // Don't check auth if user store indicates user is logged out - const currentState = $userStore; - if (!currentState.userPubkey) { - userPubkey = null; - return; - } + if (!isMounted || typeof window === 'undefined') return; + // Don't check auth if user store indicates user is logged out try { - if (isNIP07Available()) { + const currentState = $userStore; + if (!currentState || !currentState.userPubkey) { + if (isMounted) { + userPubkey = null; + } + return; + } + + if (isNIP07Available() && isMounted) { userPubkey = await getPublicKeyWithNIP07(); + } else if (isMounted) { + userPubkey = null; } } catch (err) { - console.log('NIP-07 not available or user not connected'); - userPubkey = null; + if (isMounted) { + console.log('NIP-07 not available or user not connected'); + userPubkey = null; + } } } async function login() { + if (typeof window === 'undefined' || !isMounted) return; if (!isNIP07Available()) { alert('Nostr extension not found. Please install a Nostr extension like nos2x or Alby to login.'); return; @@ -111,41 +198,53 @@ let pubkey: string; try { pubkey = await getPublicKeyWithNIP07(); - if (!pubkey) { + if (!pubkey || !isMounted) { throw new Error('No public key returned from extension'); } } catch (err) { - console.error('Failed to get public key from NIP-07:', err); - alert('Failed to connect to Nostr extension. Please make sure your extension is unlocked and try again.'); + if (isMounted) { + console.error('Failed to get public key from NIP-07:', err); + alert('Failed to connect to Nostr extension. Please make sure your extension is unlocked and try again.'); + } return; } + if (!isMounted) return; + // Convert npub to hex for API calls let pubkeyHex: string; if (/^[0-9a-f]{64}$/i.test(pubkey)) { // Already hex format pubkeyHex = pubkey.toLowerCase(); - userPubkey = pubkey; + if (isMounted) { + userPubkey = pubkey; + } } else { // Try to decode as npub try { const decoded = nip19.decode(pubkey); - if (decoded.type === 'npub') { + if (decoded.type === 'npub' && isMounted) { pubkeyHex = decoded.data as string; userPubkey = pubkey; // Keep original npub format } else { throw new Error('Invalid pubkey format'); } } catch (decodeErr) { - console.error('Failed to decode pubkey:', decodeErr); - alert('Invalid public key format. Please try again.'); + if (isMounted) { + console.error('Failed to decode pubkey:', decodeErr); + alert('Invalid public key format. Please try again.'); + } return; } } + if (!isMounted) return; + // Determine user level (checks relay write access) const levelResult = await determineUserLevel(userPubkey, pubkeyHex); + if (!isMounted) return; + // Update user store userStore.setUser( levelResult.userPubkey, @@ -155,19 +254,21 @@ ); // Update activity tracking on successful login - updateActivity(); + if (isMounted) { + updateActivity(); + } // Check for pending transfer events - if (levelResult.userPubkeyHex) { + if (levelResult.userPubkeyHex && isMounted) { try { const response = await fetch('/api/transfers/pending', { headers: { 'X-User-Pubkey': levelResult.userPubkeyHex } }); - if (response.ok) { + if (response.ok && isMounted) { const data = await response.json(); - if (data.pendingTransfers && data.pendingTransfers.length > 0) { + if (data.pendingTransfers && data.pendingTransfers.length > 0 && isMounted) { // Trigger a custom event to notify layout about pending transfers // The layout component will handle displaying the notifications window.dispatchEvent(new CustomEvent('pendingTransfers', { @@ -176,11 +277,15 @@ } } } catch (err) { - console.error('Failed to check for pending transfers:', err); + if (isMounted) { + console.error('Failed to check for pending transfers:', err); + } // Don't fail login if transfer check fails } } + if (!isMounted) return; + // Show success message const { hasUnlimitedAccess } = await import('../../lib/utils/user-access.js'); if (hasUnlimitedAccess(levelResult.level)) { @@ -189,28 +294,44 @@ console.log('Logged in with rate-limited access.'); } } catch (err) { - console.error('Login error:', err); - const errorMessage = err instanceof Error ? err.message : String(err); - alert(`Failed to login: ${errorMessage}. Please make sure your Nostr extension is unlocked and try again.`); + if (isMounted) { + console.error('Login error:', err); + const errorMessage = err instanceof Error ? err.message : String(err); + alert(`Failed to login: ${errorMessage}. Please make sure your Nostr extension is unlocked and try again.`); + } } } async function logout() { - userPubkey = null; - // Reset user store - userStore.reset(); - // Clear activity tracking - clearActivity(); + if (typeof window === 'undefined' || !isMounted) return; + if (isMounted) { + userPubkey = null; + // Reset user store + userStore.reset(); + // Clear activity tracking + clearActivity(); + } // Navigate to home page to reset all component state to anonymous // Use replace to prevent back button from going back to logged-in state - await goto('/', { replaceState: true, invalidateAll: true }); + if (isMounted) { + await goto('/', { replaceState: true, invalidateAll: true }); + } } function isActive(path: string): boolean { - return $page.url.pathname === path || $page.url.pathname.startsWith(path + '/'); + // Guard against SSR and component destruction + if (typeof window === 'undefined' || !isMounted) return false; + try { + const pageUrl = $page.url; + if (!pageUrl || !isMounted) return false; + return pageUrl.pathname === path || pageUrl.pathname.startsWith(path + '/'); + } catch { + return false; + } } +{#if typeof window !== 'undefined' || isClient} +{/if}