+
{loading && }
{zapsForFeed.map((zap) => (
@@ -530,10 +584,13 @@ function ReplyNoteList({
)}
- {replies.slice(0, showCount).map((reply) => {
- if (hideUntrustedInteractions && !isUserTrusted(reply.pubkey)) {
- const repliesForThisReply = repliesMap.get(reply.id)
- // If the reply is not trusted and there are no trusted replies for this reply, skip rendering
+ {mergedFeed.slice(0, showCount).map((item) => {
+ const isQuote = !replyIdSet.has(item.id)
+ // Don't filter by trust until trust data is loaded - prevents replies from
+ // vanishing when wotSet is still empty (all non-self appear untrusted)
+ if (isTrustLoaded && hideUntrustedInteractions && !isUserTrusted(item.pubkey)) {
+ if (isQuote) return null
+ const repliesForThisReply = repliesMap.get(item.id)
if (
!repliesForThisReply ||
repliesForThisReply.events.every((evt) => !isUserTrusted(evt.pubkey))
@@ -542,11 +599,38 @@ function ReplyNoteList({
}
}
+ if (isQuote) {
+ const quoteLabel =
+ item.kind === kinds.Highlights
+ ? t('highlighted this note')
+ : item.kind === kinds.LongFormArticle
+ ? t('cited in article')
+ : t('quoted this note')
+ const hideQuotedNote = eventReferencesEventId(item, event.id)
+ return (
+
+ (replyRefs.current[item.id] = el)}
+ className="scroll-mt-12 border-l-2 border-muted-foreground/40 pl-3 py-1 my-1 rounded-r"
+ >
+
+ {quoteLabel}
+
+
+
+
+ )
+ }
+
+ const reply = item
const parentETag = getParentETag(reply)
const parentEventHexId = parentETag?.[1]
const parentEventId = parentETag ? generateBech32IdFromETag(parentETag) : undefined
- // Check if this reply belongs to the same thread as the root event
const replyRootId = getRootEventHexId(reply)
const belongsToSameThread = rootInfo && (
(rootInfo.type === 'E' && replyRootId === rootInfo.id) ||
@@ -572,17 +656,12 @@ function ReplyNoteList({
highlightReply(parentEventHexId)
}}
onClickReply={belongsToSameThread ? (replyEvent) => {
- // Update URL without full navigation
const replyNoteUrl = toNote(replyEvent.id)
window.history.pushState(null, '', replyNoteUrl)
-
- // Ensure the reply is visible by expanding the list if needed
- const replyIndex = replies.findIndex(r => r.id === replyEvent.id)
+ const replyIndex = mergedFeed.findIndex((r) => r.id === replyEvent.id)
if (replyIndex >= 0 && replyIndex >= showCount) {
setShowCount(replyIndex + 1)
}
-
- // Highlight and scroll to the reply (use setTimeout to ensure DOM is updated)
setTimeout(() => {
highlightReply(replyEvent.id, true)
}, 50)
@@ -593,14 +672,14 @@ function ReplyNoteList({
)
})}
- {!loading && (
+ {quoteLoading && showQuotes &&
}
+ {!loading && !quoteLoading && (
- {replies.length > 0 ? t('no more replies') : t('no replies')}
+ {mergedFeed.length > 0 ? t('no more replies') : t('no replies')}
)}
{loading &&
}
- {showQuotes &&
}
)
}
diff --git a/src/constants.ts b/src/constants.ts
index b29fbffe..3bbf318b 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -169,6 +169,12 @@ export const KIND_1_BLOCKED_RELAY_URLS = [
'wss://wikifreedia.xyz'
]
+/** Relays that reject #e (and similar) tag filters; skip for reply/quote/stats fetches. */
+export const E_TAG_FILTER_BLOCKED_RELAY_URLS = [
+ 'wss://nostr.v0l.io',
+ 'wss://nostr.sovbit.host'
+]
+
// Optimized relay list for read operations (includes aggregator)
export const FAST_READ_RELAY_URLS = [
'wss://theforest.nostr1.com',
diff --git a/src/contexts/suppress-embedded-note-context.tsx b/src/contexts/suppress-embedded-note-context.tsx
new file mode 100644
index 00000000..8393fe3e
--- /dev/null
+++ b/src/contexts/suppress-embedded-note-context.tsx
@@ -0,0 +1,8 @@
+import { createContext, useContext } from 'react'
+
+/** When set, EmbeddedNote should not render notes whose id matches this (avoids redundancy when viewing "quotes of this note"). */
+export const SuppressEmbeddedNoteContext = createContext