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 @@
/>
-
+ {@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}
{#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 },