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 @@ @@ -70,3 +70,4 @@
{"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1771924650,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","pass announcement"]],"content":"Signed commit: pass announcement","id":"57e1440848e4b322a9b10a6dff49973f29c8dd20b85f6cc75fd40d32eb04f0e4","sig":"3866152051a42592e83a1850bf9f3fd49af597f7dcdb523ef39374d528f6c46df6118682cac3202c29ce89a90fec8b4284c68a57101c6c590d8d1a184cac9731"}
{"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1771949714,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","fallback to API if registered clone unavailble"]],"content":"Signed commit: fallback to API if registered clone unavailble","id":"4921a95aea13f6f72329ff8a278a8ff6321776973e8db327d59ea62b90d363cc","sig":"0efffc826cad23849bd311be582a70cb0a42f3958c742470e8488803c5882955184b9241bf77fcf65fa5ea38feef8bc82de4965de1c783adf53ed05e461dc5de"}
{"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1771952814,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","more work on branches"]],"content":"Signed commit: more work on branches","id":"adaaea7f2065a00cfd04c9de9bf82b1b976ac3d20c32389a8bd8aa7ad0a95677","sig":"71ce678d0a0732beab1f49f8318cbfe3d8b33d45eacf13392fdb9553e8b1f4732c28d8ffc33b50c9736a8324cf7604c223bb71ff4cfd32f41d7f3e81e1591fcc"}
{"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1771956701,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","implemented releases and code serach\nadd contributors to private repos\napply/merge buttons for patches and PRs\nhighlgihts and comments on patches and prs\nadded tagged downloads"]],"content":"Signed commit: implemented releases and code serach\nadd contributors to private repos\napply/merge buttons for patches and PRs\nhighlgihts and comments on patches and prs\nadded tagged downloads","id":"e822be2b0fbf3285bbedf9d8f9d1692b5503080af17a4d28941a1dc81c96187c","sig":"70c8b6e499551ce43478116cf694992102a29572d5380cbe3b070a3026bc2c9e35177587712c7414f25d1ca50038c9614479f7758bbdc48f69cc44cd52bf4842"}

276
src/lib/components/NavBar.svelte

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

336
src/routes/+layout.svelte

@ -1,6 +1,6 @@ @@ -1,6 +1,6 @@
<script lang="ts">
import '../app.css';
import { onMount, setContext } from 'svelte';
import { onMount, onDestroy, setContext } from 'svelte';
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import Footer from '$lib/components/Footer.svelte';
@ -16,6 +16,13 @@ @@ -16,6 +16,13 @@
// Accept children as a snippet prop (Svelte 5)
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)
let theme = $state<'gitrepublic-light' | 'gitrepublic-dark' | 'gitrepublic-black'>('gitrepublic-dark');
@ -40,145 +47,242 @@ @@ -40,145 +47,242 @@
// Load theme on mount and watch for changes
onMount(() => {
// Only run client-side code
if (typeof window === 'undefined') return;
if (typeof window === 'undefined' || !isMounted) return;
// Load theme from settings store (async)
(async () => {
if (!isMounted) return;
try {
const settings = await settingsStore.getSettings();
theme = settings.theme;
themeLoaded = true;
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';
if (isMounted) {
theme = settings.theme;
themeLoaded = true;
applyTheme(theme);
// Also sync to localStorage for app.html flash prevention
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)
// Session expiry is handled by user store initialization and NavBar
const currentState = $userStore;
if (currentState.userPubkey && currentState.userPubkeyHex) {
updateActivity();
try {
const currentState = $userStore;
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
// Only check if user store is not already initialized with a logged-in user
if ($page.url.pathname !== '/') {
const currentState = $userStore;
// 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') {
checkUserLevel();
// 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;
// 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
const handlePendingTransfersEvent = (event: Event) => {
const customEvent = event as CustomEvent;
if (customEvent.detail?.transfers) {
// Filter out dismissed transfers
pendingTransfers = customEvent.detail.transfers.filter(
(t: { eventId: string }) => !dismissedTransfers.has(t.eventId)
);
handlePendingTransfersEvent = (event: Event) => {
if (!isMounted) return;
try {
const customEvent = event as CustomEvent;
if (customEvent.detail?.transfers && isMounted) {
// 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
const handleThemeChanged = (event: Event) => {
const customEvent = event as CustomEvent<{ theme: 'gitrepublic-light' | 'gitrepublic-dark' | 'gitrepublic-black' }>;
if (customEvent.detail?.theme) {
theme = customEvent.detail.theme;
// Sync to localStorage for app.html flash prevention
localStorage.setItem('theme', theme);
// Theme will be applied via $effect
handleThemeChanged = (event: Event) => {
if (!isMounted) return;
try {
const customEvent = event as CustomEvent<{ theme: 'gitrepublic-light' | 'gitrepublic-dark' | 'gitrepublic-black' }>;
if (customEvent.detail?.theme && isMounted) {
theme = customEvent.detail.theme;
// 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:
// 1. User store initialization (checks on load)
// 2. NavBar component (checks on mount and periodically)
// 3. Splash page (+page.svelte) (checks on mount)
// No need for redundant checks here
});
// Return cleanup function
return () => {
window.removeEventListener('pendingTransfers', handlePendingTransfersEvent);
window.removeEventListener('themeChanged', handleThemeChanged);
};
onDestroy(() => {
// Mark component as unmounted first
isMounted = false;
// 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() {
// 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
const currentState = $userStore;
if (checkingUserLevel || (currentState.userPubkey && currentState.userLevel !== 'strictly_rate_limited')) {
return;
}
try {
const currentState = $userStore;
if (!currentState || !isMounted) return;
// Only check user level if user has explicitly logged in (has pubkey in store)
// 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
userStore.setUser(null, null, 'strictly_rate_limited', null);
return;
}
if (checkingUserLevel || (currentState.userPubkey && currentState.userLevel !== 'strictly_rate_limited')) {
return;
}
checkingUserLevel = true;
userStore.setChecking(true);
// Only check user level if user has explicitly logged in (has pubkey in store)
// 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 {
// Use pubkey from store (user has explicitly logged in)
const userPubkey = currentState.userPubkey;
const userPubkeyHex = currentState.userPubkeyHex;
// Determine user level
const levelResult = await determineUserLevel(userPubkey, userPubkeyHex);
// 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) {
updateActivity();
// Check for pending transfers
checkPendingTransfers(levelResult.userPubkeyHex);
if (!isMounted) return;
checkingUserLevel = true;
userStore.setChecking(true);
try {
// Use pubkey from store (user has explicitly logged in)
const userPubkey = currentState.userPubkey;
const userPubkeyHex = currentState.userPubkeyHex;
if (!isMounted) {
checkingUserLevel = false;
userStore.setChecking(false);
return;
}
// Determine user level
const levelResult = await determineUserLevel(userPubkey, userPubkeyHex);
if (!isMounted) {
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) {
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 {
checkingUserLevel = false;
userStore.setChecking(false);
// Ignore errors during destruction
if (isMounted) {
console.warn('User level check error:', err);
}
}
}
async function checkPendingTransfers(userPubkeyHex: string) {
if (!isMounted) return;
try {
// Add timeout to prevent hanging
const controller = new AbortController();
@ -193,9 +297,9 @@ @@ -193,9 +297,9 @@
clearTimeout(timeoutId);
if (response.ok) {
if (response.ok && isMounted) {
const data = await response.json();
if (data.pendingTransfers && data.pendingTransfers.length > 0) {
if (data.pendingTransfers && data.pendingTransfers.length > 0 && isMounted) {
// Filter out dismissed transfers
pendingTransfers = data.pendingTransfers.filter(
(t: { eventId: string }) => !dismissedTransfers.has(t.eventId)
@ -203,8 +307,8 @@ @@ -203,8 +307,8 @@
}
}
} catch (err) {
// Only log if it's not an abort (timeout)
if (err instanceof Error && err.name !== 'AbortError') {
// Only log if it's not an abort (timeout) and component is still mounted
if (isMounted && err instanceof Error && err.name !== 'AbortError') {
console.error('Failed to check for pending transfers:', err);
}
// Silently ignore timeouts - they're expected if the server is slow
@ -244,7 +348,7 @@ @@ -244,7 +348,7 @@
// Watch for theme changes and apply them (but only after initial load)
let themeLoaded = $state(false);
$effect(() => {
if (typeof window !== 'undefined' && themeLoaded) {
if (typeof window !== 'undefined' && themeLoaded && isMounted) {
applyTheme(theme);
}
});
@ -278,20 +382,48 @@ @@ -278,20 +382,48 @@
}
// 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
const userState = $derived($userStore);
// Check for transfers when user logs in
$effect(() => {
const currentUser = $userStore;
if (currentUser.userPubkeyHex && !checkingUserLevel) {
checkPendingTransfers(currentUser.userPubkeyHex);
} else if (!currentUser.userPubkeyHex) {
// Clear transfers when user logs out
pendingTransfers = [];
dismissedTransfers.clear();
if (!isMounted || typeof window === 'undefined') return;
try {
const currentUser = $userStore;
if (!currentUser || !isMounted) return;
if (currentUser.userPubkeyHex && !checkingUserLevel && isMounted) {
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 @@ @@ -1,5 +1,5 @@
<script lang="ts">
import { onMount } from 'svelte';
import { onMount, onDestroy } from 'svelte';
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { getPublicKeyWithNIP07, isNIP07Available } from '../lib/services/nostr/nip07-signer.js';
@ -14,100 +14,163 @@ @@ -14,100 +14,163 @@
let checkingLevel = $state(false);
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)
$effect(() => {
const currentUser = $userStore;
if (currentUser.userPubkey && currentUser.userPubkeyHex) {
// User is logged in - sync local state with store
userPubkey = currentUser.userPubkey;
userPubkeyHex = currentUser.userPubkeyHex;
} else {
// User has logged out - clear local state
userPubkey = null;
userPubkeyHex = null;
if (!isMounted || typeof window === 'undefined') return;
try {
const currentUser = $userStore;
if (!currentUser || !isMounted) return;
if (currentUser.userPubkey && currentUser.userPubkeyHex && isMounted) {
// User is logged in - sync local state with store
userPubkey = currentUser.userPubkey;
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(() => {
if (typeof window === 'undefined' || !isMounted) return;
// 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
if (isSessionExpired()) {
// Session expired - logout user
userStore.reset();
checkingAuth = true;
checkAuth();
} else {
// Check userStore first - if user is already logged in, use store values
const currentUser = $userStore;
if (currentUser.userPubkey && currentUser.userPubkeyHex) {
// User is already logged in - use store values
userPubkey = currentUser.userPubkey;
userPubkeyHex = currentUser.userPubkeyHex;
checkingAuth = false;
// Update activity to extend session
updateActivity();
// Don't redirect immediately - let the user see they're logged in
// They can click "View Repositories" or navigate away
} else {
// User not logged in - check if extension is available
checkingAuth = true;
checkAuth();
try {
if (isSessionExpired()) {
// Session expired - logout user
if (isMounted) {
userStore.reset();
checkingAuth = true;
checkAuth();
}
} else if (isMounted) {
// Check userStore first - if user is already logged in, use store values
const currentUser = $userStore;
if (currentUser && currentUser.userPubkey && currentUser.userPubkeyHex) {
// User is already logged in - use store values
userPubkey = currentUser.userPubkey;
userPubkeyHex = currentUser.userPubkeyHex;
checkingAuth = false;
// Update activity to extend session
updateActivity();
// Don't redirect immediately - let the user see they're logged in
// They can click "View Repositories" or navigate away
} 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
return () => {
// Re-enable scrolling when component is destroyed
document.body.style.overflow = '';
};
onDestroy(() => {
// Mark component as unmounted first
isMounted = false;
// 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() {
checkingAuth = true;
if (!isMounted || typeof window === 'undefined') return;
// Check userStore first - if user has logged out, clear state
const currentUser = $userStore;
if (!currentUser.userPubkey) {
userPubkey = null;
userPubkeyHex = null;
checkingAuth = false;
return;
if (isMounted) {
checkingAuth = true;
}
if (isNIP07Available()) {
try {
userPubkey = await getPublicKeyWithNIP07();
// 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
userPubkeyHex = userPubkey.toLowerCase();
} else {
// 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
// Check userStore first - if user has logged out, clear state
try {
const currentUser = $userStore;
if (!currentUser || !currentUser.userPubkey) {
if (isMounted) {
userPubkey = null;
userPubkeyHex = null;
checkingAuth = false;
}
return;
}
if (!isMounted) return;
if (isNIP07Available()) {
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 {
userPubkeyHex = userPubkey; // Assume it's already hex or use as-is
} else if (isMounted) {
// 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) {
console.warn('Failed to load user pubkey:', err);
} else if (isMounted) {
// Extension not available, clear state
userPubkey = null;
userPubkeyHex = null;
}
} else {
// Extension not available, clear state
userPubkey = null;
userPubkeyHex = null;
if (isMounted) {
checkingAuth = false;
}
} catch (err) {
if (isMounted) {
console.warn('Auth check error:', err);
checkingAuth = false;
}
}
checkingAuth = false;
}
async function handleLogin() {

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

@ -24,8 +24,9 @@ @@ -24,8 +24,9 @@
import { hasUnlimitedAccess } from '$lib/utils/user-access.js';
import { fetchUserEmail, fetchUserName } from '$lib/utils/user-profile.js';
// Get page data for OpenGraph metadata - use $derived to make it reactive
const pageData = $derived($page.data as {
// Get page data for OpenGraph metadata - use state to avoid SSR issues
// Guard against SSR - $page store can only be accessed in component context
let pageData = $state<{
title?: string;
description?: string;
image?: string;
@ -33,10 +34,54 @@ @@ -33,10 +34,54 @@
repoUrl?: string;
announcement?: NostrEvent;
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 || '';
const repo = ($page.params as { npub?: string; repo?: string }).repo || '';
// Guard params access during SSR - use state that gets updated reactively
// 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
const repoAnnouncement = $derived(pageData.announcement);
@ -84,6 +129,26 @@ @@ -84,6 +129,26 @@
// 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
let autoSaveInterval: ReturnType<typeof setInterval> | null = null;
@ -92,37 +157,53 @@ @@ -92,37 +157,53 @@
let maintainersEffectRan = $state(false);
$effect(() => {
const data = $page.data as typeof pageData;
const currentRepoKey = `${npub}/${repo}`;
// Reset flags if repo changed
if (currentRepoKey !== lastRepoKey) {
maintainersLoaded = false;
maintainersEffectRan = false;
lastRepoKey = currentRepoKey;
}
// Only load if:
// 1. We have page data
// 2. Effect hasn't run yet for this repo
// 3. We're not currently loading
if ((repoOwnerPubkeyDerived || (repoMaintainers && repoMaintainers.length > 0)) &&
!maintainersEffectRan &&
!loadingMaintainers) {
maintainersEffectRan = true; // Mark as ran to prevent re-running
maintainersLoaded = true; // Set flag before loading to prevent concurrent calls
loadAllMaintainers().catch(err => {
maintainersLoaded = false; // Reset on error so we can retry
maintainersEffectRan = false; // Allow retry
console.warn('Failed to load maintainers:', err);
});
// Guard against SSR and component destruction
if (typeof window === 'undefined' || !isMounted) return;
try {
const data = $page.data as typeof pageData;
if (!data || !isMounted) return;
const currentRepoKey = `${npub}/${repo}`;
// Reset flags if repo changed
if (currentRepoKey !== lastRepoKey && isMounted) {
maintainersLoaded = false;
maintainersEffectRan = false;
lastRepoKey = currentRepoKey;
}
// Only load if:
// 1. We have page data
// 2. Effect hasn't run yet for this repo
// 3. We're not currently loading
// 4. Component is still mounted
if (isMounted &&
(repoOwnerPubkeyDerived || (repoMaintainers && repoMaintainers.length > 0)) &&
!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
$effect(() => {
if (!isMounted) return;
// Check auto-save setting and update interval (async, but don't await)
settingsStore.getSettings().then(settings => {
if (!isMounted) return;
if (settings.autoSave && !autoSaveInterval) {
// Auto-save was enabled, set it up
setupAutoSave();
@ -134,69 +215,110 @@ @@ -134,69 +215,110 @@
}
}
}).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
$effect(() => {
const currentUser = $userStore;
const wasLoggedIn = userPubkey !== null || userPubkeyHex !== null;
if (currentUser.userPubkey && currentUser.userPubkeyHex) {
const wasDifferent = userPubkey !== currentUser.userPubkey || userPubkeyHex !== currentUser.userPubkeyHex;
userPubkey = currentUser.userPubkey;
userPubkeyHex = currentUser.userPubkeyHex;
// Reload data when user logs in or pubkey changes
if (wasDifferent) {
// Reset repoNotFound flag when user logs in, so we can retry loading
repoNotFound = false;
// Clear cached email and name when user changes
if (!isMounted) return;
try {
const currentUser = $userStore;
if (!currentUser || !isMounted) return;
const wasLoggedIn = userPubkey !== null || userPubkeyHex !== null;
if (currentUser.userPubkey && currentUser.userPubkeyHex && isMounted) {
const wasDifferent = userPubkey !== currentUser.userPubkey || userPubkeyHex !== currentUser.userPubkeyHex;
userPubkey = currentUser.userPubkey;
userPubkeyHex = currentUser.userPubkeyHex;
// 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;
cachedUserName = null;
checkMaintainerStatus().catch(err => console.warn('Failed to reload maintainer status after login:', err));
loadBookmarkStatus().catch(err => console.warn('Failed to reload bookmark status after login:', err));
// Reset flags to allow reload
maintainersLoaded = false;
maintainersEffectRan = false;
lastRepoKey = null;
loadAllMaintainers().catch(err => 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(() => {
checkCloneStatus(true).catch(err => console.warn('Failed to recheck clone status after login:', err));
}, 100);
// Reload all repository data with the new user context
if (!loading) {
loadBranches().catch(err => console.warn('Failed to reload branches after login:', 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));
loadTags().catch(err => console.warn('Failed to reload tags after login:', err));
// Reload discussions when user logs in (needs user context for relay selection)
loadDiscussions().catch(err => console.warn('Failed to reload discussions after login:', err));
// Reload data when user logs out to hide private content
if (wasLoggedIn && isMounted) {
checkMaintainerStatus().catch(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
maintainersLoaded = false;
maintainersEffectRan = false;
lastRepoKey = null;
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 (!loading && activeTab === 'files' && isMounted) {
loadFiles().catch(err => {
if (isMounted) console.warn('Failed to reload files after logout:', err);
});
}
}
}
} else {
userPubkey = null;
userPubkeyHex = null;
// Clear cached email and name when user logs out
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));
}
} catch (err) {
// Ignore errors during destruction
if (isMounted) {
console.warn('User store sync error:', err);
}
}
});
@ -282,6 +404,8 @@ @@ -282,6 +404,8 @@
try {
// 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
// Guard against SSR - $page store can only be accessed in component context
if (typeof window === 'undefined') return;
const currentUrl = $page.url;
const host = currentUrl.host; // Includes port if present (e.g., "localhost:5173")
const protocol = currentUrl.protocol.slice(0, -1); // Remove trailing ":"
@ -380,9 +504,10 @@ @@ -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
$effect(() => {
if (!isMounted) return;
if (isRepoCloned === false && !canUseApiFallback && tabs.length > 0) {
const currentTab = tabs.find(t => t.id === activeTab);
if (!currentTab) {
if (!currentTab && isMounted) {
// Current tab requires cloning, switch to first available tab
activeTab = tabs[0].id as typeof activeTab;
}
@ -1992,6 +2117,8 @@ @@ -1992,6 +2117,8 @@
loadingDocs = true;
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
const data = $page.data as typeof pageData;
if (repoIsPrivate) {
@ -2129,6 +2256,8 @@ @@ -2129,6 +2256,8 @@
try {
// Get images from page data (loaded from announcement)
// 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;
if (data.image) {
repoImage = data.image;
@ -2141,6 +2270,8 @@ @@ -2141,6 +2270,8 @@
// Also fetch from announcement directly as fallback (only if not private or user has access)
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;
// Check access for private repos
if (repoIsPrivate) {
@ -2212,15 +2343,25 @@ @@ -2212,15 +2343,25 @@
// Reactively update images when pageData changes (only once, when data becomes available)
$effect(() => {
const data = $page.data as typeof pageData;
// Only update if we have new data and don't already have the images set
if (data.image && data.image !== repoImage) {
repoImage = data.image;
console.log('[Repo Images] Updated image from pageData (reactive):', repoImage);
}
if (data.banner && data.banner !== repoBanner) {
repoBanner = data.banner;
console.log('[Repo Images] Updated banner from pageData (reactive):', repoBanner);
// Guard against SSR and component destruction
if (typeof window === 'undefined' || !isMounted) return;
try {
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
if (data.image && data.image !== repoImage && isMounted) {
repoImage = data.image;
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 @@ @@ -2243,52 +2384,110 @@
}
// Close menu when clicking outside (handled by RepoHeaderEnhanced component)
function handleClickOutside(event: MouseEvent) {
const target = event.target as HTMLElement;
if (showRepoMenu && !target.closest('.repo-header')) {
showRepoMenu = false;
clickOutsideHandler = (event: MouseEvent) => {
if (!isMounted) return;
try {
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();
if (!isMounted) return;
// Skip other API calls if repository doesn't exist
if (repoNotFound) {
loading = false;
return;
}
// loadBranches() already handles setting currentBranch to the default branch
await loadFiles();
if (!isMounted) return;
await checkAuth();
if (!isMounted) return;
await loadTags();
if (!isMounted) return;
await checkMaintainerStatus();
if (!isMounted) return;
await loadBookmarkStatus();
if (!isMounted) return;
await loadAllMaintainers();
if (!isMounted) return;
// Check clone status (needed to disable write operations)
await checkCloneStatus();
if (!isMounted) return;
await checkVerification();
if (!isMounted) return;
await loadReadme();
if (!isMounted) return;
await loadForkInfo();
if (!isMounted) return;
await loadRepoImages();
if (!isMounted) return;
// 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
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
onDestroy(() => {
if (autoSaveInterval) {
clearInterval(autoSaveInterval);
autoSaveInterval = null;
// Mark component as unmounted first to prevent any state updates
isMounted = false;
// Clean up intervals and timeouts
try {
if (autoSaveInterval) {
clearInterval(autoSaveInterval);
autoSaveInterval = null;
}
} catch (err) {
// Ignore errors during cleanup
}
if (readmeAutoLoadTimeout) {
clearTimeout(readmeAutoLoadTimeout);
readmeAutoLoadTimeout = null;
try {
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 @@ @@ -4572,38 +4771,63 @@
// Only load tab content when tab actually changes, not on every render
let lastTab = $state<string | null>(null);
$effect(() => {
if (!isMounted) return;
if (activeTab !== lastTab) {
lastTab = activeTab;
if (!isMounted) return;
if (activeTab === 'files') {
// Files tab - ensure files are loaded and README is shown if available
if (files.length === 0 || currentPath !== '') {
loadFiles('');
} else if (files.length > 0 && !currentFile) {
loadFiles('').catch(err => {
if (isMounted) console.warn('Failed to load files:', err);
});
} else if (files.length > 0 && !currentFile && isMounted) {
// Files already loaded, ensure README is shown
const readmeFile = findReadmeFile(files);
if (readmeFile) {
setTimeout(() => {
loadFile(readmeFile.path);
if (isMounted) {
loadFile(readmeFile.path).catch(err => {
if (isMounted) console.warn('Failed to load README file:', err);
});
}
}, 100);
}
}
} else if (activeTab === 'history') {
loadCommitHistory();
} else if (activeTab === 'tags') {
loadTags();
loadReleases(); // Load releases to check for tag associations
} else if (activeTab === 'history' && isMounted) {
loadCommitHistory().catch(err => {
if (isMounted) console.warn('Failed to load commit history:', err);
});
} 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') {
// Code search is performed on demand, not auto-loaded
} else if (activeTab === 'issues') {
loadIssues();
} else if (activeTab === 'prs') {
loadPRs();
} else if (activeTab === 'docs') {
loadDocumentation();
} else if (activeTab === 'discussions') {
loadDiscussions();
} else if (activeTab === 'patches') {
loadPatches();
} else if (activeTab === 'issues' && isMounted) {
loadIssues().catch(err => {
if (isMounted) console.warn('Failed to load issues:', err);
});
} else if (activeTab === 'prs' && isMounted) {
loadPRs().catch(err => {
if (isMounted) console.warn('Failed to load PRs:', err);
});
} else if (activeTab === 'docs' && isMounted) {
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 @@ @@ -4611,32 +4835,44 @@
// Reload all branch-dependent data when branch changes
let lastBranch = $state<string | null>(null);
$effect(() => {
if (!isMounted) return;
if (currentBranch && currentBranch !== lastBranch) {
lastBranch = currentBranch;
if (!isMounted) return;
// 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
if (activeTab === 'files') {
if (activeTab === 'files' && isMounted) {
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 {
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
if (activeTab === 'history') {
loadCommitHistory().catch(err => console.warn('Failed to reload commit history after branch change:', err));
if (activeTab === 'history' && isMounted) {
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)
if (activeTab === 'docs') {
if (activeTab === 'docs' && isMounted) {
documentationHtml = null;
documentationContent = 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 @@ @@ -4650,7 +4886,7 @@
<meta property="og:type" content="website" />
<meta property="og:title" content={pageData.title || `${repoName} - Repository`} />
<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()}
<meta property="og:image" content={pageData.image || repoImage} />
{/if}

Loading…
Cancel
Save