Browse Source

fix crash on download

Nostr-Signature: 3fdcc681cdda4b523f9c3752309b8cf740b58178ca02dcff4ef97ec714bf394c 573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc e405612a5aafeef66818f0a3c683e322f862d1fc3c662c32f618f516fd8c11ece5f4539b94893583301d31fd2ecd3de3b6d7a953505e2696915afe10710a16d7
main
Silberengel 3 weeks ago
parent
commit
a3f0678284
  1. 1
      nostr/commit-signatures.jsonl
  2. 180
      src/lib/components/NavBar.svelte
  3. 178
      src/routes/+layout.svelte
  4. 87
      src/routes/+page.svelte
  5. 356
      src/routes/repos/[npub]/[repo]/+page.svelte

1
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":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":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":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"}

180
src/lib/components/NavBar.svelte

@ -5,102 +5,189 @@
import { nip19 } from 'nostr-tools'; import { nip19 } from 'nostr-tools';
import SettingsButton from './SettingsButton.svelte'; import SettingsButton from './SettingsButton.svelte';
import UserBadge from './UserBadge.svelte'; import UserBadge from './UserBadge.svelte';
import { onMount } from 'svelte'; import { onMount, onDestroy } from 'svelte';
import { userStore } from '../stores/user-store.js'; import { userStore } from '../stores/user-store.js';
import { clearActivity, updateActivity, isSessionExpired } from '../services/activity-tracker.js'; import { clearActivity, updateActivity, isSessionExpired } from '../services/activity-tracker.js';
import { determineUserLevel, decodePubkey } from '../services/nostr/user-level-service.js'; import { determineUserLevel, decodePubkey } from '../services/nostr/user-level-service.js';
let userPubkey = $state<string | null>(null); let userPubkey = $state<string | null>(null);
let mobileMenuOpen = $state(false); 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<typeof setInterval> | null = null;
let updateActivityOnInteraction: ((event: Event) => void) | null = null;
// Sync with userStore changes // Sync with userStore changes
$effect(() => { $effect(() => {
if (!isMounted || typeof window === 'undefined') return;
try {
const currentUser = $userStore; const currentUser = $userStore;
if (currentUser.userPubkey && currentUser.userPubkeyHex) { if (!currentUser || !isMounted) return;
if (currentUser.userPubkey && currentUser.userPubkeyHex && isMounted) {
// Check if session expired // Check if session expired
if (isSessionExpired()) { if (isSessionExpired()) {
if (isMounted) {
userStore.reset(); userStore.reset();
userPubkey = null; userPubkey = null;
} else { }
} else if (isMounted) {
userPubkey = currentUser.userPubkey; userPubkey = currentUser.userPubkey;
updateActivity(); updateActivity();
} }
} else { } else if (isMounted) {
userPubkey = null; userPubkey = null;
} }
} catch (err) {
// Ignore errors during destruction
if (isMounted) {
console.warn('User store sync error in NavBar:', err);
}
}
}); });
onMount(() => { 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 // User store already checks session expiry on initialization
// Just restore state from store (which loads from localStorage) // Just restore state from store (which loads from localStorage)
try {
const currentState = $userStore; const currentState = $userStore;
if (currentState.userPubkey && currentState.userPubkeyHex) { if (currentState && currentState.userPubkey && currentState.userPubkeyHex && isMounted) {
// User is logged in - restore state (already synced by $effect, but ensure it's set) // User is logged in - restore state (already synced by $effect, but ensure it's set)
userPubkey = currentState.userPubkey; userPubkey = currentState.userPubkey;
// Update activity to extend session // Update activity to extend session
updateActivity(); updateActivity();
} else { } else if (isMounted) {
// User not logged in - check auth // User not logged in - check auth
checkAuth(); checkAuth();
} }
} catch (err) {
if (isMounted) {
console.warn('Failed to restore user state in NavBar:', err);
}
}
// Set up activity tracking for user interactions // Set up activity tracking for user interactions
const updateActivityOnInteraction = () => { updateActivityOnInteraction = () => {
if (userPubkey) { if (isMounted && userPubkey) {
updateActivity(); updateActivity();
} }
}; };
// Track various user interactions // Track various user interactions
if (updateActivityOnInteraction) {
document.addEventListener('click', updateActivityOnInteraction, { passive: true }); document.addEventListener('click', updateActivityOnInteraction, { passive: true });
document.addEventListener('keydown', updateActivityOnInteraction, { passive: true }); document.addEventListener('keydown', updateActivityOnInteraction, { passive: true });
document.addEventListener('scroll', updateActivityOnInteraction, { passive: true }); document.addEventListener('scroll', updateActivityOnInteraction, { passive: true });
}
// Check session expiry periodically (every 5 minutes) // Check session expiry periodically (every 5 minutes)
const expiryCheckInterval = setInterval(() => { expiryCheckInterval = setInterval(() => {
if (!isMounted) {
if (expiryCheckInterval) {
clearInterval(expiryCheckInterval);
expiryCheckInterval = null;
}
return;
}
try {
if (isSessionExpired()) { if (isSessionExpired()) {
// Session expired - logout user // Session expired - logout user
if (isMounted) {
userStore.reset(); userStore.reset();
userPubkey = null; userPubkey = null;
}
if (expiryCheckInterval) {
clearInterval(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 }, 5 * 60 * 1000); // Check every 5 minutes
});
onDestroy(() => {
// Mark component as unmounted first
isMounted = false;
return () => { // Clean up event listeners
try {
if (updateActivityOnInteraction) {
document.removeEventListener('click', updateActivityOnInteraction); document.removeEventListener('click', updateActivityOnInteraction);
document.removeEventListener('keydown', updateActivityOnInteraction); document.removeEventListener('keydown', updateActivityOnInteraction);
document.removeEventListener('scroll', updateActivityOnInteraction); document.removeEventListener('scroll', updateActivityOnInteraction);
updateActivityOnInteraction = null;
}
} catch (err) {
// Ignore errors during cleanup
}
// Clean up interval
try {
if (expiryCheckInterval) {
clearInterval(expiryCheckInterval); clearInterval(expiryCheckInterval);
}; expiryCheckInterval = null;
}
} catch (err) {
// Ignore errors during cleanup
}
}); });
function toggleMobileMenu() { function toggleMobileMenu() {
if (typeof window === 'undefined') return;
mobileMenuOpen = !mobileMenuOpen; mobileMenuOpen = !mobileMenuOpen;
} }
function closeMobileMenu() { function closeMobileMenu() {
if (typeof window === 'undefined') return;
mobileMenuOpen = false; mobileMenuOpen = false;
} }
async function checkAuth() { async function checkAuth() {
if (!isMounted || typeof window === 'undefined') return;
// Don't check auth if user store indicates user is logged out // Don't check auth if user store indicates user is logged out
try {
const currentState = $userStore; const currentState = $userStore;
if (!currentState.userPubkey) { if (!currentState || !currentState.userPubkey) {
if (isMounted) {
userPubkey = null; userPubkey = null;
}
return; return;
} }
try { if (isNIP07Available() && isMounted) {
if (isNIP07Available()) {
userPubkey = await getPublicKeyWithNIP07(); userPubkey = await getPublicKeyWithNIP07();
} else if (isMounted) {
userPubkey = null;
} }
} catch (err) { } catch (err) {
if (isMounted) {
console.log('NIP-07 not available or user not connected'); console.log('NIP-07 not available or user not connected');
userPubkey = null; userPubkey = null;
} }
} }
}
async function login() { async function login() {
if (typeof window === 'undefined' || !isMounted) return;
if (!isNIP07Available()) { if (!isNIP07Available()) {
alert('Nostr extension not found. Please install a Nostr extension like nos2x or Alby to login.'); alert('Nostr extension not found. Please install a Nostr extension like nos2x or Alby to login.');
return; return;
@ -111,41 +198,53 @@
let pubkey: string; let pubkey: string;
try { try {
pubkey = await getPublicKeyWithNIP07(); pubkey = await getPublicKeyWithNIP07();
if (!pubkey) { if (!pubkey || !isMounted) {
throw new Error('No public key returned from extension'); throw new Error('No public key returned from extension');
} }
} catch (err) { } catch (err) {
if (isMounted) {
console.error('Failed to get public key from NIP-07:', 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.'); alert('Failed to connect to Nostr extension. Please make sure your extension is unlocked and try again.');
}
return; return;
} }
if (!isMounted) return;
// Convert npub to hex for API calls // Convert npub to hex for API calls
let pubkeyHex: string; let pubkeyHex: string;
if (/^[0-9a-f]{64}$/i.test(pubkey)) { if (/^[0-9a-f]{64}$/i.test(pubkey)) {
// Already hex format // Already hex format
pubkeyHex = pubkey.toLowerCase(); pubkeyHex = pubkey.toLowerCase();
if (isMounted) {
userPubkey = pubkey; userPubkey = pubkey;
}
} else { } else {
// Try to decode as npub // Try to decode as npub
try { try {
const decoded = nip19.decode(pubkey); const decoded = nip19.decode(pubkey);
if (decoded.type === 'npub') { if (decoded.type === 'npub' && isMounted) {
pubkeyHex = decoded.data as string; pubkeyHex = decoded.data as string;
userPubkey = pubkey; // Keep original npub format userPubkey = pubkey; // Keep original npub format
} else { } else {
throw new Error('Invalid pubkey format'); throw new Error('Invalid pubkey format');
} }
} catch (decodeErr) { } catch (decodeErr) {
if (isMounted) {
console.error('Failed to decode pubkey:', decodeErr); console.error('Failed to decode pubkey:', decodeErr);
alert('Invalid public key format. Please try again.'); alert('Invalid public key format. Please try again.');
}
return; return;
} }
} }
if (!isMounted) return;
// Determine user level (checks relay write access) // Determine user level (checks relay write access)
const levelResult = await determineUserLevel(userPubkey, pubkeyHex); const levelResult = await determineUserLevel(userPubkey, pubkeyHex);
if (!isMounted) return;
// Update user store // Update user store
userStore.setUser( userStore.setUser(
levelResult.userPubkey, levelResult.userPubkey,
@ -155,19 +254,21 @@
); );
// Update activity tracking on successful login // Update activity tracking on successful login
if (isMounted) {
updateActivity(); updateActivity();
}
// Check for pending transfer events // Check for pending transfer events
if (levelResult.userPubkeyHex) { if (levelResult.userPubkeyHex && isMounted) {
try { try {
const response = await fetch('/api/transfers/pending', { const response = await fetch('/api/transfers/pending', {
headers: { headers: {
'X-User-Pubkey': levelResult.userPubkeyHex 'X-User-Pubkey': levelResult.userPubkeyHex
} }
}); });
if (response.ok) { if (response.ok && isMounted) {
const data = await response.json(); 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 // Trigger a custom event to notify layout about pending transfers
// The layout component will handle displaying the notifications // The layout component will handle displaying the notifications
window.dispatchEvent(new CustomEvent('pendingTransfers', { window.dispatchEvent(new CustomEvent('pendingTransfers', {
@ -176,11 +277,15 @@
} }
} }
} catch (err) { } catch (err) {
if (isMounted) {
console.error('Failed to check for pending transfers:', err); console.error('Failed to check for pending transfers:', err);
}
// Don't fail login if transfer check fails // Don't fail login if transfer check fails
} }
} }
if (!isMounted) return;
// Show success message // Show success message
const { hasUnlimitedAccess } = await import('../../lib/utils/user-access.js'); const { hasUnlimitedAccess } = await import('../../lib/utils/user-access.js');
if (hasUnlimitedAccess(levelResult.level)) { if (hasUnlimitedAccess(levelResult.level)) {
@ -189,28 +294,44 @@
console.log('Logged in with rate-limited access.'); console.log('Logged in with rate-limited access.');
} }
} catch (err) { } catch (err) {
if (isMounted) {
console.error('Login error:', err); console.error('Login error:', err);
const errorMessage = err instanceof Error ? err.message : String(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.`); alert(`Failed to login: ${errorMessage}. Please make sure your Nostr extension is unlocked and try again.`);
} }
} }
}
async function logout() { async function logout() {
if (typeof window === 'undefined' || !isMounted) return;
if (isMounted) {
userPubkey = null; userPubkey = null;
// Reset user store // Reset user store
userStore.reset(); userStore.reset();
// Clear activity tracking // Clear activity tracking
clearActivity(); clearActivity();
}
// Navigate to home page to reset all component state to anonymous // Navigate to home page to reset all component state to anonymous
// Use replace to prevent back button from going back to logged-in state // Use replace to prevent back button from going back to logged-in state
if (isMounted) {
await goto('/', { replaceState: true, invalidateAll: true }); await goto('/', { replaceState: true, invalidateAll: true });
} }
}
function isActive(path: string): boolean { 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;
}
} }
</script> </script>
{#if typeof window !== 'undefined' || isClient}
<header class="site-header"> <header class="site-header">
<div class="header-container"> <div class="header-container">
<a href="/" class="header-logo"> <a href="/" class="header-logo">
@ -219,29 +340,30 @@
</a> </a>
<nav class:mobile-open={mobileMenuOpen}> <nav class:mobile-open={mobileMenuOpen}>
<div class="nav-links"> <div class="nav-links">
<a href="/repos" class:active={isActive('/repos')} onclick={closeMobileMenu}>Repositories</a> <a href="/repos" class:active={isActive('/repos')} onclick={() => closeMobileMenu()}>Repositories</a>
<a href="/search" class:active={isActive('/search')} onclick={closeMobileMenu}>Search</a> <a href="/search" class:active={isActive('/search')} onclick={() => closeMobileMenu()}>Search</a>
<a href="/signup" class:active={isActive('/signup')} onclick={closeMobileMenu}>Register</a> <a href="/signup" class:active={isActive('/signup')} onclick={() => closeMobileMenu()}>Register</a>
<a href="/docs" class:active={isActive('/docs')} onclick={closeMobileMenu}>Docs</a> <a href="/docs" class:active={isActive('/docs')} onclick={() => closeMobileMenu()}>Docs</a>
<a href="/api-docs" class:active={isActive('/api-docs')} onclick={closeMobileMenu}>API Docs</a> <a href="/api-docs" class:active={isActive('/api-docs')} onclick={() => closeMobileMenu()}>API Docs</a>
</div> </div>
</nav> </nav>
<div class="auth-section"> <div class="auth-section">
<SettingsButton /> <SettingsButton />
{#if userPubkey} {#if userPubkey}
<UserBadge pubkey={userPubkey} /> <UserBadge pubkey={userPubkey} />
<button onclick={logout} class="logout-button">Logout</button> <button onclick={(e) => { e.preventDefault(); logout(); }} class="logout-button">Logout</button>
{:else} {:else}
<button onclick={login} class="login-button" disabled={!isNIP07Available()}> <button onclick={(e) => { e.preventDefault(); login(); }} class="login-button" disabled={!nip07Available}>
{isNIP07Available() ? 'Login' : 'NIP-07 Not Available'} {nip07Available ? 'Login' : 'NIP-07 Not Available'}
</button> </button>
{/if} {/if}
<button class="mobile-menu-toggle" onclick={toggleMobileMenu} aria-label="Toggle menu"> <button class="mobile-menu-toggle" onclick={() => toggleMobileMenu()} aria-label="Toggle menu">
<img src="/icons/menu.svg" alt="Menu" class="hamburger-icon" /> <img src="/icons/menu.svg" alt="Menu" class="hamburger-icon" />
</button> </button>
</div> </div>
</div> </div>
</header> </header>
{/if}
<style> <style>
.site-header { .site-header {

178
src/routes/+layout.svelte

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import '../app.css'; import '../app.css';
import { onMount, setContext } from 'svelte'; import { onMount, onDestroy, setContext } from 'svelte';
import { page } from '$app/stores'; import { page } from '$app/stores';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import Footer from '$lib/components/Footer.svelte'; import Footer from '$lib/components/Footer.svelte';
@ -16,6 +16,13 @@
// Accept children as a snippet prop (Svelte 5) // Accept children as a snippet prop (Svelte 5)
let { children }: { children: Snippet } = $props(); let { children }: { children: Snippet } = $props();
// Component mount tracking to prevent state updates after destruction
let isMounted = $state(true);
// Store cleanup references
let handlePendingTransfersEvent: ((event: Event) => void) | null = null;
let handleThemeChanged: ((event: Event) => void) | null = null;
// Theme management - default to gitrepublic-dark (purple) // Theme management - default to gitrepublic-dark (purple)
let theme = $state<'gitrepublic-light' | 'gitrepublic-dark' | 'gitrepublic-black'>('gitrepublic-dark'); let theme = $state<'gitrepublic-light' | 'gitrepublic-dark' | 'gitrepublic-black'>('gitrepublic-dark');
@ -40,97 +47,166 @@
// Load theme on mount and watch for changes // Load theme on mount and watch for changes
onMount(() => { onMount(() => {
// Only run client-side code // Only run client-side code
if (typeof window === 'undefined') return; if (typeof window === 'undefined' || !isMounted) return;
// Load theme from settings store (async) // Load theme from settings store (async)
(async () => { (async () => {
if (!isMounted) return;
try { try {
const settings = await settingsStore.getSettings(); const settings = await settingsStore.getSettings();
if (isMounted) {
theme = settings.theme; theme = settings.theme;
themeLoaded = true; themeLoaded = true;
applyTheme(theme); applyTheme(theme);
// Also sync to localStorage for app.html flash prevention // Also sync to localStorage for app.html flash prevention
localStorage.setItem('theme', theme); localStorage.setItem('theme', theme);
}
} catch (err) { } catch (err) {
if (!isMounted) return;
console.warn('Failed to load theme from settings, using default:', err); console.warn('Failed to load theme from settings, using default:', err);
// Fallback to localStorage for migration // Fallback to localStorage for migration
try {
const savedTheme = localStorage.getItem('theme') as 'gitrepublic-light' | 'gitrepublic-dark' | 'gitrepublic-black' | null; const savedTheme = localStorage.getItem('theme') as 'gitrepublic-light' | 'gitrepublic-dark' | 'gitrepublic-black' | null;
if (savedTheme === 'gitrepublic-light' || savedTheme === 'gitrepublic-dark' || savedTheme === 'gitrepublic-black') { if (savedTheme === 'gitrepublic-light' || savedTheme === 'gitrepublic-dark' || savedTheme === 'gitrepublic-black') {
if (isMounted) {
theme = savedTheme; theme = savedTheme;
themeLoaded = true; themeLoaded = true;
applyTheme(theme); applyTheme(theme);
// Migrate to settings store // Migrate to settings store
settingsStore.setSetting('theme', theme).catch(console.error); settingsStore.setSetting('theme', theme).catch(console.error);
} else { }
} else if (isMounted) {
theme = 'gitrepublic-dark'; theme = 'gitrepublic-dark';
themeLoaded = true; themeLoaded = true;
applyTheme(theme); applyTheme(theme);
localStorage.setItem('theme', theme); localStorage.setItem('theme', theme);
} }
} catch {
// Ignore localStorage errors
}
} }
})(); })();
// Update activity on mount (if user is logged in) // Update activity on mount (if user is logged in)
// Session expiry is handled by user store initialization and NavBar // Session expiry is handled by user store initialization and NavBar
try {
const currentState = $userStore; const currentState = $userStore;
if (currentState.userPubkey && currentState.userPubkeyHex) { if (currentState && currentState.userPubkey && currentState.userPubkeyHex && isMounted) {
updateActivity(); updateActivity();
} }
} catch (err) {
if (isMounted) {
console.warn('Failed to update activity on mount:', err);
}
}
// Check user level if not on splash page // Check user level if not on splash page
// Only check if user store is not already initialized with a logged-in user // Only check if user store is not already initialized with a logged-in user
if ($page.url.pathname !== '/') { // Guard against SSR - $page store can only be accessed in component context
if (typeof window !== 'undefined' && isMounted) {
try {
const pageUrl = $page.url;
if (pageUrl && pageUrl.pathname !== '/') {
const currentState = $userStore; const currentState = $userStore;
// Only check if we don't have a user or if user level is strictly_rate_limited // Only check if we don't have a user or if user level is strictly_rate_limited
if (!currentState.userPubkey || currentState.userLevel === 'strictly_rate_limited') { if (isMounted && (!currentState.userPubkey || currentState.userLevel === 'strictly_rate_limited')) {
checkUserLevel(); checkUserLevel();
} }
} }
} catch (err) {
// Ignore errors accessing $page during SSR or destruction
if (isMounted) {
console.warn('Failed to check user level on mount:', err);
}
}
}
// Listen for pending transfers events from login functions // Listen for pending transfers events from login functions
const handlePendingTransfersEvent = (event: Event) => { handlePendingTransfersEvent = (event: Event) => {
if (!isMounted) return;
try {
const customEvent = event as CustomEvent; const customEvent = event as CustomEvent;
if (customEvent.detail?.transfers) { if (customEvent.detail?.transfers && isMounted) {
// Filter out dismissed transfers // Filter out dismissed transfers
pendingTransfers = customEvent.detail.transfers.filter( pendingTransfers = customEvent.detail.transfers.filter(
(t: { eventId: string }) => !dismissedTransfers.has(t.eventId) (t: { eventId: string }) => !dismissedTransfers.has(t.eventId)
); );
} }
} catch (err) {
// Ignore errors during destruction
if (isMounted) {
console.warn('Pending transfers event handler error:', err);
}
}
}; };
if (handlePendingTransfersEvent) {
window.addEventListener('pendingTransfers', handlePendingTransfersEvent); window.addEventListener('pendingTransfers', handlePendingTransfersEvent);
}
// Listen for theme changes from SettingsModal // Listen for theme changes from SettingsModal
const handleThemeChanged = (event: Event) => { handleThemeChanged = (event: Event) => {
if (!isMounted) return;
try {
const customEvent = event as CustomEvent<{ theme: 'gitrepublic-light' | 'gitrepublic-dark' | 'gitrepublic-black' }>; const customEvent = event as CustomEvent<{ theme: 'gitrepublic-light' | 'gitrepublic-dark' | 'gitrepublic-black' }>;
if (customEvent.detail?.theme) { if (customEvent.detail?.theme && isMounted) {
theme = customEvent.detail.theme; theme = customEvent.detail.theme;
// Sync to localStorage for app.html flash prevention // Sync to localStorage for app.html flash prevention
localStorage.setItem('theme', theme); localStorage.setItem('theme', theme);
// Theme will be applied via $effect // Theme will be applied via $effect
} }
} catch (err) {
// Ignore errors during destruction
if (isMounted) {
console.warn('Theme changed event handler error:', err);
}
}
}; };
if (handleThemeChanged) {
window.addEventListener('themeChanged', handleThemeChanged); window.addEventListener('themeChanged', handleThemeChanged);
}
// Session expiry checking is handled by: // Session expiry checking is handled by:
// 1. User store initialization (checks on load) // 1. User store initialization (checks on load)
// 2. NavBar component (checks on mount and periodically) // 2. NavBar component (checks on mount and periodically)
// 3. Splash page (+page.svelte) (checks on mount) // 3. Splash page (+page.svelte) (checks on mount)
// No need for redundant checks here // No need for redundant checks here
});
// Return cleanup function onDestroy(() => {
return () => { // Mark component as unmounted first
isMounted = false;
// Clean up event listeners
try {
if (handlePendingTransfersEvent) {
window.removeEventListener('pendingTransfers', handlePendingTransfersEvent); window.removeEventListener('pendingTransfers', handlePendingTransfersEvent);
handlePendingTransfersEvent = null;
}
} catch (err) {
// Ignore errors during cleanup
}
try {
if (handleThemeChanged) {
window.removeEventListener('themeChanged', handleThemeChanged); window.removeEventListener('themeChanged', handleThemeChanged);
}; handleThemeChanged = null;
}
} catch (err) {
// Ignore errors during cleanup
}
}); });
async function checkUserLevel() { async function checkUserLevel() {
// Only run client-side // Only run client-side
if (typeof window === 'undefined') return; if (typeof window === 'undefined' || !isMounted) return;
// Skip if already checking or if user store is already set // Skip if already checking or if user store is already set
try {
const currentState = $userStore; const currentState = $userStore;
if (!currentState || !isMounted) return;
if (checkingUserLevel || (currentState.userPubkey && currentState.userLevel !== 'strictly_rate_limited')) { if (checkingUserLevel || (currentState.userPubkey && currentState.userLevel !== 'strictly_rate_limited')) {
return; return;
} }
@ -139,10 +215,14 @@
// Don't automatically get pubkey from NIP-07 - that should only happen on explicit login // Don't automatically get pubkey from NIP-07 - that should only happen on explicit login
if (!currentState.userPubkey) { if (!currentState.userPubkey) {
// User not logged in - set to strictly rate limited without checking // User not logged in - set to strictly rate limited without checking
if (isMounted) {
userStore.setUser(null, null, 'strictly_rate_limited', null); userStore.setUser(null, null, 'strictly_rate_limited', null);
}
return; return;
} }
if (!isMounted) return;
checkingUserLevel = true; checkingUserLevel = true;
userStore.setChecking(true); userStore.setChecking(true);
@ -151,9 +231,21 @@
const userPubkey = currentState.userPubkey; const userPubkey = currentState.userPubkey;
const userPubkeyHex = currentState.userPubkeyHex; const userPubkeyHex = currentState.userPubkeyHex;
if (!isMounted) {
checkingUserLevel = false;
userStore.setChecking(false);
return;
}
// Determine user level // Determine user level
const levelResult = await determineUserLevel(userPubkey, userPubkeyHex); const levelResult = await determineUserLevel(userPubkey, userPubkeyHex);
if (!isMounted) {
checkingUserLevel = false;
userStore.setChecking(false);
return;
}
// Update user store // Update user store
userStore.setUser( userStore.setUser(
levelResult.userPubkey, levelResult.userPubkey,
@ -163,22 +255,34 @@
); );
// Update activity if user is logged in // Update activity if user is logged in
if (levelResult.userPubkey && levelResult.userPubkeyHex) { if (levelResult.userPubkey && levelResult.userPubkeyHex && isMounted) {
updateActivity(); updateActivity();
// Check for pending transfers // Check for pending transfers
checkPendingTransfers(levelResult.userPubkeyHex); checkPendingTransfers(levelResult.userPubkeyHex);
} }
} catch (err) { } catch (err) {
if (isMounted) {
console.error('Failed to check user level:', err); console.error('Failed to check user level:', err);
// Set to strictly rate limited on error // Set to strictly rate limited on error
userStore.setUser(null, null, 'strictly_rate_limited', 'Failed to check user level'); userStore.setUser(null, null, 'strictly_rate_limited', 'Failed to check user level');
}
} finally { } finally {
if (isMounted) {
checkingUserLevel = false; checkingUserLevel = false;
userStore.setChecking(false); userStore.setChecking(false);
} }
} }
} catch (err) {
// Ignore errors during destruction
if (isMounted) {
console.warn('User level check error:', err);
}
}
}
async function checkPendingTransfers(userPubkeyHex: string) { async function checkPendingTransfers(userPubkeyHex: string) {
if (!isMounted) return;
try { try {
// Add timeout to prevent hanging // Add timeout to prevent hanging
const controller = new AbortController(); const controller = new AbortController();
@ -193,9 +297,9 @@
clearTimeout(timeoutId); clearTimeout(timeoutId);
if (response.ok) { if (response.ok && isMounted) {
const data = await response.json(); const data = await response.json();
if (data.pendingTransfers && data.pendingTransfers.length > 0) { if (data.pendingTransfers && data.pendingTransfers.length > 0 && isMounted) {
// Filter out dismissed transfers // Filter out dismissed transfers
pendingTransfers = data.pendingTransfers.filter( pendingTransfers = data.pendingTransfers.filter(
(t: { eventId: string }) => !dismissedTransfers.has(t.eventId) (t: { eventId: string }) => !dismissedTransfers.has(t.eventId)
@ -203,8 +307,8 @@
} }
} }
} catch (err) { } catch (err) {
// Only log if it's not an abort (timeout) // Only log if it's not an abort (timeout) and component is still mounted
if (err instanceof Error && err.name !== 'AbortError') { if (isMounted && err instanceof Error && err.name !== 'AbortError') {
console.error('Failed to check for pending transfers:', err); console.error('Failed to check for pending transfers:', err);
} }
// Silently ignore timeouts - they're expected if the server is slow // Silently ignore timeouts - they're expected if the server is slow
@ -244,7 +348,7 @@
// Watch for theme changes and apply them (but only after initial load) // Watch for theme changes and apply them (but only after initial load)
let themeLoaded = $state(false); let themeLoaded = $state(false);
$effect(() => { $effect(() => {
if (typeof window !== 'undefined' && themeLoaded) { if (typeof window !== 'undefined' && themeLoaded && isMounted) {
applyTheme(theme); applyTheme(theme);
} }
}); });
@ -278,21 +382,49 @@
} }
// Hide nav bar and footer on splash page (root path) // Hide nav bar and footer on splash page (root path)
const isSplashPage = $derived($page.url.pathname === '/'); // Use state that gets updated on mount to avoid SSR issues with $page store
let isSplashPage = $state(false);
// Update splash page state on mount (client-side only)
$effect(() => {
if (typeof window !== 'undefined' && isMounted) {
try {
const pageUrl = $page.url;
if (pageUrl && isMounted) {
isSplashPage = pageUrl.pathname === '/';
}
} catch (err) {
// Ignore errors accessing $page during SSR or destruction
if (isMounted) {
console.warn('Failed to check splash page state:', err);
}
}
}
});
// Subscribe to user store // Subscribe to user store
const userState = $derived($userStore); const userState = $derived($userStore);
// Check for transfers when user logs in // Check for transfers when user logs in
$effect(() => { $effect(() => {
if (!isMounted || typeof window === 'undefined') return;
try {
const currentUser = $userStore; const currentUser = $userStore;
if (currentUser.userPubkeyHex && !checkingUserLevel) { if (!currentUser || !isMounted) return;
if (currentUser.userPubkeyHex && !checkingUserLevel && isMounted) {
checkPendingTransfers(currentUser.userPubkeyHex); checkPendingTransfers(currentUser.userPubkeyHex);
} else if (!currentUser.userPubkeyHex) { } else if (!currentUser.userPubkeyHex && isMounted) {
// Clear transfers when user logs out // Clear transfers when user logs out
pendingTransfers = []; pendingTransfers = [];
dismissedTransfers.clear(); dismissedTransfers.clear();
} }
} catch (err) {
// Ignore errors during destruction
if (isMounted) {
console.warn('Transfer check effect error:', err);
}
}
}); });
</script> </script>

87
src/routes/+page.svelte

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte'; import { onMount, onDestroy } from 'svelte';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { page } from '$app/stores'; import { page } from '$app/stores';
import { getPublicKeyWithNIP07, isNIP07Available } from '../lib/services/nostr/nip07-signer.js'; import { getPublicKeyWithNIP07, isNIP07Available } from '../lib/services/nostr/nip07-signer.js';
@ -14,34 +14,58 @@
let checkingLevel = $state(false); let checkingLevel = $state(false);
let levelMessage = $state<string | null>(null); let levelMessage = $state<string | null>(null);
// Component mount tracking to prevent state updates after destruction
let isMounted = $state(true);
// React to userStore changes (e.g., when user logs in or out) // React to userStore changes (e.g., when user logs in or out)
$effect(() => { $effect(() => {
if (!isMounted || typeof window === 'undefined') return;
try {
const currentUser = $userStore; const currentUser = $userStore;
if (currentUser.userPubkey && currentUser.userPubkeyHex) { if (!currentUser || !isMounted) return;
if (currentUser.userPubkey && currentUser.userPubkeyHex && isMounted) {
// User is logged in - sync local state with store // User is logged in - sync local state with store
userPubkey = currentUser.userPubkey; userPubkey = currentUser.userPubkey;
userPubkeyHex = currentUser.userPubkeyHex; userPubkeyHex = currentUser.userPubkeyHex;
} else { } else if (isMounted) {
// User has logged out - clear local state // User has logged out - clear local state
userPubkey = null; userPubkey = null;
userPubkeyHex = null; userPubkeyHex = null;
} }
} catch (err) {
// Ignore errors during destruction
if (isMounted) {
console.warn('User store sync error in splash page:', err);
}
}
}); });
onMount(() => { onMount(() => {
if (typeof window === 'undefined' || !isMounted) return;
// Prevent body scroll when splash page is shown // Prevent body scroll when splash page is shown
try {
document.body.style.overflow = 'hidden'; document.body.style.overflow = 'hidden';
} catch (err) {
if (isMounted) {
console.warn('Failed to set body overflow:', err);
}
}
// Check for session expiry first // Check for session expiry first
try {
if (isSessionExpired()) { if (isSessionExpired()) {
// Session expired - logout user // Session expired - logout user
if (isMounted) {
userStore.reset(); userStore.reset();
checkingAuth = true; checkingAuth = true;
checkAuth(); checkAuth();
} else { }
} else if (isMounted) {
// Check userStore first - if user is already logged in, use store values // Check userStore first - if user is already logged in, use store values
const currentUser = $userStore; const currentUser = $userStore;
if (currentUser.userPubkey && currentUser.userPubkeyHex) { if (currentUser && currentUser.userPubkey && currentUser.userPubkeyHex) {
// User is already logged in - use store values // User is already logged in - use store values
userPubkey = currentUser.userPubkey; userPubkey = currentUser.userPubkey;
userPubkeyHex = currentUser.userPubkeyHex; userPubkeyHex = currentUser.userPubkeyHex;
@ -50,41 +74,67 @@
updateActivity(); updateActivity();
// Don't redirect immediately - let the user see they're logged in // Don't redirect immediately - let the user see they're logged in
// They can click "View Repositories" or navigate away // They can click "View Repositories" or navigate away
} else { } else if (isMounted) {
// User not logged in - check if extension is available // User not logged in - check if extension is available
checkingAuth = true; checkingAuth = true;
checkAuth(); checkAuth();
} }
} }
} catch (err) {
if (isMounted) {
console.warn('Failed to check auth on mount:', err);
}
}
});
onDestroy(() => {
// Mark component as unmounted first
isMounted = false;
// Return cleanup function
return () => {
// Re-enable scrolling when component is destroyed // Re-enable scrolling when component is destroyed
try {
if (typeof document !== 'undefined' && document.body) {
document.body.style.overflow = ''; document.body.style.overflow = '';
}; }
} catch (err) {
// Ignore errors during cleanup
}
}); });
async function checkAuth() { async function checkAuth() {
if (!isMounted || typeof window === 'undefined') return;
if (isMounted) {
checkingAuth = true; checkingAuth = true;
}
// Check userStore first - if user has logged out, clear state // Check userStore first - if user has logged out, clear state
try {
const currentUser = $userStore; const currentUser = $userStore;
if (!currentUser.userPubkey) { if (!currentUser || !currentUser.userPubkey) {
if (isMounted) {
userPubkey = null; userPubkey = null;
userPubkeyHex = null; userPubkeyHex = null;
checkingAuth = false; checkingAuth = false;
}
return; return;
} }
if (!isMounted) return;
if (isNIP07Available()) { if (isNIP07Available()) {
try { try {
userPubkey = await getPublicKeyWithNIP07(); userPubkey = await getPublicKeyWithNIP07();
if (!isMounted) return;
// Convert npub to hex for API calls // Convert npub to hex for API calls
// NIP-07 may return either npub or hex // NIP-07 may return either npub or hex
if (/^[0-9a-f]{64}$/i.test(userPubkey)) { if (/^[0-9a-f]{64}$/i.test(userPubkey)) {
// Already hex format // Already hex format
if (isMounted) {
userPubkeyHex = userPubkey.toLowerCase(); userPubkeyHex = userPubkey.toLowerCase();
} else { }
} else if (isMounted) {
// Try to decode as npub // Try to decode as npub
try { try {
const decoded = nip19.decode(userPubkey); const decoded = nip19.decode(userPubkey);
@ -94,21 +144,34 @@
userPubkeyHex = userPubkey; // Unknown type, use as-is userPubkeyHex = userPubkey; // Unknown type, use as-is
} }
} catch { } catch {
if (isMounted) {
userPubkeyHex = userPubkey; // Assume it's already hex or use as-is userPubkeyHex = userPubkey; // Assume it's already hex or use as-is
} }
} }
}
} catch (err) { } catch (err) {
if (isMounted) {
console.warn('Failed to load user pubkey:', err); console.warn('Failed to load user pubkey:', err);
userPubkey = null; userPubkey = null;
userPubkeyHex = null; userPubkeyHex = null;
} }
} else { }
} else if (isMounted) {
// Extension not available, clear state // Extension not available, clear state
userPubkey = null; userPubkey = null;
userPubkeyHex = null; userPubkeyHex = null;
} }
if (isMounted) {
checkingAuth = false;
}
} catch (err) {
if (isMounted) {
console.warn('Auth check error:', err);
checkingAuth = false; checkingAuth = false;
} }
}
}
async function handleLogin() { async function handleLogin() {
if (!isNIP07Available()) { if (!isNIP07Available()) {

356
src/routes/repos/[npub]/[repo]/+page.svelte

@ -24,8 +24,9 @@
import { hasUnlimitedAccess } from '$lib/utils/user-access.js'; import { hasUnlimitedAccess } from '$lib/utils/user-access.js';
import { fetchUserEmail, fetchUserName } from '$lib/utils/user-profile.js'; import { fetchUserEmail, fetchUserName } from '$lib/utils/user-profile.js';
// Get page data for OpenGraph metadata - use $derived to make it reactive // Get page data for OpenGraph metadata - use state to avoid SSR issues
const pageData = $derived($page.data as { // Guard against SSR - $page store can only be accessed in component context
let pageData = $state<{
title?: string; title?: string;
description?: string; description?: string;
image?: string; image?: string;
@ -33,10 +34,54 @@
repoUrl?: string; repoUrl?: string;
announcement?: NostrEvent; announcement?: NostrEvent;
gitDomain?: string; gitDomain?: string;
}>({});
// Update pageData from $page when available (client-side)
$effect(() => {
if (typeof window === 'undefined' || !isMounted) return;
try {
const data = $page.data as typeof pageData;
if (data && isMounted) {
pageData = data || {};
}
} catch (err) {
// Ignore SSR errors and errors during destruction
if (isMounted) {
console.warn('Failed to update pageData:', err);
}
}
}); });
const npub = ($page.params as { npub?: string; repo?: string }).npub || ''; // Guard params access during SSR - use state that gets updated reactively
const repo = ($page.params as { npub?: string; repo?: string }).repo || ''; // Params come from the route, so we can parse from URL or get from $page.params on client
let npub = $state('');
let repo = $state('');
// Update params from $page when available (client-side)
$effect(() => {
if (typeof window === 'undefined' || !isMounted) return;
try {
const params = $page.params as { npub?: string; repo?: string };
if (params && isMounted) {
if (params.npub && params.npub !== npub) npub = params.npub;
if (params.repo && params.repo !== repo) repo = params.repo;
}
} catch {
// If $page.params fails, try to parse from URL path
if (!isMounted) return;
try {
if (typeof window !== 'undefined') {
const pathParts = window.location.pathname.split('/').filter(Boolean);
if (pathParts[0] === 'repos' && pathParts[1] && pathParts[2] && isMounted) {
npub = pathParts[1];
repo = pathParts[2];
}
}
} catch {
// Ignore errors - params will be set eventually
}
}
});
// Extract fields from announcement for convenience // Extract fields from announcement for convenience
const repoAnnouncement = $derived(pageData.announcement); const repoAnnouncement = $derived(pageData.announcement);
@ -84,6 +129,26 @@
// Tabs will be defined as derived after issues and prs are declared // Tabs will be defined as derived after issues and prs are declared
// Component mount tracking to prevent state updates after destruction
let isMounted = $state(true);
// Helper function to safely update state only if component is still mounted
function safeStateUpdate<T>(updateFn: () => T): T | null {
if (!isMounted) return null;
try {
return updateFn();
} catch (err) {
// Silently ignore errors during destruction
if (isMounted) {
console.warn('State update error (component may be destroying):', err);
}
return null;
}
}
// Store event listener handler for cleanup
let clickOutsideHandler: ((event: MouseEvent) => void) | null = null;
// Auto-save // Auto-save
let autoSaveInterval: ReturnType<typeof setInterval> | null = null; let autoSaveInterval: ReturnType<typeof setInterval> | null = null;
@ -92,11 +157,16 @@
let maintainersEffectRan = $state(false); let maintainersEffectRan = $state(false);
$effect(() => { $effect(() => {
// Guard against SSR and component destruction
if (typeof window === 'undefined' || !isMounted) return;
try {
const data = $page.data as typeof pageData; const data = $page.data as typeof pageData;
if (!data || !isMounted) return;
const currentRepoKey = `${npub}/${repo}`; const currentRepoKey = `${npub}/${repo}`;
// Reset flags if repo changed // Reset flags if repo changed
if (currentRepoKey !== lastRepoKey) { if (currentRepoKey !== lastRepoKey && isMounted) {
maintainersLoaded = false; maintainersLoaded = false;
maintainersEffectRan = false; maintainersEffectRan = false;
lastRepoKey = currentRepoKey; lastRepoKey = currentRepoKey;
@ -106,23 +176,34 @@
// 1. We have page data // 1. We have page data
// 2. Effect hasn't run yet for this repo // 2. Effect hasn't run yet for this repo
// 3. We're not currently loading // 3. We're not currently loading
if ((repoOwnerPubkeyDerived || (repoMaintainers && repoMaintainers.length > 0)) && // 4. Component is still mounted
if (isMounted &&
(repoOwnerPubkeyDerived || (repoMaintainers && repoMaintainers.length > 0)) &&
!maintainersEffectRan && !maintainersEffectRan &&
!loadingMaintainers) { !loadingMaintainers) {
maintainersEffectRan = true; // Mark as ran to prevent re-running maintainersEffectRan = true; // Mark as ran to prevent re-running
maintainersLoaded = true; // Set flag before loading to prevent concurrent calls maintainersLoaded = true; // Set flag before loading to prevent concurrent calls
loadAllMaintainers().catch(err => { loadAllMaintainers().catch(err => {
if (!isMounted) return;
maintainersLoaded = false; // Reset on error so we can retry maintainersLoaded = false; // Reset on error so we can retry
maintainersEffectRan = false; // Allow retry maintainersEffectRan = false; // Allow retry
console.warn('Failed to load maintainers:', err); console.warn('Failed to load maintainers:', err);
}); });
} }
} catch (err) {
// Ignore SSR errors and errors during destruction
if (isMounted) {
console.warn('Maintainers effect error:', err);
}
}
}); });
// Watch for auto-save setting changes // Watch for auto-save setting changes
$effect(() => { $effect(() => {
if (!isMounted) return;
// Check auto-save setting and update interval (async, but don't await) // Check auto-save setting and update interval (async, but don't await)
settingsStore.getSettings().then(settings => { settingsStore.getSettings().then(settings => {
if (!isMounted) return;
if (settings.autoSave && !autoSaveInterval) { if (settings.autoSave && !autoSaveInterval) {
// Auto-save was enabled, set it up // Auto-save was enabled, set it up
setupAutoSave(); setupAutoSave();
@ -134,50 +215,77 @@
} }
} }
}).catch(err => { }).catch(err => {
if (isMounted) {
console.warn('Failed to check auto-save setting:', err); console.warn('Failed to check auto-save setting:', err);
}
}); });
}); });
// Sync with userStore // Sync with userStore
$effect(() => { $effect(() => {
if (!isMounted) return;
try {
const currentUser = $userStore; const currentUser = $userStore;
if (!currentUser || !isMounted) return;
const wasLoggedIn = userPubkey !== null || userPubkeyHex !== null; const wasLoggedIn = userPubkey !== null || userPubkeyHex !== null;
if (currentUser.userPubkey && currentUser.userPubkeyHex) { if (currentUser.userPubkey && currentUser.userPubkeyHex && isMounted) {
const wasDifferent = userPubkey !== currentUser.userPubkey || userPubkeyHex !== currentUser.userPubkeyHex; const wasDifferent = userPubkey !== currentUser.userPubkey || userPubkeyHex !== currentUser.userPubkeyHex;
userPubkey = currentUser.userPubkey; userPubkey = currentUser.userPubkey;
userPubkeyHex = currentUser.userPubkeyHex; userPubkeyHex = currentUser.userPubkeyHex;
// Reload data when user logs in or pubkey changes // Reload data when user logs in or pubkey changes
if (wasDifferent) { if (wasDifferent && isMounted) {
// Reset repoNotFound flag when user logs in, so we can retry loading // Reset repoNotFound flag when user logs in, so we can retry loading
repoNotFound = false; repoNotFound = false;
// Clear cached email and name when user changes // Clear cached email and name when user changes
cachedUserEmail = null; cachedUserEmail = null;
cachedUserName = null; cachedUserName = null;
checkMaintainerStatus().catch(err => console.warn('Failed to reload maintainer status after login:', err)); if (!isMounted) return;
loadBookmarkStatus().catch(err => console.warn('Failed to reload bookmark status after login:', err)); checkMaintainerStatus().catch(err => {
if (isMounted) console.warn('Failed to reload maintainer status after login:', err);
});
loadBookmarkStatus().catch(err => {
if (isMounted) console.warn('Failed to reload bookmark status after login:', err);
});
// Reset flags to allow reload // Reset flags to allow reload
maintainersLoaded = false; maintainersLoaded = false;
maintainersEffectRan = false; maintainersEffectRan = false;
lastRepoKey = null; lastRepoKey = null;
loadAllMaintainers().catch(err => console.warn('Failed to reload maintainers after login:', err)); loadAllMaintainers().catch(err => {
if (isMounted) console.warn('Failed to reload maintainers after login:', err);
});
// Recheck clone status after login (force refresh) - delay slightly to ensure auth headers are ready // Recheck clone status after login (force refresh) - delay slightly to ensure auth headers are ready
setTimeout(() => { setTimeout(() => {
checkCloneStatus(true).catch(err => console.warn('Failed to recheck clone status after login:', err)); if (isMounted) {
checkCloneStatus(true).catch(err => {
if (isMounted) console.warn('Failed to recheck clone status after login:', err);
});
}
}, 100); }, 100);
// Reload all repository data with the new user context // Reload all repository data with the new user context
if (!loading) { if (!loading && isMounted) {
loadBranches().catch(err => console.warn('Failed to reload branches after login:', err)); loadBranches().catch(err => {
loadFiles().catch(err => console.warn('Failed to reload files after login:', err)); if (isMounted) console.warn('Failed to reload branches after login:', err);
loadReadme().catch(err => console.warn('Failed to reload readme after login:', err)); });
loadTags().catch(err => console.warn('Failed to reload tags after login:', err)); loadFiles().catch(err => {
if (isMounted) console.warn('Failed to reload files after login:', err);
});
loadReadme().catch(err => {
if (isMounted) console.warn('Failed to reload readme after login:', err);
});
loadTags().catch(err => {
if (isMounted) console.warn('Failed to reload tags after login:', err);
});
// Reload discussions when user logs in (needs user context for relay selection) // Reload discussions when user logs in (needs user context for relay selection)
loadDiscussions().catch(err => console.warn('Failed to reload discussions after login:', err)); loadDiscussions().catch(err => {
if (isMounted) console.warn('Failed to reload discussions after login:', err);
});
} }
} }
} else { } else if (isMounted) {
userPubkey = null; userPubkey = null;
userPubkeyHex = null; userPubkeyHex = null;
// Clear cached email and name when user logs out // Clear cached email and name when user logs out
@ -185,18 +293,32 @@
cachedUserName = null; cachedUserName = null;
// Reload data when user logs out to hide private content // Reload data when user logs out to hide private content
if (wasLoggedIn) { if (wasLoggedIn && isMounted) {
checkMaintainerStatus().catch(err => console.warn('Failed to reload maintainer status after logout:', err)); checkMaintainerStatus().catch(err => {
loadBookmarkStatus().catch(err => console.warn('Failed to reload bookmark status after logout:', err)); if (isMounted) console.warn('Failed to reload maintainer status after logout:', err);
});
loadBookmarkStatus().catch(err => {
if (isMounted) console.warn('Failed to reload bookmark status after logout:', err);
});
// Reset flags to allow reload // Reset flags to allow reload
maintainersLoaded = false; maintainersLoaded = false;
maintainersEffectRan = false; maintainersEffectRan = false;
lastRepoKey = null; lastRepoKey = null;
loadAllMaintainers().catch(err => console.warn('Failed to reload maintainers after logout:', err)); loadAllMaintainers().catch(err => {
if (isMounted) console.warn('Failed to reload maintainers after logout:', err);
});
// If repo is private and user logged out, reload to trigger access check // If repo is private and user logged out, reload to trigger access check
if (!loading && activeTab === 'files') { if (!loading && activeTab === 'files' && isMounted) {
loadFiles().catch(err => console.warn('Failed to reload files after logout:', err)); loadFiles().catch(err => {
if (isMounted) console.warn('Failed to reload files after logout:', err);
});
}
}
} }
} catch (err) {
// Ignore errors during destruction
if (isMounted) {
console.warn('User store sync error:', err);
} }
} }
}); });
@ -282,6 +404,8 @@
try { try {
// Use the current page URL to get the correct host and port // Use the current page URL to get the correct host and port
// This ensures we use the same domain/port the user is currently viewing // This ensures we use the same domain/port the user is currently viewing
// Guard against SSR - $page store can only be accessed in component context
if (typeof window === 'undefined') return;
const currentUrl = $page.url; const currentUrl = $page.url;
const host = currentUrl.host; // Includes port if present (e.g., "localhost:5173") const host = currentUrl.host; // Includes port if present (e.g., "localhost:5173")
const protocol = currentUrl.protocol.slice(0, -1); // Remove trailing ":" const protocol = currentUrl.protocol.slice(0, -1); // Remove trailing ":"
@ -380,9 +504,10 @@
// Redirect to a valid tab if current tab requires cloning but repo isn't cloned and API fallback isn't available // Redirect to a valid tab if current tab requires cloning but repo isn't cloned and API fallback isn't available
$effect(() => { $effect(() => {
if (!isMounted) return;
if (isRepoCloned === false && !canUseApiFallback && tabs.length > 0) { if (isRepoCloned === false && !canUseApiFallback && tabs.length > 0) {
const currentTab = tabs.find(t => t.id === activeTab); const currentTab = tabs.find(t => t.id === activeTab);
if (!currentTab) { if (!currentTab && isMounted) {
// Current tab requires cloning, switch to first available tab // Current tab requires cloning, switch to first available tab
activeTab = tabs[0].id as typeof activeTab; activeTab = tabs[0].id as typeof activeTab;
} }
@ -1992,6 +2117,8 @@
loadingDocs = true; loadingDocs = true;
try { try {
// Guard against SSR - $page store can only be accessed in component context
if (typeof window === 'undefined') return;
// Check if repo is private and user has access // Check if repo is private and user has access
const data = $page.data as typeof pageData; const data = $page.data as typeof pageData;
if (repoIsPrivate) { if (repoIsPrivate) {
@ -2129,6 +2256,8 @@
try { try {
// Get images from page data (loaded from announcement) // Get images from page data (loaded from announcement)
// Use $page.data directly to ensure we get the latest data // Use $page.data directly to ensure we get the latest data
// Guard against SSR - $page store can only be accessed in component context
if (typeof window === 'undefined') return;
const data = $page.data as typeof pageData; const data = $page.data as typeof pageData;
if (data.image) { if (data.image) {
repoImage = data.image; repoImage = data.image;
@ -2141,6 +2270,8 @@
// Also fetch from announcement directly as fallback (only if not private or user has access) // Also fetch from announcement directly as fallback (only if not private or user has access)
if (!repoImage && !repoBanner) { if (!repoImage && !repoBanner) {
// Guard against SSR - $page store can only be accessed in component context
if (typeof window === 'undefined') return;
const data = $page.data as typeof pageData; const data = $page.data as typeof pageData;
// Check access for private repos // Check access for private repos
if (repoIsPrivate) { if (repoIsPrivate) {
@ -2212,16 +2343,26 @@
// Reactively update images when pageData changes (only once, when data becomes available) // Reactively update images when pageData changes (only once, when data becomes available)
$effect(() => { $effect(() => {
// Guard against SSR and component destruction
if (typeof window === 'undefined' || !isMounted) return;
try {
const data = $page.data as typeof pageData; const data = $page.data as typeof pageData;
if (!data || !isMounted) return;
// Only update if we have new data and don't already have the images set // Only update if we have new data and don't already have the images set
if (data.image && data.image !== repoImage) { if (data.image && data.image !== repoImage && isMounted) {
repoImage = data.image; repoImage = data.image;
console.log('[Repo Images] Updated image from pageData (reactive):', repoImage); console.log('[Repo Images] Updated image from pageData (reactive):', repoImage);
} }
if (data.banner && data.banner !== repoBanner) { if (data.banner && data.banner !== repoBanner && isMounted) {
repoBanner = data.banner; repoBanner = data.banner;
console.log('[Repo Images] Updated banner from pageData (reactive):', repoBanner); console.log('[Repo Images] Updated banner from pageData (reactive):', repoBanner);
} }
} catch (err) {
// Ignore errors during destruction
if (isMounted) {
console.warn('Image update effect error:', err);
}
}
}); });
onMount(async () => { onMount(async () => {
@ -2243,53 +2384,111 @@
} }
// Close menu when clicking outside (handled by RepoHeaderEnhanced component) // Close menu when clicking outside (handled by RepoHeaderEnhanced component)
function handleClickOutside(event: MouseEvent) { clickOutsideHandler = (event: MouseEvent) => {
if (!isMounted) return;
try {
const target = event.target as HTMLElement; const target = event.target as HTMLElement;
if (showRepoMenu && !target.closest('.repo-header')) { if (showRepoMenu && !target.closest('.repo-header') && isMounted) {
showRepoMenu = false; showRepoMenu = false;
} }
} catch (err) {
// Ignore errors during destruction
if (isMounted) {
console.warn('Click outside handler error:', err);
}
} }
};
document.addEventListener('click', handleClickOutside); document.addEventListener('click', clickOutsideHandler);
await loadBranches(); await loadBranches();
if (!isMounted) return;
// Skip other API calls if repository doesn't exist // Skip other API calls if repository doesn't exist
if (repoNotFound) { if (repoNotFound) {
loading = false; loading = false;
return; return;
} }
// loadBranches() already handles setting currentBranch to the default branch // loadBranches() already handles setting currentBranch to the default branch
await loadFiles(); await loadFiles();
if (!isMounted) return;
await checkAuth(); await checkAuth();
if (!isMounted) return;
await loadTags(); await loadTags();
if (!isMounted) return;
await checkMaintainerStatus(); await checkMaintainerStatus();
if (!isMounted) return;
await loadBookmarkStatus(); await loadBookmarkStatus();
if (!isMounted) return;
await loadAllMaintainers(); await loadAllMaintainers();
if (!isMounted) return;
// Check clone status (needed to disable write operations) // Check clone status (needed to disable write operations)
await checkCloneStatus(); await checkCloneStatus();
if (!isMounted) return;
await checkVerification(); await checkVerification();
if (!isMounted) return;
await loadReadme(); await loadReadme();
if (!isMounted) return;
await loadForkInfo(); await loadForkInfo();
if (!isMounted) return;
await loadRepoImages(); await loadRepoImages();
if (!isMounted) return;
// Load clone URL reachability status // Load clone URL reachability status
loadCloneUrlReachability().catch(err => console.warn('Failed to load clone URL reachability:', err)); loadCloneUrlReachability().catch(err => {
if (isMounted) console.warn('Failed to load clone URL reachability:', err);
});
// Set up auto-save if enabled // Set up auto-save if enabled
setupAutoSave().catch(err => console.warn('Failed to setup auto-save:', err)); setupAutoSave().catch(err => {
if (isMounted) console.warn('Failed to setup auto-save:', err);
});
}); });
// Cleanup on destroy // Cleanup on destroy
onDestroy(() => { onDestroy(() => {
// Mark component as unmounted first to prevent any state updates
isMounted = false;
// Clean up intervals and timeouts
try {
if (autoSaveInterval) { if (autoSaveInterval) {
clearInterval(autoSaveInterval); clearInterval(autoSaveInterval);
autoSaveInterval = null; autoSaveInterval = null;
} }
} catch (err) {
// Ignore errors during cleanup
}
try {
if (readmeAutoLoadTimeout) { if (readmeAutoLoadTimeout) {
clearTimeout(readmeAutoLoadTimeout); clearTimeout(readmeAutoLoadTimeout);
readmeAutoLoadTimeout = null; readmeAutoLoadTimeout = null;
} }
} catch (err) {
// Ignore errors during cleanup
}
// Clean up event listeners
try {
if (clickOutsideHandler) {
document.removeEventListener('click', clickOutsideHandler);
clickOutsideHandler = null;
}
} catch (err) {
// Ignore errors - listener may not exist or already removed
}
}); });
async function checkAuth() { async function checkAuth() {
@ -4572,38 +4771,63 @@
// Only load tab content when tab actually changes, not on every render // Only load tab content when tab actually changes, not on every render
let lastTab = $state<string | null>(null); let lastTab = $state<string | null>(null);
$effect(() => { $effect(() => {
if (!isMounted) return;
if (activeTab !== lastTab) { if (activeTab !== lastTab) {
lastTab = activeTab; lastTab = activeTab;
if (!isMounted) return;
if (activeTab === 'files') { if (activeTab === 'files') {
// Files tab - ensure files are loaded and README is shown if available // Files tab - ensure files are loaded and README is shown if available
if (files.length === 0 || currentPath !== '') { if (files.length === 0 || currentPath !== '') {
loadFiles(''); loadFiles('').catch(err => {
} else if (files.length > 0 && !currentFile) { if (isMounted) console.warn('Failed to load files:', err);
});
} else if (files.length > 0 && !currentFile && isMounted) {
// Files already loaded, ensure README is shown // Files already loaded, ensure README is shown
const readmeFile = findReadmeFile(files); const readmeFile = findReadmeFile(files);
if (readmeFile) { if (readmeFile) {
setTimeout(() => { setTimeout(() => {
loadFile(readmeFile.path); if (isMounted) {
loadFile(readmeFile.path).catch(err => {
if (isMounted) console.warn('Failed to load README file:', err);
});
}
}, 100); }, 100);
} }
} }
} else if (activeTab === 'history') { } else if (activeTab === 'history' && isMounted) {
loadCommitHistory(); loadCommitHistory().catch(err => {
} else if (activeTab === 'tags') { if (isMounted) console.warn('Failed to load commit history:', err);
loadTags(); });
loadReleases(); // Load releases to check for tag associations } else if (activeTab === 'tags' && isMounted) {
loadTags().catch(err => {
if (isMounted) console.warn('Failed to load tags:', err);
});
loadReleases().catch(err => {
if (isMounted) console.warn('Failed to load releases:', err);
}); // Load releases to check for tag associations
} else if (activeTab === 'code-search') { } else if (activeTab === 'code-search') {
// Code search is performed on demand, not auto-loaded // Code search is performed on demand, not auto-loaded
} else if (activeTab === 'issues') { } else if (activeTab === 'issues' && isMounted) {
loadIssues(); loadIssues().catch(err => {
} else if (activeTab === 'prs') { if (isMounted) console.warn('Failed to load issues:', err);
loadPRs(); });
} else if (activeTab === 'docs') { } else if (activeTab === 'prs' && isMounted) {
loadDocumentation(); loadPRs().catch(err => {
} else if (activeTab === 'discussions') { if (isMounted) console.warn('Failed to load PRs:', err);
loadDiscussions(); });
} else if (activeTab === 'patches') { } else if (activeTab === 'docs' && isMounted) {
loadPatches(); loadDocumentation().catch(err => {
if (isMounted) console.warn('Failed to load documentation:', err);
});
} else if (activeTab === 'discussions' && isMounted) {
loadDiscussions().catch(err => {
if (isMounted) console.warn('Failed to load discussions:', err);
});
} else if (activeTab === 'patches' && isMounted) {
loadPatches().catch(err => {
if (isMounted) console.warn('Failed to load patches:', err);
});
} }
} }
}); });
@ -4611,32 +4835,44 @@
// Reload all branch-dependent data when branch changes // Reload all branch-dependent data when branch changes
let lastBranch = $state<string | null>(null); let lastBranch = $state<string | null>(null);
$effect(() => { $effect(() => {
if (!isMounted) return;
if (currentBranch && currentBranch !== lastBranch) { if (currentBranch && currentBranch !== lastBranch) {
lastBranch = currentBranch; lastBranch = currentBranch;
if (!isMounted) return;
// Reload README (always branch-specific) // Reload README (always branch-specific)
loadReadme().catch(err => console.warn('Failed to reload README after branch change:', err)); loadReadme().catch(err => {
if (isMounted) console.warn('Failed to reload README after branch change:', err);
});
// Reload files if files tab is active // Reload files if files tab is active
if (activeTab === 'files') { if (activeTab === 'files' && isMounted) {
if (currentFile) { if (currentFile) {
loadFile(currentFile).catch(err => console.warn('Failed to reload file after branch change:', err)); loadFile(currentFile).catch(err => {
if (isMounted) console.warn('Failed to reload file after branch change:', err);
});
} else { } else {
loadFiles(currentPath).catch(err => console.warn('Failed to reload files after branch change:', err)); loadFiles(currentPath).catch(err => {
if (isMounted) console.warn('Failed to reload files after branch change:', err);
});
} }
} }
// Reload commit history if history tab is active // Reload commit history if history tab is active
if (activeTab === 'history') { if (activeTab === 'history' && isMounted) {
loadCommitHistory().catch(err => console.warn('Failed to reload commit history after branch change:', err)); loadCommitHistory().catch(err => {
if (isMounted) console.warn('Failed to reload commit history after branch change:', err);
});
} }
// Reload documentation if docs tab is active (reset to force reload) // Reload documentation if docs tab is active (reset to force reload)
if (activeTab === 'docs') { if (activeTab === 'docs' && isMounted) {
documentationHtml = null; documentationHtml = null;
documentationContent = null; documentationContent = null;
documentationKind = null; documentationKind = null;
loadDocumentation().catch(err => console.warn('Failed to reload documentation after branch change:', err)); loadDocumentation().catch(err => {
if (isMounted) console.warn('Failed to reload documentation after branch change:', err);
});
} }
} }
}); });
@ -4650,7 +4886,7 @@
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
<meta property="og:title" content={pageData.title || `${repoName} - Repository`} /> <meta property="og:title" content={pageData.title || `${repoName} - Repository`} />
<meta property="og:description" content={pageData.description || repoDescription || `Repository: ${repoName}`} /> <meta property="og:description" content={pageData.description || repoDescription || `Repository: ${repoName}`} />
<meta property="og:url" content={pageData.repoUrl || `https://${$page.url.host}${$page.url.pathname}`} /> <meta property="og:url" content={pageData.repoUrl || (typeof window !== 'undefined' ? `https://${$page.url.host}${$page.url.pathname}` : '')} />
{#if (pageData.image || repoImage) && String(pageData.image || repoImage).trim()} {#if (pageData.image || repoImage) && String(pageData.image || repoImage).trim()}
<meta property="og:image" content={pageData.image || repoImage} /> <meta property="og:image" content={pageData.image || repoImage} />
{/if} {/if}

Loading…
Cancel
Save