Browse Source

bug-fixes

imwald
Silberengel 3 weeks ago
parent
commit
8708ece0ee
  1. 57
      src/components/ReplyNoteList/index.tsx
  2. 4
      src/components/RssWebFeedCard/index.tsx
  3. 40
      src/lib/event-metadata.ts
  4. 11
      src/lib/thread-reply-root-match.ts

57
src/components/ReplyNoteList/index.tsx

@ -846,8 +846,26 @@ function ReplyNoteList({ @@ -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({ @@ -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

4
src/components/RssWebFeedCard/index.tsx

@ -57,7 +57,7 @@ export default function RssWebFeedCard({ @@ -57,7 +57,7 @@ export default function RssWebFeedCard({
}}
>
<div
className="flex items-center gap-1.5 border-b border-border/40 px-3 py-1.5 text-[11px] sm:text-xs text-muted-foreground"
className="flex min-w-0 items-center gap-1.5 border-b border-border/40 px-3 py-1.5 text-[11px] sm:text-xs text-muted-foreground"
aria-label={hasRealRss ? t('RSS feed item label') : t('Web URL item label')}
>
{hasRealRss ? (
@ -65,7 +65,7 @@ export default function RssWebFeedCard({ @@ -65,7 +65,7 @@ export default function RssWebFeedCard({
) : (
<Globe className="size-3.5 shrink-0 opacity-90" strokeWidth={2} aria-hidden />
)}
<span>{hasRealRss ? t('RSS feed item label') : t('Web URL item label')}</span>
<span className="min-w-0 flex-1 truncate">{canonicalUrl}</span>
</div>
<div className="not-prose max-w-full border-b border-border/60 bg-muted/10 pointer-events-none">

40
src/lib/event-metadata.ts

@ -1,4 +1,4 @@ @@ -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 @@ -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

11
src/lib/thread-reply-root-match.ts

@ -102,6 +102,17 @@ export function eventReplyMatchesThreadRoot(evt: Event, root: TThreadRootRef): b @@ -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') {

Loading…
Cancel
Save