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. 276
      src/lib/components/NavBar.svelte
  3. 336
      src/routes/+layout.svelte
  4. 203
      src/routes/+page.svelte
  5. 500
      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"}

276
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(() => {
const currentUser = $userStore; if (!isMounted || typeof window === 'undefined') return;
if (currentUser.userPubkey && currentUser.userPubkeyHex) { try {
// Check if session expired const currentUser = $userStore;
if (isSessionExpired()) { if (!currentUser || !isMounted) return;
userStore.reset();
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; userPubkey = null;
} else {
userPubkey = currentUser.userPubkey;
updateActivity();
} }
} else { } catch (err) {
userPubkey = null; // 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)
const currentState = $userStore; try {
if (currentState.userPubkey && currentState.userPubkeyHex) { const currentState = $userStore;
// User is logged in - restore state (already synced by $effect, but ensure it's set) if (currentState && currentState.userPubkey && currentState.userPubkeyHex && isMounted) {
userPubkey = currentState.userPubkey; // User is logged in - restore state (already synced by $effect, but ensure it's set)
// Update activity to extend session userPubkey = currentState.userPubkey;
updateActivity(); // Update activity to extend session
} else { updateActivity();
// User not logged in - check auth } else if (isMounted) {
checkAuth(); // 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 // 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
document.addEventListener('click', updateActivityOnInteraction, { passive: true }); if (updateActivityOnInteraction) {
document.addEventListener('keydown', updateActivityOnInteraction, { passive: true }); document.addEventListener('click', updateActivityOnInteraction, { passive: true });
document.addEventListener('scroll', updateActivityOnInteraction, { passive: true }); document.addEventListener('keydown', 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 (isSessionExpired()) { if (!isMounted) {
// Session expired - logout user if (expiryCheckInterval) {
userStore.reset(); clearInterval(expiryCheckInterval);
userPubkey = null; expiryCheckInterval = null;
clearInterval(expiryCheckInterval); }
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 }, 5 * 60 * 1000); // Check every 5 minutes
});
return () => { onDestroy(() => {
document.removeEventListener('click', updateActivityOnInteraction); // Mark component as unmounted first
document.removeEventListener('keydown', updateActivityOnInteraction); isMounted = false;
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() { 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() {
// Don't check auth if user store indicates user is logged out if (!isMounted || typeof window === 'undefined') return;
const currentState = $userStore;
if (!currentState.userPubkey) {
userPubkey = null;
return;
}
// Don't check auth if user store indicates user is logged out
try { try {
if (isNIP07Available()) { const currentState = $userStore;
if (!currentState || !currentState.userPubkey) {
if (isMounted) {
userPubkey = null;
}
return;
}
if (isNIP07Available() && isMounted) {
userPubkey = await getPublicKeyWithNIP07(); userPubkey = await getPublicKeyWithNIP07();
} else if (isMounted) {
userPubkey = null;
} }
} catch (err) { } catch (err) {
console.log('NIP-07 not available or user not connected'); if (isMounted) {
userPubkey = null; console.log('NIP-07 not available or user not connected');
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) {
console.error('Failed to get public key from NIP-07:', err); if (isMounted) {
alert('Failed to connect to Nostr extension. Please make sure your extension is unlocked and try again.'); 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; 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();
userPubkey = pubkey; if (isMounted) {
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) {
console.error('Failed to decode pubkey:', decodeErr); if (isMounted) {
alert('Invalid public key format. Please try again.'); console.error('Failed to decode pubkey:', decodeErr);
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
updateActivity(); if (isMounted) {
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) {
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 // 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) {
console.error('Login error:', err); if (isMounted) {
const errorMessage = err instanceof Error ? err.message : String(err); console.error('Login error:', err);
alert(`Failed to login: ${errorMessage}. Please make sure your Nostr extension is unlocked and try again.`); 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() { async function logout() {
userPubkey = null; if (typeof window === 'undefined' || !isMounted) return;
// Reset user store if (isMounted) {
userStore.reset(); userPubkey = null;
// Clear activity tracking // Reset user store
clearActivity(); userStore.reset();
// Clear activity tracking
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
await goto('/', { replaceState: true, invalidateAll: true }); if (isMounted) {
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 {

336
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,145 +47,242 @@
// 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();
theme = settings.theme; if (isMounted) {
themeLoaded = true; theme = settings.theme;
applyTheme(theme);
// Also sync to localStorage for app.html flash prevention
localStorage.setItem('theme', theme);
} catch (err) {
console.warn('Failed to load theme from settings, using default:', err);
// Fallback to localStorage for migration
const savedTheme = localStorage.getItem('theme') as 'gitrepublic-light' | 'gitrepublic-dark' | 'gitrepublic-black' | null;
if (savedTheme === 'gitrepublic-light' || savedTheme === 'gitrepublic-dark' || savedTheme === 'gitrepublic-black') {
theme = savedTheme;
themeLoaded = true;
applyTheme(theme);
// Migrate to settings store
settingsStore.setSetting('theme', theme).catch(console.error);
} else {
theme = 'gitrepublic-dark';
themeLoaded = true; themeLoaded = true;
applyTheme(theme); applyTheme(theme);
// Also sync to localStorage for app.html flash prevention
localStorage.setItem('theme', theme); localStorage.setItem('theme', theme);
} }
} catch (err) {
if (!isMounted) return;
console.warn('Failed to load theme from settings, using default:', err);
// Fallback to localStorage for migration
try {
const savedTheme = localStorage.getItem('theme') as 'gitrepublic-light' | 'gitrepublic-dark' | 'gitrepublic-black' | null;
if (savedTheme === 'gitrepublic-light' || savedTheme === 'gitrepublic-dark' || savedTheme === 'gitrepublic-black') {
if (isMounted) {
theme = savedTheme;
themeLoaded = true;
applyTheme(theme);
// Migrate to settings store
settingsStore.setSetting('theme', theme).catch(console.error);
}
} else if (isMounted) {
theme = 'gitrepublic-dark';
themeLoaded = true;
applyTheme(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
const currentState = $userStore; try {
if (currentState.userPubkey && currentState.userPubkeyHex) { const currentState = $userStore;
updateActivity(); if (currentState && currentState.userPubkey && currentState.userPubkeyHex && isMounted) {
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
const currentState = $userStore; if (typeof window !== 'undefined' && isMounted) {
// Only check if we don't have a user or if user level is strictly_rate_limited try {
if (!currentState.userPubkey || currentState.userLevel === 'strictly_rate_limited') { const pageUrl = $page.url;
checkUserLevel(); if (pageUrl && pageUrl.pathname !== '/') {
const currentState = $userStore;
// Only check if we don't have a user or if user level is strictly_rate_limited
if (isMounted && (!currentState.userPubkey || currentState.userLevel === 'strictly_rate_limited')) {
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) => {
const customEvent = event as CustomEvent; if (!isMounted) return;
if (customEvent.detail?.transfers) { try {
// Filter out dismissed transfers const customEvent = event as CustomEvent;
pendingTransfers = customEvent.detail.transfers.filter( if (customEvent.detail?.transfers && isMounted) {
(t: { eventId: string }) => !dismissedTransfers.has(t.eventId) // Filter out dismissed transfers
); pendingTransfers = customEvent.detail.transfers.filter(
(t: { eventId: string }) => !dismissedTransfers.has(t.eventId)
);
}
} catch (err) {
// Ignore errors during destruction
if (isMounted) {
console.warn('Pending transfers event handler error:', err);
}
} }
}; };
window.addEventListener('pendingTransfers', handlePendingTransfersEvent); if (handlePendingTransfersEvent) {
window.addEventListener('pendingTransfers', handlePendingTransfersEvent);
}
// Listen for theme changes from SettingsModal // Listen for theme changes from SettingsModal
const handleThemeChanged = (event: Event) => { handleThemeChanged = (event: Event) => {
const customEvent = event as CustomEvent<{ theme: 'gitrepublic-light' | 'gitrepublic-dark' | 'gitrepublic-black' }>; if (!isMounted) return;
if (customEvent.detail?.theme) { try {
theme = customEvent.detail.theme; const customEvent = event as CustomEvent<{ theme: 'gitrepublic-light' | 'gitrepublic-dark' | 'gitrepublic-black' }>;
// Sync to localStorage for app.html flash prevention if (customEvent.detail?.theme && isMounted) {
localStorage.setItem('theme', theme); theme = customEvent.detail.theme;
// Theme will be applied via $effect // Sync to localStorage for app.html flash prevention
localStorage.setItem('theme', theme);
// Theme will be applied via $effect
}
} catch (err) {
// Ignore errors during destruction
if (isMounted) {
console.warn('Theme changed event handler error:', err);
}
} }
}; };
window.addEventListener('themeChanged', handleThemeChanged);
if (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
window.removeEventListener('pendingTransfers', handlePendingTransfersEvent); isMounted = false;
window.removeEventListener('themeChanged', handleThemeChanged);
}; // Clean up event listeners
try {
if (handlePendingTransfersEvent) {
window.removeEventListener('pendingTransfers', handlePendingTransfersEvent);
handlePendingTransfersEvent = null;
}
} catch (err) {
// Ignore errors during cleanup
}
try {
if (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
const currentState = $userStore; try {
if (checkingUserLevel || (currentState.userPubkey && currentState.userLevel !== 'strictly_rate_limited')) { const currentState = $userStore;
return; if (!currentState || !isMounted) return;
}
// Only check user level if user has explicitly logged in (has pubkey in store) if (checkingUserLevel || (currentState.userPubkey && currentState.userLevel !== 'strictly_rate_limited')) {
// Don't automatically get pubkey from NIP-07 - that should only happen on explicit login return;
if (!currentState.userPubkey) { }
// User not logged in - set to strictly rate limited without checking
userStore.setUser(null, null, 'strictly_rate_limited', null);
return;
}
checkingUserLevel = true; // Only check user level if user has explicitly logged in (has pubkey in store)
userStore.setChecking(true); // Don't automatically get pubkey from NIP-07 - that should only happen on explicit login
if (!currentState.userPubkey) {
// User not logged in - set to strictly rate limited without checking
if (isMounted) {
userStore.setUser(null, null, 'strictly_rate_limited', null);
}
return;
}
try { if (!isMounted) return;
// Use pubkey from store (user has explicitly logged in)
const userPubkey = currentState.userPubkey; checkingUserLevel = true;
const userPubkeyHex = currentState.userPubkeyHex; userStore.setChecking(true);
// Determine user level try {
const levelResult = await determineUserLevel(userPubkey, userPubkeyHex); // Use pubkey from store (user has explicitly logged in)
const userPubkey = currentState.userPubkey;
// Update user store const userPubkeyHex = currentState.userPubkeyHex;
userStore.setUser(
levelResult.userPubkey, if (!isMounted) {
levelResult.userPubkeyHex, checkingUserLevel = false;
levelResult.level, userStore.setChecking(false);
levelResult.error || null return;
); }
// Update activity if user is logged in // Determine user level
if (levelResult.userPubkey && levelResult.userPubkeyHex) { const levelResult = await determineUserLevel(userPubkey, userPubkeyHex);
updateActivity();
// Check for pending transfers if (!isMounted) {
checkPendingTransfers(levelResult.userPubkeyHex); checkingUserLevel = false;
userStore.setChecking(false);
return;
}
// Update user store
userStore.setUser(
levelResult.userPubkey,
levelResult.userPubkeyHex,
levelResult.level,
levelResult.error || null
);
// Update activity if user is logged in
if (levelResult.userPubkey && levelResult.userPubkeyHex && isMounted) {
updateActivity();
// Check for pending transfers
checkPendingTransfers(levelResult.userPubkeyHex);
}
} catch (err) {
if (isMounted) {
console.error('Failed to check user level:', err);
// Set to strictly rate limited on error
userStore.setUser(null, null, 'strictly_rate_limited', 'Failed to check user level');
}
} finally {
if (isMounted) {
checkingUserLevel = false;
userStore.setChecking(false);
}
} }
} catch (err) { } catch (err) {
console.error('Failed to check user level:', err); // Ignore errors during destruction
// Set to strictly rate limited on error if (isMounted) {
userStore.setUser(null, null, 'strictly_rate_limited', 'Failed to check user level'); console.warn('User level check error:', err);
} finally { }
checkingUserLevel = false;
userStore.setChecking(false);
} }
} }
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,20 +382,48 @@
} }
// 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(() => {
const currentUser = $userStore; if (!isMounted || typeof window === 'undefined') return;
if (currentUser.userPubkeyHex && !checkingUserLevel) { try {
checkPendingTransfers(currentUser.userPubkeyHex); const currentUser = $userStore;
} else if (!currentUser.userPubkeyHex) { if (!currentUser || !isMounted) return;
// Clear transfers when user logs out
pendingTransfers = []; if (currentUser.userPubkeyHex && !checkingUserLevel && isMounted) {
dismissedTransfers.clear(); checkPendingTransfers(currentUser.userPubkeyHex);
} else if (!currentUser.userPubkeyHex && isMounted) {
// Clear transfers when user logs out
pendingTransfers = [];
dismissedTransfers.clear();
}
} catch (err) {
// Ignore errors during destruction
if (isMounted) {
console.warn('Transfer check effect error:', err);
}
} }
}); });

203
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,100 +14,163 @@
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(() => {
const currentUser = $userStore; if (!isMounted || typeof window === 'undefined') return;
if (currentUser.userPubkey && currentUser.userPubkeyHex) { try {
// User is logged in - sync local state with store const currentUser = $userStore;
userPubkey = currentUser.userPubkey; if (!currentUser || !isMounted) return;
userPubkeyHex = currentUser.userPubkeyHex;
} else { if (currentUser.userPubkey && currentUser.userPubkeyHex && isMounted) {
// User has logged out - clear local state // User is logged in - sync local state with store
userPubkey = null; userPubkey = currentUser.userPubkey;
userPubkeyHex = null; userPubkeyHex = currentUser.userPubkeyHex;
} else if (isMounted) {
// User has logged out - clear local state
userPubkey = 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
document.body.style.overflow = 'hidden'; try {
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
if (isSessionExpired()) { try {
// Session expired - logout user if (isSessionExpired()) {
userStore.reset(); // Session expired - logout user
checkingAuth = true; if (isMounted) {
checkAuth(); userStore.reset();
} else { checkingAuth = true;
// Check userStore first - if user is already logged in, use store values checkAuth();
const currentUser = $userStore; }
if (currentUser.userPubkey && currentUser.userPubkeyHex) { } else if (isMounted) {
// User is already logged in - use store values // Check userStore first - if user is already logged in, use store values
userPubkey = currentUser.userPubkey; const currentUser = $userStore;
userPubkeyHex = currentUser.userPubkeyHex; if (currentUser && currentUser.userPubkey && currentUser.userPubkeyHex) {
checkingAuth = false; // User is already logged in - use store values
// Update activity to extend session userPubkey = currentUser.userPubkey;
updateActivity(); userPubkeyHex = currentUser.userPubkeyHex;
// Don't redirect immediately - let the user see they're logged in checkingAuth = false;
// They can click "View Repositories" or navigate away // Update activity to extend session
} else { updateActivity();
// User not logged in - check if extension is available // Don't redirect immediately - let the user see they're logged in
checkingAuth = true; // They can click "View Repositories" or navigate away
checkAuth(); } else if (isMounted) {
// User not logged in - check if extension is available
checkingAuth = true;
checkAuth();
}
}
} catch (err) {
if (isMounted) {
console.warn('Failed to check auth on mount:', err);
} }
} }
});
// Return cleanup function onDestroy(() => {
return () => { // Mark component as unmounted first
// Re-enable scrolling when component is destroyed isMounted = false;
document.body.style.overflow = '';
}; // Re-enable scrolling when component is destroyed
try {
if (typeof document !== 'undefined' && document.body) {
document.body.style.overflow = '';
}
} catch (err) {
// Ignore errors during cleanup
}
}); });
async function checkAuth() { async function checkAuth() {
checkingAuth = true; if (!isMounted || typeof window === 'undefined') return;
// Check userStore first - if user has logged out, clear state if (isMounted) {
const currentUser = $userStore; checkingAuth = true;
if (!currentUser.userPubkey) {
userPubkey = null;
userPubkeyHex = null;
checkingAuth = false;
return;
} }
if (isNIP07Available()) { // Check userStore first - if user has logged out, clear state
try { try {
userPubkey = await getPublicKeyWithNIP07(); const currentUser = $userStore;
// Convert npub to hex for API calls if (!currentUser || !currentUser.userPubkey) {
// NIP-07 may return either npub or hex if (isMounted) {
if (/^[0-9a-f]{64}$/i.test(userPubkey)) { userPubkey = null;
// Already hex format userPubkeyHex = null;
userPubkeyHex = userPubkey.toLowerCase(); checkingAuth = false;
} else { }
// Try to decode as npub return;
try { }
const decoded = nip19.decode(userPubkey);
if (decoded.type === 'npub') { if (!isMounted) return;
userPubkeyHex = decoded.data as string;
} else { if (isNIP07Available()) {
userPubkeyHex = userPubkey; // Unknown type, use as-is try {
userPubkey = await getPublicKeyWithNIP07();
if (!isMounted) return;
// Convert npub to hex for API calls
// NIP-07 may return either npub or hex
if (/^[0-9a-f]{64}$/i.test(userPubkey)) {
// Already hex format
if (isMounted) {
userPubkeyHex = userPubkey.toLowerCase();
} }
} catch { } else if (isMounted) {
userPubkeyHex = userPubkey; // Assume it's already hex or use as-is // Try to decode as npub
try {
const decoded = nip19.decode(userPubkey);
if (decoded.type === 'npub') {
userPubkeyHex = decoded.data as string;
} else {
userPubkeyHex = userPubkey; // Unknown type, use as-is
}
} catch {
if (isMounted) {
userPubkeyHex = userPubkey; // Assume it's already hex or use as-is
}
}
}
} catch (err) {
if (isMounted) {
console.warn('Failed to load user pubkey:', err);
userPubkey = null;
userPubkeyHex = null;
} }
} }
} catch (err) { } else if (isMounted) {
console.warn('Failed to load user pubkey:', err); // Extension not available, clear state
userPubkey = null; userPubkey = null;
userPubkeyHex = null; userPubkeyHex = null;
} }
} else {
// Extension not available, clear state if (isMounted) {
userPubkey = null; checkingAuth = false;
userPubkeyHex = null; }
} catch (err) {
if (isMounted) {
console.warn('Auth check error:', err);
checkingAuth = false;
}
} }
checkingAuth = false;
} }
async function handleLogin() { async function handleLogin() {

500
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,37 +157,53 @@
let maintainersEffectRan = $state(false); let maintainersEffectRan = $state(false);
$effect(() => { $effect(() => {
const data = $page.data as typeof pageData; // Guard against SSR and component destruction
const currentRepoKey = `${npub}/${repo}`; if (typeof window === 'undefined' || !isMounted) return;
try {
// Reset flags if repo changed const data = $page.data as typeof pageData;
if (currentRepoKey !== lastRepoKey) { if (!data || !isMounted) return;
maintainersLoaded = false;
maintainersEffectRan = false; const currentRepoKey = `${npub}/${repo}`;
lastRepoKey = currentRepoKey;
} // Reset flags if repo changed
if (currentRepoKey !== lastRepoKey && isMounted) {
// Only load if: maintainersLoaded = false;
// 1. We have page data maintainersEffectRan = false;
// 2. Effect hasn't run yet for this repo lastRepoKey = currentRepoKey;
// 3. We're not currently loading }
if ((repoOwnerPubkeyDerived || (repoMaintainers && repoMaintainers.length > 0)) &&
!maintainersEffectRan && // Only load if:
!loadingMaintainers) { // 1. We have page data
maintainersEffectRan = true; // Mark as ran to prevent re-running // 2. Effect hasn't run yet for this repo
maintainersLoaded = true; // Set flag before loading to prevent concurrent calls // 3. We're not currently loading
loadAllMaintainers().catch(err => { // 4. Component is still mounted
maintainersLoaded = false; // Reset on error so we can retry if (isMounted &&
maintainersEffectRan = false; // Allow retry (repoOwnerPubkeyDerived || (repoMaintainers && repoMaintainers.length > 0)) &&
console.warn('Failed to load maintainers:', err); !maintainersEffectRan &&
}); !loadingMaintainers) {
maintainersEffectRan = true; // Mark as ran to prevent re-running
maintainersLoaded = true; // Set flag before loading to prevent concurrent calls
loadAllMaintainers().catch(err => {
if (!isMounted) return;
maintainersLoaded = false; // Reset on error so we can retry
maintainersEffectRan = false; // Allow retry
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,69 +215,110 @@
} }
} }
}).catch(err => { }).catch(err => {
console.warn('Failed to check auto-save setting:', err); if (isMounted) {
console.warn('Failed to check auto-save setting:', err);
}
}); });
}); });
// Sync with userStore // Sync with userStore
$effect(() => { $effect(() => {
const currentUser = $userStore; if (!isMounted) return;
const wasLoggedIn = userPubkey !== null || userPubkeyHex !== null; try {
const currentUser = $userStore;
if (currentUser.userPubkey && currentUser.userPubkeyHex) { if (!currentUser || !isMounted) return;
const wasDifferent = userPubkey !== currentUser.userPubkey || userPubkeyHex !== currentUser.userPubkeyHex;
userPubkey = currentUser.userPubkey; const wasLoggedIn = userPubkey !== null || userPubkeyHex !== null;
userPubkeyHex = currentUser.userPubkeyHex;
if (currentUser.userPubkey && currentUser.userPubkeyHex && isMounted) {
// Reload data when user logs in or pubkey changes const wasDifferent = userPubkey !== currentUser.userPubkey || userPubkeyHex !== currentUser.userPubkeyHex;
if (wasDifferent) { userPubkey = currentUser.userPubkey;
// Reset repoNotFound flag when user logs in, so we can retry loading userPubkeyHex = currentUser.userPubkeyHex;
repoNotFound = false;
// Clear cached email and name when user changes // Reload data when user logs in or pubkey changes
if (wasDifferent && isMounted) {
// Reset repoNotFound flag when user logs in, so we can retry loading
repoNotFound = false;
// Clear cached email and name when user changes
cachedUserEmail = null;
cachedUserName = null;
if (!isMounted) return;
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
maintainersLoaded = false;
maintainersEffectRan = false;
lastRepoKey = null;
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
setTimeout(() => {
if (isMounted) {
checkCloneStatus(true).catch(err => {
if (isMounted) console.warn('Failed to recheck clone status after login:', err);
});
}
}, 100);
// Reload all repository data with the new user context
if (!loading && isMounted) {
loadBranches().catch(err => {
if (isMounted) console.warn('Failed to reload branches 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)
loadDiscussions().catch(err => {
if (isMounted) console.warn('Failed to reload discussions after login:', err);
});
}
}
} else if (isMounted) {
userPubkey = null;
userPubkeyHex = null;
// Clear cached email and name when user logs out
cachedUserEmail = null; cachedUserEmail = null;
cachedUserName = null; cachedUserName = null;
checkMaintainerStatus().catch(err => console.warn('Failed to reload maintainer status after login:', err)); // Reload data when user logs out to hide private content
loadBookmarkStatus().catch(err => console.warn('Failed to reload bookmark status after login:', err)); if (wasLoggedIn && isMounted) {
// Reset flags to allow reload checkMaintainerStatus().catch(err => {
maintainersLoaded = false; if (isMounted) console.warn('Failed to reload maintainer status after logout:', err);
maintainersEffectRan = false; });
lastRepoKey = null; loadBookmarkStatus().catch(err => {
loadAllMaintainers().catch(err => console.warn('Failed to reload maintainers after login:', err)); if (isMounted) console.warn('Failed to reload bookmark status after logout:', err);
// Recheck clone status after login (force refresh) - delay slightly to ensure auth headers are ready });
setTimeout(() => { // Reset flags to allow reload
checkCloneStatus(true).catch(err => console.warn('Failed to recheck clone status after login:', err)); maintainersLoaded = false;
}, 100); maintainersEffectRan = false;
// Reload all repository data with the new user context lastRepoKey = null;
if (!loading) { loadAllMaintainers().catch(err => {
loadBranches().catch(err => console.warn('Failed to reload branches after login:', err)); if (isMounted) console.warn('Failed to reload maintainers after logout:', err);
loadFiles().catch(err => console.warn('Failed to reload files after login:', err)); });
loadReadme().catch(err => console.warn('Failed to reload readme after login:', err)); // If repo is private and user logged out, reload to trigger access check
loadTags().catch(err => console.warn('Failed to reload tags after login:', err)); if (!loading && activeTab === 'files' && isMounted) {
// Reload discussions when user logs in (needs user context for relay selection) loadFiles().catch(err => {
loadDiscussions().catch(err => console.warn('Failed to reload discussions after login:', err)); if (isMounted) console.warn('Failed to reload files after logout:', err);
});
}
} }
} }
} else { } catch (err) {
userPubkey = null; // Ignore errors during destruction
userPubkeyHex = null; if (isMounted) {
// Clear cached email and name when user logs out console.warn('User store sync error:', err);
cachedUserEmail = null;
cachedUserName = null;
// Reload data when user logs out to hide private content
if (wasLoggedIn) {
checkMaintainerStatus().catch(err => console.warn('Failed to reload maintainer status after logout:', err));
loadBookmarkStatus().catch(err => console.warn('Failed to reload bookmark status after logout:', err));
// Reset flags to allow reload
maintainersLoaded = false;
maintainersEffectRan = false;
lastRepoKey = null;
loadAllMaintainers().catch(err => console.warn('Failed to reload maintainers after logout:', err));
// If repo is private and user logged out, reload to trigger access check
if (!loading && activeTab === 'files') {
loadFiles().catch(err => console.warn('Failed to reload files after logout:', 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,15 +2343,25 @@
// 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(() => {
const data = $page.data as typeof pageData; // Guard against SSR and component destruction
// Only update if we have new data and don't already have the images set if (typeof window === 'undefined' || !isMounted) return;
if (data.image && data.image !== repoImage) { try {
repoImage = data.image; const data = $page.data as typeof pageData;
console.log('[Repo Images] Updated image from pageData (reactive):', repoImage); if (!data || !isMounted) return;
} // Only update if we have new data and don't already have the images set
if (data.banner && data.banner !== repoBanner) { if (data.image && data.image !== repoImage && isMounted) {
repoBanner = data.banner; repoImage = data.image;
console.log('[Repo Images] Updated banner from pageData (reactive):', repoBanner); console.log('[Repo Images] Updated image from pageData (reactive):', repoImage);
}
if (data.banner && data.banner !== repoBanner && isMounted) {
repoBanner = data.banner;
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);
}
} }
}); });
@ -2243,52 +2384,110 @@
} }
// 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) => {
const target = event.target as HTMLElement; if (!isMounted) return;
if (showRepoMenu && !target.closest('.repo-header')) { try {
showRepoMenu = false; const target = event.target as HTMLElement;
if (showRepoMenu && !target.closest('.repo-header') && isMounted) {
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(() => {
if (autoSaveInterval) { // Mark component as unmounted first to prevent any state updates
clearInterval(autoSaveInterval); isMounted = false;
autoSaveInterval = null;
// Clean up intervals and timeouts
try {
if (autoSaveInterval) {
clearInterval(autoSaveInterval);
autoSaveInterval = null;
}
} catch (err) {
// Ignore errors during cleanup
} }
if (readmeAutoLoadTimeout) {
clearTimeout(readmeAutoLoadTimeout); try {
readmeAutoLoadTimeout = null; if (readmeAutoLoadTimeout) {
clearTimeout(readmeAutoLoadTimeout);
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
} }
}); });
@ -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