diff --git a/src/components/ReplyNoteList/index.tsx b/src/components/ReplyNoteList/index.tsx
index 3cea481b..f0f2d207 100644
--- a/src/components/ReplyNoteList/index.tsx
+++ b/src/components/ReplyNoteList/index.tsx
@@ -846,8 +846,26 @@ function ReplyNoteList({
filters.push(...buildRssArticleUrlThreadInteractionFilters(rootInfo.id, LIMIT))
}
+ // For URL threads: stream events as they arrive from each relay so replies appear
+ // immediately, rather than waiting up to 10 s for all relays to EOSE.
+ const urlThreadRootInfo = rootInfo.type === 'I' ? rootInfo : null
+ const urlThreadOnevent = urlThreadRootInfo
+ ? (evt: NEvent) => {
+ if (fetchGeneration !== replyFetchGenRef.current) return
+ if (!isRssArticleUrlThreadInteraction(evt, urlThreadRootInfo.id)) return
+ if (shouldHideThreadResponseEvent(evt, mutePubkeySet, hideContentMentioningMutedUsers))
+ return
+ addReplies([evt])
+ if (!hasCache) setLoading(false)
+ }
+ : undefined
+
// Use fetchEvents instead of subscribeTimeline for one-time fetching
- const allReplies = await queryService.fetchEvents(finalRelayUrls, filters)
+ const allReplies = await queryService.fetchEvents(
+ finalRelayUrls,
+ filters,
+ urlThreadOnevent ? { onevent: urlThreadOnevent } : undefined
+ )
if (fetchGeneration !== replyFetchGenRef.current) return
@@ -887,6 +905,43 @@ function ReplyNoteList({
// No cache: stop loading after adding replies
setLoading(false)
}
+
+ // Second pass for URL threads: fetch replies to individual comments that may omit the
+ // root I tag (non-NIP-22-compliant clients). NoteStats counts them via #e; without this
+ // pass they appear as reply counts only, with no actual content shown.
+ if (rootInfo.type === 'I' && regularReplies.length > 0) {
+ const commentKinds = [
+ ExtendedKind.COMMENT,
+ ExtendedKind.VOICE_COMMENT,
+ kinds.ShortTextNote
+ ]
+ const parentIds = regularReplies
+ .filter((evt) => commentKinds.includes(evt.kind))
+ .map((evt) => evt.id)
+ if (parentIds.length > 0) {
+ const nestedFilters: Filter[] = [
+ { '#e': parentIds, kinds: commentKinds, limit: LIMIT }
+ ]
+ const nestedReplies = await queryService.fetchEvents(finalRelayUrls, nestedFilters, {
+ onevent: (evt: NEvent) => {
+ if (fetchGeneration !== replyFetchGenRef.current) return
+ if (shouldHideThreadResponseEvent(evt, mutePubkeySet, hideContentMentioningMutedUsers))
+ return
+ addReplies([evt])
+ }
+ })
+ if (fetchGeneration !== replyFetchGenRef.current) return
+ const validNested = nestedReplies.filter(
+ (evt) =>
+ !shouldHideThreadResponseEvent(evt, mutePubkeySet, hideContentMentioningMutedUsers)
+ )
+ if (validNested.length > 0) {
+ discussionFeedCache.setCachedReplies(rootInfo, validNested)
+ const merged = discussionFeedCache.getCachedReplies(rootInfo)
+ addReplies(merged ?? validNested)
+ }
+ }
+ }
} catch (error) {
logger.error('[ReplyNoteList] Error fetching replies:', error)
if (fetchGeneration !== replyFetchGenRef.current) return
diff --git a/src/components/RssWebFeedCard/index.tsx b/src/components/RssWebFeedCard/index.tsx
index 3c33c2f6..b8a993be 100644
--- a/src/components/RssWebFeedCard/index.tsx
+++ b/src/components/RssWebFeedCard/index.tsx
@@ -57,7 +57,7 @@ export default function RssWebFeedCard({
}}
>
{hasRealRss ? (
@@ -65,7 +65,7 @@ export default function RssWebFeedCard({
) : (
)}
- {hasRealRss ? t('RSS feed item label') : t('Web URL item label')}
+ {canonicalUrl}
diff --git a/src/lib/event-metadata.ts b/src/lib/event-metadata.ts
index a215cd64..563fbb0f 100644
--- a/src/lib/event-metadata.ts
+++ b/src/lib/event-metadata.ts
@@ -1,4 +1,4 @@
-import { FAST_READ_RELAY_URLS, POLL_TYPE } from '@/constants'
+import { ExtendedKind, FAST_READ_RELAY_URLS, POLL_TYPE } from '@/constants'
import { TEmoji, TMailboxRelay, TPollType, TRelayList, TRelaySet, TPaymentInfo, TProfile } from '@/types'
import { Event, kinds } from 'nostr-tools'
import { buildATag } from './draft-event'
@@ -343,7 +343,43 @@ export function getRelaySetFromEvent(event: Event, blockedRelays?: string[]): TR
}
export function getZapInfoFromEvent(receiptEvent: Event) {
- if (receiptEvent.kind !== kinds.Zap) return null
+ if (receiptEvent.kind !== kinds.Zap && receiptEvent.kind !== ExtendedKind.ZAP_REQUEST) return null
+
+ // Kind 9734 — zap request: all data is directly on the event (no bolt11, no description wrapper).
+ if (receiptEvent.kind === ExtendedKind.ZAP_REQUEST) {
+ const senderPubkey = receiptEvent.pubkey
+ let recipientPubkey: string | undefined
+ let originalEventId: string | undefined
+ let eventId: string | undefined
+ let amount: number | undefined
+ const comment = receiptEvent.content || undefined
+ try {
+ receiptEvent.tags.forEach((tag) => {
+ const [tagName, tagValue] = tag
+ switch (tagName) {
+ case 'p':
+ recipientPubkey = tagValue
+ break
+ case 'e':
+ case 'E':
+ originalEventId = tag[1]
+ eventId = generateBech32IdFromETag(tag)
+ break
+ case 'a':
+ originalEventId = tag[1]
+ eventId = generateBech32IdFromATag(tag)
+ break
+ case 'amount':
+ if (tagValue) amount = Math.floor(parseInt(tagValue, 10) / 1000)
+ break
+ }
+ })
+ if (!recipientPubkey || !amount) return null
+ return { senderPubkey, recipientPubkey, eventId, originalEventId, invoice: undefined, amount, comment, preimage: undefined }
+ } catch {
+ return null
+ }
+ }
let senderPubkey: string | undefined
let recipientPubkey: string | undefined
diff --git a/src/lib/thread-reply-root-match.ts b/src/lib/thread-reply-root-match.ts
index 53e2c30a..7499c99f 100644
--- a/src/lib/thread-reply-root-match.ts
+++ b/src/lib/thread-reply-root-match.ts
@@ -102,6 +102,17 @@ export function eventReplyMatchesThreadRoot(evt: Event, root: TThreadRootRef): b
const hu = getHighlightSourceHttpUrl(evt)
return !!hu && canonicalizeRssArticleUrl(hu) === canonicalizeRssArticleUrl(root.id)
}
+ // Some clients omit the root I tag on nested replies. Walk one level up via the session
+ // cache: if the declared root or direct parent is a URL-thread comment, accept this event.
+ const urlMatchesRoot = (hexId: string | undefined): boolean => {
+ if (!hexId || !/^[0-9a-f]{64}$/i.test(hexId)) return false
+ const ancestor = client.peekSessionCachedEvent(hexId.toLowerCase())
+ if (!ancestor) return false
+ const aUrl = getArticleUrlFromCommentITags(ancestor)
+ return !!aUrl && canonicalizeRssArticleUrl(aUrl) === canonicalizeRssArticleUrl(root.id)
+ }
+ if (urlMatchesRoot(getRootEventHexId(evt))) return true
+ if (urlMatchesRoot(getParentEventHexId(evt))) return true
return false
}
if (root.type === 'A') {