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