diff --git a/src/components/NoteStats/TopZaps.tsx b/src/components/NoteStats/TopZaps.tsx
deleted file mode 100644
index 65d6d5a1..00000000
--- a/src/components/NoteStats/TopZaps.tsx
+++ /dev/null
@@ -1,57 +0,0 @@
-import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area'
-import { useNoteStatsById } from '@/hooks/useNoteStatsById'
-import { formatAmount } from '@/lib/lightning'
-import { Zap } from 'lucide-react'
-import { Event } from 'nostr-tools'
-import { useMemo, useState } from 'react'
-import { SimpleUserAvatar } from '../UserAvatar'
-import ZapDialog from '../ZapDialog'
-
-export default function TopZaps({ event }: { event: Event }) {
- const noteStats = useNoteStatsById(event.id)
- const [zapIndex, setZapIndex] = useState(-1)
- const topZaps = useMemo(() => {
- return noteStats?.zaps?.sort((a, b) => b.amount - a.amount).slice(0, 10) || []
- }, [noteStats])
-
- if (!topZaps.length) return null
-
- return (
-
-
- {topZaps.map((zap, index) => (
-
{
- e.stopPropagation()
- setZapIndex(index)
- }}
- >
-
-
-
{formatAmount(zap.amount)}
-
{zap.comment}
-
e.stopPropagation()}>
- {
- if (open) {
- setZapIndex(index)
- } else {
- setZapIndex(-1)
- }
- }}
- pubkey={event.pubkey}
- event={event}
- defaultAmount={zap.amount}
- defaultComment={zap.comment}
- />
-
-
- ))}
-
-
-
- )
-}
diff --git a/src/components/NoteStats/index.tsx b/src/components/NoteStats/index.tsx
index 4d6f4544..4d2c9d63 100644
--- a/src/components/NoteStats/index.tsx
+++ b/src/components/NoteStats/index.tsx
@@ -15,7 +15,6 @@ import Likes from './Likes'
import ReplyButton from './ReplyButton'
import RepostButton from './RepostButton'
import SeenOnButton from './SeenOnButton'
-import TopZaps from './TopZaps'
import ZapButton from './ZapButton'
export default function NoteStats({
@@ -76,7 +75,6 @@ export default function NoteStats({
e.stopPropagation()}>
{displayTopZapsAndLikes && (
<>
- {!isRssArticleRoot &&
}
{/* Kind 11: LikeButton already shows ⬆️/⬇️; Likes row would duplicate those pills */}
{!isDiscussion && !isRssArticleRoot &&
}
>
@@ -105,7 +103,6 @@ export default function NoteStats({
e.stopPropagation()}>
{displayTopZapsAndLikes && (
<>
- {!isRssArticleRoot && }
{!isDiscussion && !isRssArticleRoot && }
>
)}
diff --git a/src/components/ReplyNoteList/index.tsx b/src/components/ReplyNoteList/index.tsx
index 7c4579d5..847db41e 100644
--- a/src/components/ReplyNoteList/index.tsx
+++ b/src/components/ReplyNoteList/index.tsx
@@ -230,9 +230,10 @@ function ReplyNoteList({
const zapsForFeed = useMemo(() => {
if (shouldHideInteractions(event)) return []
const raw = noteStats?.zaps ?? []
+ const nonZero = raw.filter((z) => z.amount > 0) // Suppress 0 sat zaps (spam)
const filtered =
- isTrustLoaded && hideUntrustedInteractions ? raw.filter((z) => isUserTrusted(z.pubkey)) : raw
- return [...filtered].sort((a, b) => b.amount - a.amount)
+ isTrustLoaded && hideUntrustedInteractions ? nonZero.filter((z) => isUserTrusted(z.pubkey)) : nonZero
+ return [...filtered].sort((a, b) => b.amount - a.amount) // Largest to smallest
}, [event, noteStats, isTrustLoaded, hideUntrustedInteractions, isUserTrusted])
const [timelineKey] = useState(undefined)
diff --git a/src/services/note-stats.service.ts b/src/services/note-stats.service.ts
index 500a70ad..16539d45 100644
--- a/src/services/note-stats.service.ts
+++ b/src/services/note-stats.service.ts
@@ -1,10 +1,10 @@
-import { ExtendedKind, FAST_READ_RELAY_URLS } from '@/constants'
+import { E_TAG_FILTER_BLOCKED_RELAY_URLS, ExtendedKind, SEARCHABLE_RELAY_URLS } from '@/constants'
import { getReplaceableCoordinateFromEvent, isReplaceableEvent } from '@/lib/event'
import { getZapInfoFromEvent } from '@/lib/event-metadata'
import logger from '@/lib/logger'
import { getEmojiInfosFromEmojiTags, getFirstHexEventIdFromETags, tagNameEquals } from '@/lib/tag'
import { normalizeUrl } from '@/lib/url'
-import { eventService } from '@/services/client.service'
+import client, { eventService } from '@/services/client.service'
import { TEmoji } from '@/types'
import dayjs from 'dayjs'
import { Event, Filter, kinds } from 'nostr-tools'
@@ -113,8 +113,7 @@ class NoteStatsService {
since = oldStats.updatedAt
}
- // Use optimized relay selection - fewer relays, better performance
- const finalRelayUrls = this.getOptimizedRelayList()
+ const finalRelayUrls = await this.buildNoteStatsRelayList(event)
const replaceableCoordinate = isReplaceableEvent(event.kind)
? getReplaceableCoordinateFromEvent(event)
@@ -145,14 +144,41 @@ class NoteStatsService {
}
}
- private getOptimizedRelayList(): string[] {
- // Use only FAST_READ_RELAY_URLS for optimal performance
- const normalizedRelays = FAST_READ_RELAY_URLS
- .map(url => normalizeUrl(url))
- .filter((url): url is string => !!url)
- .slice(0, 2) // Limit to 2 relays for better performance and reduced load
-
- return Array.from(new Set(normalizedRelays))
+ /**
+ * Build relay list for note stats: search relays + relay(s) event was seen on + author's inboxes, deduplicated.
+ * Excludes E_TAG_FILTER_BLOCKED_RELAY_URLS (stats use #e filters).
+ */
+ private async buildNoteStatsRelayList(event: Event): Promise {
+ const blocked = new Set(
+ E_TAG_FILTER_BLOCKED_RELAY_URLS.map((u) => (normalizeUrl(u) || u).toLowerCase()).filter(Boolean)
+ )
+ const seen = new Set()
+
+ const add = (url: string | undefined) => {
+ if (!url) return
+ const n = normalizeUrl(url)
+ if (!n || blocked.has(n.toLowerCase()) || seen.has(n)) return
+ seen.add(n)
+ }
+
+ // 1. Search relays
+ SEARCHABLE_RELAY_URLS.forEach(add)
+
+ // 2. Relay(s) where the event was seen
+ client.getSeenEventRelayUrls(event.id).forEach(add)
+
+ // 3. Author's inboxes (read relays from kind 10002)
+ try {
+ const relayList = await Promise.race([
+ client.fetchRelayList(event.pubkey),
+ new Promise<{ read?: string[] }>((r) => setTimeout(() => r({}), 2000))
+ ])
+ ;(relayList?.read ?? []).slice(0, 10).forEach(add)
+ } catch {
+ // ignore
+ }
+
+ return Array.from(seen)
}
private buildFilters(event: Event, replaceableCoordinate?: string, since?: number): Filter[] {
@@ -162,6 +188,11 @@ class NoteStatsService {
kinds: [kinds.Reaction, kinds.Repost, kinds.ShortTextNote, ExtendedKind.COMMENT, ExtendedKind.VOICE_COMMENT, kinds.Highlights],
limit: 50 // Reduced limit for better performance
},
+ {
+ '#e': [event.id],
+ kinds: [kinds.Zap],
+ limit: 100
+ },
{
'#q': [event.id],
kinds: [kinds.ShortTextNote, ExtendedKind.COMMENT, ExtendedKind.VOICE_COMMENT],
@@ -176,6 +207,11 @@ class NoteStatsService {
kinds: [kinds.Reaction, kinds.Repost, kinds.ShortTextNote, ExtendedKind.COMMENT, ExtendedKind.VOICE_COMMENT, kinds.Highlights],
limit: 50
},
+ {
+ '#a': [replaceableCoordinate],
+ kinds: [kinds.Zap],
+ limit: 100
+ },
{
'#q': [replaceableCoordinate],
kinds: [kinds.ShortTextNote, ExtendedKind.COMMENT, ExtendedKind.VOICE_COMMENT],
@@ -379,6 +415,7 @@ class NoteStatsService {
if (!info) return
const { originalEventId, senderPubkey, invoice, amount, comment } = info
if (!originalEventId || !senderPubkey) return
+ if (!amount || amount <= 0) return // Suppress 0 sat zaps (spam)
if (originalEventAuthor && originalEventAuthor === senderPubkey) {
return