From c4a7f1675925aea80be5b427df5f138fe7e0f142 Mon Sep 17 00:00:00 2001 From: Silberengel Date: Thu, 5 Feb 2026 14:43:03 +0100 Subject: [PATCH] more bug-fixes --- src/app.css | 8 +- .../components/content/EmbeddedEvent.svelte | 13 + .../components/content/FileExplorer.svelte | 655 ++++++++++++++ .../content/MarkdownRenderer.svelte | 108 ++- .../preferences/UserPreferences.svelte | 809 +++++++----------- src/lib/components/write/FindEventForm.svelte | 15 + .../modules/discussions/DiscussionList.svelte | 55 +- src/lib/modules/feed/FeedPage.svelte | 228 ++++- src/lib/modules/feed/FeedPost.svelte | 83 +- src/lib/services/cache/event-cache.ts | 47 + src/routes/find/+page.svelte | 33 +- src/routes/repos/+page.svelte | 49 +- src/routes/repos/[naddr]/+page.svelte | 536 +++++++++--- src/routes/rss/+page.svelte | 396 ++++++++- src/routes/topics/+page.svelte | 135 ++- src/routes/topics/[name]/+page.svelte | 96 ++- 16 files changed, 2531 insertions(+), 735 deletions(-) create mode 100644 src/lib/components/content/FileExplorer.svelte diff --git a/src/app.css b/src/app.css index 7af3d0f..f1addb8 100644 --- a/src/app.css +++ b/src/app.css @@ -6,21 +6,21 @@ @tailwind utilities; :root { - --text-size: 16px; + --text-size: 12px; --line-height: 1.6; --content-width: 800px; } [data-text-size='small'] { - --text-size: 12px; + --text-size: 10px; } [data-text-size='medium'] { - --text-size: 14px; + --text-size: 12px; } [data-text-size='large'] { - --text-size: 16px; + --text-size: 14px; } [data-line-spacing='tight'] { diff --git a/src/lib/components/content/EmbeddedEvent.svelte b/src/lib/components/content/EmbeddedEvent.svelte index ff8152d..70c35fb 100644 --- a/src/lib/components/content/EmbeddedEvent.svelte +++ b/src/lib/components/content/EmbeddedEvent.svelte @@ -113,12 +113,25 @@ } catch (e) { console.error('Failed to decode event ID:', e); error = true; + loading = false; + loadingEvent = false; return; } } if (!hexId) { error = true; + loading = false; + loadingEvent = false; + return; + } + + // Validate hexId is exactly 64 characters (proper event ID length) + if (hexId.length !== 64 || !/^[0-9a-f]{64}$/i.test(hexId)) { + console.warn('Invalid hex event ID length or format:', hexId); + error = true; + loading = false; + loadingEvent = false; return; } diff --git a/src/lib/components/content/FileExplorer.svelte b/src/lib/components/content/FileExplorer.svelte new file mode 100644 index 0000000..ffa1b4e --- /dev/null +++ b/src/lib/components/content/FileExplorer.svelte @@ -0,0 +1,655 @@ + + +
+
+
+

Files

+ + +
+
+ {#each Object.entries(fileTree) as [name, value]} + {@const isFile = value && typeof value === 'object' && 'path' in value} + {@const isDir = value && typeof value === 'object' && !('path' in value)} + {#if isDir} + {@const dirPath = name} + {@const isExpandedDir = isExpanded(dirPath)} +
+ + {#if isExpandedDir} +
+ {#each Object.entries(value).sort(([a, valA], [b, valB]) => { + const aIsFile = valA && typeof valA === 'object' && 'path' in valA; + const bIsFile = valB && typeof valB === 'object' && 'path' in valB; + const aIsDir = valA && typeof valA === 'object' && !('path' in valA); + const bIsDir = valB && typeof valB === 'object' && !('path' in valB); + if (aIsDir && !bIsDir) return -1; + if (!aIsDir && bIsDir) return 1; + return a.localeCompare(b); + }) as [subName, subValue]} + {#if subValue && typeof subValue === 'object' && 'path' in subValue} + {@const file = subValue as GitFile} +
+ +
+ {:else if subValue && typeof subValue === 'object'} + {@const subDirPath = `${dirPath}/${subName}`} + {@const isExpandedSubDir = isExpanded(subDirPath)} +
+ + {#if isExpandedSubDir} +
+ + {#each Object.entries(subValue).sort(([a, valA], [b, valB]) => { + const aIsFile = valA && typeof valA === 'object' && 'path' in valA; + const bIsFile = valB && typeof valB === 'object' && 'path' in valB; + const aIsDir = valA && typeof valA === 'object' && !('path' in valA); + const bIsDir = valB && typeof valB === 'object' && !('path' in valB); + if (aIsDir && !bIsDir) return -1; + if (!aIsDir && bIsDir) return 1; + return a.localeCompare(b); + }) as [nestedName, nestedValue]} + {#if nestedValue && typeof nestedValue === 'object' && 'path' in nestedValue} + {@const nestedFile = nestedValue as GitFile} +
+ +
+ {:else if nestedValue && typeof nestedValue === 'object'} + {@const deeperDirPath = `${subDirPath}/${nestedName}`} + {@const isExpandedDeeper = isExpanded(deeperDirPath)} +
+ + {#if isExpandedDeeper} +
+ {#each Object.entries(nestedValue).sort(([a, valA], [b, valB]) => { + const aIsFile = valA && typeof valA === 'object' && 'path' in valA; + const bIsFile = valB && typeof valB === 'object' && 'path' in valB; + const aIsDir = valA && typeof valA === 'object' && !('path' in valA); + const bIsDir = valB && typeof valB === 'object' && !('path' in valB); + if (aIsDir && !bIsDir) return -1; + if (!aIsDir && bIsDir) return 1; + return a.localeCompare(b); + }) as [deepName, deepValue]} + {#if deepValue && typeof deepValue === 'object' && 'path' in deepValue} + {@const deepFile = deepValue as GitFile} +
+ +
+ {:else} +
+ 📁 + {deepName}/ + (deeper nesting not fully expanded) +
+ {/if} + {/each} +
+ {/if} +
+ {/if} + {/each} +
+ {/if} +
+ {/if} + {/each} +
+ {/if} +
+ {:else if isFile} + {@const file = value as GitFile} +
+ +
+ {/if} + {/each} +
+
+ +
+ {#if loadingContent} +
+

Loading file content...

+
+ {:else if contentError} +
+

Error: {contentError}

+
+ {:else if selectedFile && fileContent !== null} +
+

{selectedFile.path}

+ {formatFileSize(selectedFile.size)} +
+
+
{fileContent}
+
+ {:else} +
+

Select a file to view its contents

+
+ {/if} +
+
+ + diff --git a/src/lib/components/content/MarkdownRenderer.svelte b/src/lib/components/content/MarkdownRenderer.svelte index bbe42d0..cdc74df 100644 --- a/src/lib/components/content/MarkdownRenderer.svelte +++ b/src/lib/components/content/MarkdownRenderer.svelte @@ -14,6 +14,7 @@ // Lazy load EmbeddedEvent component (heavy component) - will be loaded on demand let EmbeddedEventComponent: any = null; let embeddedEventLoading = $state(false); + let mountingEmbeddedEvents = $state(false); // Guard for mounting, separate from loading component interface Props { content: string; @@ -391,7 +392,8 @@ for (let i = eventLinks.length - 1; i >= 0; i--) { const link = eventLinks[i]; const eventId = getEventIdFromNIP21(link.parsed); - if (eventId) { + // Validate event ID before creating placeholder to prevent invalid fetches + if (eventId && isValidNostrId(eventId)) { // Escape event ID to prevent XSS const escapedEventId = escapeHtml(eventId); // Create a div element for embedded event cards (block-level) @@ -755,44 +757,66 @@ // Mount EmbeddedEvent components after rendering (lazy loaded) async function mountEmbeddedEvents() { - if (!containerRef) return; + if (!containerRef || mountingEmbeddedEvents) return; // Find all event placeholders and mount EmbeddedEvent components const placeholders = containerRef.querySelectorAll('[data-nostr-event]:not([data-mounted])'); if (placeholders.length > 0) { - console.debug(`Mounting ${placeholders.length} EmbeddedEvent components`); - - // Load component only when we have placeholders to mount - const Component = await loadEmbeddedEventComponent(); - if (!Component) { - console.warn('Failed to load EmbeddedEvent component'); - return; - } - - placeholders.forEach((placeholder) => { - const eventId = placeholder.getAttribute('data-event-id'); - if (eventId) { - placeholder.setAttribute('data-mounted', 'true'); + mountingEmbeddedEvents = true; + try { + // Validate event IDs before mounting to prevent invalid fetches + const validPlaceholders: Element[] = []; + placeholders.forEach((placeholder) => { + const eventId = placeholder.getAttribute('data-event-id'); + // Use strict validation to prevent invalid fetches + if (eventId && isValidNostrId(eventId)) { + validPlaceholders.push(placeholder); + } else if (eventId) { + // Invalid event ID - mark as mounted to prevent retries + placeholder.setAttribute('data-mounted', 'true'); + placeholder.textContent = ''; // Don't show invalid IDs + console.debug('Skipping invalid event ID in MarkdownRenderer:', eventId); + } + }); + + if (validPlaceholders.length > 0) { + console.debug(`Mounting ${validPlaceholders.length} EmbeddedEvent components`); - try { - // Clear and mount component - placeholder.innerHTML = ''; - // Mount EmbeddedEvent component - it will decode and fetch the event - const instance = mountComponent(placeholder as HTMLElement, Component as any, { eventId }); - - if (!instance) { - console.warn('EmbeddedEvent mount returned null', { eventId }); - // Fallback: show the event ID - placeholder.textContent = eventId.slice(0, 20) + '...'; - } - } catch (error) { - console.error('Error mounting EmbeddedEvent:', error, { eventId }); - // Show fallback - placeholder.textContent = eventId.slice(0, 20) + '...'; + // Load component only when we have placeholders to mount + const Component = await loadEmbeddedEventComponent(); + if (!Component) { + console.warn('Failed to load EmbeddedEvent component'); + return; } + + validPlaceholders.forEach((placeholder) => { + const eventId = placeholder.getAttribute('data-event-id'); + if (eventId) { + placeholder.setAttribute('data-mounted', 'true'); + + try { + // Clear and mount component + placeholder.innerHTML = ''; + // Mount EmbeddedEvent component - it will decode and fetch the event + const instance = mountComponent(placeholder as HTMLElement, Component as any, { eventId }); + + if (!instance) { + console.warn('EmbeddedEvent mount returned null', { eventId }); + // Fallback: show the event ID + placeholder.textContent = eventId.slice(0, 20) + '...'; + } + } catch (error) { + console.error('Error mounting EmbeddedEvent:', error, { eventId }); + // Show fallback + placeholder.textContent = eventId.slice(0, 20) + '...'; + } + } + }); } - }); + } finally { + mountingEmbeddedEvents = false; + } } } @@ -814,12 +838,22 @@ }); // Also use MutationObserver to catch any placeholders added later + // Debounce to prevent excessive re-mounts + let mutationDebounceTimeout: ReturnType | null = null; + $effect(() => { if (!containerRef) return; const observer = new MutationObserver(() => { - mountProfileBadges(); - mountEmbeddedEvents(); + // Debounce mutations to prevent excessive re-mounts + if (mutationDebounceTimeout) { + clearTimeout(mutationDebounceTimeout); + } + mutationDebounceTimeout = setTimeout(() => { + mountProfileBadges(); + mountEmbeddedEvents(); + mutationDebounceTimeout = null; + }, 300); // 300ms debounce }); observer.observe(containerRef, { @@ -827,7 +861,13 @@ subtree: true }); - return () => observer.disconnect(); + return () => { + observer.disconnect(); + if (mutationDebounceTimeout) { + clearTimeout(mutationDebounceTimeout); + mutationDebounceTimeout = null; + } + }; }); diff --git a/src/lib/components/preferences/UserPreferences.svelte b/src/lib/components/preferences/UserPreferences.svelte index a3bd738..a2e1a89 100644 --- a/src/lib/components/preferences/UserPreferences.svelte +++ b/src/lib/components/preferences/UserPreferences.svelte @@ -1,7 +1,11 @@ + -{#if showPreferences} - + +{#if open}
(showPreferences = false)} - role="presentation" - >
- - -