From 37fa84e5bb9b7fbd6794b02e06b6c16aaf672602 Mon Sep 17 00:00:00 2001 From: Silberengel Date: Tue, 3 Feb 2026 09:56:14 +0100 Subject: [PATCH] bug-fixes --- .../content/MarkdownRenderer.svelte | 17 +- .../components/content/QuotedContext.svelte | 135 +++++ .../components/content/ReplyContext.svelte | 84 ++- src/lib/modules/feed/FeedPage.svelte | 546 +++++++++++++----- src/lib/modules/feed/FeedPost.svelte | 39 +- src/lib/modules/feed/ThreadDrawer.svelte | 440 ++++++++++++++ src/lib/modules/feed/ZapReceiptReply.svelte | 16 +- src/lib/services/nostr/nostr-client.ts | 36 +- src/lib/types/kind-lookup.ts | 2 +- 9 files changed, 1141 insertions(+), 174 deletions(-) create mode 100644 src/lib/components/content/QuotedContext.svelte create mode 100644 src/lib/modules/feed/ThreadDrawer.svelte diff --git a/src/lib/components/content/MarkdownRenderer.svelte b/src/lib/components/content/MarkdownRenderer.svelte index 282e9ae..1fa1936 100644 --- a/src/lib/components/content/MarkdownRenderer.svelte +++ b/src/lib/components/content/MarkdownRenderer.svelte @@ -28,7 +28,8 @@ const sortedLinks = [...links].sort((a, b) => b.start - a.start); for (const link of sortedLinks) { - const placeholder = `__NIP21_LINK_${offset}__`; + // Use a unique placeholder that won't be processed by markdown + const placeholder = `\u200B\u200B\u200BNIP21_LINK_${offset}\u200B\u200B\u200B`; const before = processed.slice(0, link.start); const after = processed.slice(link.end); processed = before + placeholder + after; @@ -68,9 +69,14 @@ } } - finalHtml = finalHtml.replace(new RegExp(placeholder, 'g'), replacement); + // Escape placeholder for regex replacement + const escapedPlaceholder = placeholder.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + finalHtml = finalHtml.replace(new RegExp(escapedPlaceholder, 'g'), replacement); } + // Clean up any remaining placeholders (fallback) + finalHtml = finalHtml.replace(/\u200B\u200B\u200BNIP21_LINK_\d+\u200B\u200B\u200B/g, ''); + rendered = finalHtml; }); } else { @@ -103,9 +109,14 @@ } } - finalHtml = finalHtml.replace(new RegExp(placeholder, 'g'), replacement); + // Escape placeholder for regex replacement + const escapedPlaceholder = placeholder.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + finalHtml = finalHtml.replace(new RegExp(escapedPlaceholder, 'g'), replacement); } + // Clean up any remaining placeholders (fallback) + finalHtml = finalHtml.replace(/\u200B\u200B\u200BNIP21_LINK_\d+\u200B\u200B\u200B/g, ''); + rendered = finalHtml; } } else { diff --git a/src/lib/components/content/QuotedContext.svelte b/src/lib/components/content/QuotedContext.svelte new file mode 100644 index 0000000..53dd01f --- /dev/null +++ b/src/lib/components/content/QuotedContext.svelte @@ -0,0 +1,135 @@ + + +
{ + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + scrollToQuoted(); + } + }} +> + Quoting: {getQuotedPreview()} + {#if loadingQuoted} + (loading...) + {/if} +
+ + diff --git a/src/lib/components/content/ReplyContext.svelte b/src/lib/components/content/ReplyContext.svelte index 5cee1b5..5aa4596 100644 --- a/src/lib/components/content/ReplyContext.svelte +++ b/src/lib/components/content/ReplyContext.svelte @@ -1,27 +1,96 @@ @@ -360,6 +549,16 @@ /> Show OPs only + {#if currentPubkey} + + {/if} @@ -383,8 +582,45 @@ {#if item.type === 'post'} {@const parentId = item.event.tags.find((t) => t[0] === 'e' && t[3] === 'reply')?.[1]} {@const parentEvent = parentId ? posts.find(p => p.id === parentId) : undefined} -
- + {@const quotedId = item.event.tags.find((t) => t[0] === 'q')?.[1]} + {@const quotedEvent = quotedId ? posts.find(p => p.id === quotedId) : undefined} +
openThreadDrawer(item.event, e)} + role="button" + tabindex="0" + onkeydown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + openThreadDrawer(item.event); + } + }} + > + { + // Add loaded parent to posts array if not already there + // Don't re-sort - just append to prevent feed jumping + if (!posts.find(p => p.id === event.id)) { + posts = [...posts, event]; + // Invalidate cache + cachedFeedItems = null; + } + }} + onQuotedLoaded={(event) => { + // Add loaded quoted event to posts array if not already there + // Don't re-sort - just append to prevent feed jumping + if (!posts.find(p => p.id === event.id)) { + posts = [...posts, event]; + // Invalidate cache + cachedFeedItems = null; + } + }} + />
{:else if item.type === 'replaceable'}
@@ -399,12 +635,26 @@ {#if !hasMore && getAllFeedItems().length > 0}

No more posts

{/if} - {#if showOPsOnly && getFilteredPosts().length === 0 && posts.length > 0 && replaceableEvents.length === 0} -

No original posts found. Try unchecking "Show OPs only".

+ {#if (showOPsOnly || showResponsesToMe) && getFilteredPosts().length === 0 && posts.length > 0 && replaceableEvents.length === 0} +

+ {#if showResponsesToMe && showOPsOnly} + No original posts responding to you found. Try unchecking the filters. + {:else if showResponsesToMe} + No responses to you found. Try unchecking "Show responses to me". + {:else} + No original posts found. Try unchecking "Show OPs only". + {/if} +

{/if} {/if}
+ + diff --git a/src/lib/modules/feed/ZapReceiptReply.svelte b/src/lib/modules/feed/ZapReceiptReply.svelte index 5fcf6b0..45159f3 100644 --- a/src/lib/modules/feed/ZapReceiptReply.svelte +++ b/src/lib/modules/feed/ZapReceiptReply.svelte @@ -51,6 +51,11 @@ return zapReceipt.pubkey; } + function isReply(): boolean { + // Check if this zap receipt is a reply (has e tag pointing to another event) + return zapReceipt.tags.some((t) => t[0] === 'e' && t[1] !== zapReceipt.id); + } + $effect(() => { if (contentElement) { checkContentHeight(); @@ -82,7 +87,10 @@
{#if parentEvent} - +
+ ⚡ Zapping + +
{/if}
@@ -90,12 +98,6 @@ {getAmount().toLocaleString()} sats {getRelativeTime()} - {#if getZappedPubkey()} - {@const zappedPubkey = getZappedPubkey()!} - - to - - {/if}
{#if zapReceipt.content} diff --git a/src/lib/services/nostr/nostr-client.ts b/src/lib/services/nostr/nostr-client.ts index 642671d..51413d9 100644 --- a/src/lib/services/nostr/nostr-client.ts +++ b/src/lib/services/nostr/nostr-client.ts @@ -19,6 +19,7 @@ class NostrClient { private relays: Map = new Map(); private subscriptions: Map = new Map(); private nextSubId = 1; + private activeFetches: Map> = new Map(); // Track active fetches to prevent duplicates /** * Initialize the client @@ -359,6 +360,16 @@ class NostrClient { ): Promise { const { useCache = true, cacheResults = true, onUpdate } = options || {}; + // Create a key for this fetch to prevent duplicate concurrent requests + const fetchKey = JSON.stringify({ filters, relays: relays.sort() }); + + // Check if there's already an active fetch for this combination + const activeFetch = this.activeFetches.get(fetchKey); + if (activeFetch) { + // Return the existing promise to prevent duplicate requests + return activeFetch; + } + // Query from cache first if enabled if (useCache) { try { @@ -366,17 +377,23 @@ class NostrClient { if (cachedEvents.length > 0) { // Return cached events immediately - if (onUpdate) { - setTimeout(() => onUpdate(cachedEvents), 0); - } + // Don't call onUpdate here - only call it when fresh data arrives + // This prevents duplicate updates that cause feed jumping - // Fetch fresh data in background + // Fetch fresh data in background (only if cacheResults is true) + // Add a delay to prevent immediate background refresh that might cause rate limiting if (cacheResults) { setTimeout(() => { - this.fetchFromRelays(filters, relays, { cacheResults, onUpdate, timeout: options?.timeout }).catch((error) => { + // Use a different key for background refresh to allow it to run + const bgFetchKey = `${fetchKey}_bg_${Date.now()}`; + const bgPromise = this.fetchFromRelays(filters, relays, { cacheResults, onUpdate, timeout: options?.timeout }); + this.activeFetches.set(bgFetchKey, bgPromise); + bgPromise.finally(() => { + this.activeFetches.delete(bgFetchKey); + }).catch((error) => { console.error('Error fetching fresh events from relays:', error); }); - }, 0); + }, 1000); // Delay background refresh by 1 second to reduce concurrent requests } return cachedEvents; @@ -387,7 +404,12 @@ class NostrClient { } // Fetch from relays - return this.fetchFromRelays(filters, relays, { cacheResults, onUpdate, timeout: options?.timeout }); + const fetchPromise = this.fetchFromRelays(filters, relays, { cacheResults, onUpdate, timeout: options?.timeout }); + this.activeFetches.set(fetchKey, fetchPromise); + fetchPromise.finally(() => { + this.activeFetches.delete(fetchKey); + }); + return fetchPromise; } diff --git a/src/lib/types/kind-lookup.ts b/src/lib/types/kind-lookup.ts index cc83f23..3da3212 100644 --- a/src/lib/types/kind-lookup.ts +++ b/src/lib/types/kind-lookup.ts @@ -49,7 +49,7 @@ export const KIND_LOOKUP: Record = { 30315: { number: 30315, description: 'User Status', showInFeed: false, isReplaceable: false, isSecondaryKind: true }, // Zaps - 9735: { number: 9735, description: 'Zap Receipt', showInFeed: true, isReplaceable: false, isSecondaryKind: true }, + 9735: { number: 9735, description: 'Zap Receipt', showInFeed: false, isReplaceable: false, isSecondaryKind: true }, // Relay lists 10002: { number: 10002, description: 'Relay List Metadata', showInFeed: false, isReplaceable: false, isSecondaryKind: false },