Browse Source

more efficient nostr stats

imwald
Silberengel 5 months ago
parent
commit
c9f3d8fc07
  1. 6
      src/components/NormalFeed/index.tsx
  2. 24
      src/components/NoteList/index.tsx
  3. 144
      src/components/ReplyNoteList/index.tsx
  4. 22
      src/components/TrendingNotes/index.tsx
  5. 2
      src/constants.ts
  6. 23
      src/pages/primary/DiscussionsPage/index.tsx
  7. 360
      src/services/note-stats.service.ts

6
src/components/NormalFeed/index.tsx

@ -25,7 +25,11 @@ const NormalFeed = forwardRef<TNoteListRef, {
const { hideUntrustedNotes } = useUserTrust() const { hideUntrustedNotes } = useUserTrust()
const { showKinds } = useKindFilter() const { showKinds } = useKindFilter()
const [temporaryShowKinds, setTemporaryShowKinds] = useState(showKinds) const [temporaryShowKinds, setTemporaryShowKinds] = useState(showKinds)
const [listMode, setListMode] = useState<TNoteListMode>(() => storage.getNoteListMode()) const [listMode, setListMode] = useState<TNoteListMode>(() => {
const storedMode = storage.getNoteListMode()
// Default to 'posts' (Notes tab) for main feed, not replies
return storedMode || 'posts'
})
const supportTouch = useMemo(() => isTouchDevice(), []) const supportTouch = useMemo(() => isTouchDevice(), [])
const internalNoteListRef = useRef<TNoteListRef>(null) const internalNoteListRef = useRef<TNoteListRef>(null)
const noteListRef = ref || internalNoteListRef const noteListRef = ref || internalNoteListRef

24
src/components/NoteList/index.tsx

@ -160,11 +160,16 @@ const NoteList = forwardRef(
useImperativeHandle(ref, () => ({ scrollToTop, refresh }), []) useImperativeHandle(ref, () => ({ scrollToTop, refresh }), [])
useEffect(() => { useEffect(() => {
console.log('🚀 [NoteList] useEffect triggered:', {
subRequests: subRequests.length,
showKinds: showKinds.length,
refreshCount
})
logger.debug('NoteList useEffect:', { subRequests, subRequestsLength: subRequests.length }) logger.debug('NoteList useEffect:', { subRequests, subRequestsLength: subRequests.length })
if (!subRequests.length) return if (!subRequests.length) return
async function init() { async function init() {
console.log('🔄 [NoteList] Initializing feed...')
setLoading(true) setLoading(true)
setEvents([]) setEvents([])
setNewEvents([]) setNewEvents([])
@ -197,8 +202,15 @@ const NoteList = forwardRef(
})), })),
{ {
onEvents: (events, eosed) => { onEvents: (events, eosed) => {
console.log('📥 [NoteList] Received events:', {
eventsCount: events.length,
eosed,
loading,
hasMore
})
logger.debug('NoteList received events:', { eventsCount: events.length, eosed }) logger.debug('NoteList received events:', { eventsCount: events.length, eosed })
if (events.length > 0) { if (events.length > 0) {
console.log('✅ [NoteList] Setting events and stopping loading')
setEvents(events) setEvents(events)
// Stop loading as soon as we have events, don't wait for all relays // Stop loading as soon as we have events, don't wait for all relays
setLoading(false) setLoading(false)
@ -207,6 +219,7 @@ const NoteList = forwardRef(
setHasMore(false) setHasMore(false)
} }
if (eosed) { if (eosed) {
console.log('🏁 [NoteList] EOSED - setting loading false, hasMore:', events.length > 0)
setLoading(false) setLoading(false)
setHasMore(events.length > 0) setHasMore(events.length > 0)
} }
@ -329,6 +342,14 @@ const NoteList = forwardRef(
}, 0) }, 0)
} }
console.log('🎨 [NoteList] Rendering with state:', {
eventsCount: events.length,
filteredEventsCount: filteredEvents.length,
loading,
hasMore,
showKinds: showKinds.length
})
const list = ( const list = (
<div className="min-h-screen"> <div className="min-h-screen">
{customHeader} {customHeader}
@ -349,6 +370,7 @@ const NoteList = forwardRef(
) : ( ) : (
<div className="flex justify-center w-full mt-2"> <div className="flex justify-center w-full mt-2">
<Button size="lg" onClick={() => { <Button size="lg" onClick={() => {
console.log('🔄 [NoteList] Reload button clicked, refreshing feed')
// Clear relay connection state to force fresh connections // Clear relay connection state to force fresh connections
const relayUrls = subRequests.flatMap(req => req.urls) const relayUrls = subRequests.flatMap(req => req.urls)
client.clearRelayConnectionState(relayUrls) client.clearRelayConnectionState(relayUrls)

144
src/components/ReplyNoteList/index.tsx

@ -13,13 +13,14 @@ import logger from '@/lib/logger'
import { toNote } from '@/lib/link' import { toNote } from '@/lib/link'
import { generateBech32IdFromETag, tagNameEquals } from '@/lib/tag' import { generateBech32IdFromETag, tagNameEquals } from '@/lib/tag'
import { normalizeUrl } from '@/lib/url' import { normalizeUrl } from '@/lib/url'
import { useSmartNoteNavigation, useSecondaryPage } from '@/PageManager' import { useSmartNoteNavigation } from '@/PageManager'
import { useContentPolicy } from '@/providers/ContentPolicyProvider' import { useContentPolicy } from '@/providers/ContentPolicyProvider'
import { useMuteList } from '@/providers/MuteListProvider' import { useMuteList } from '@/providers/MuteListProvider'
import { useNostr } from '@/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import { useReply } from '@/providers/ReplyProvider' import { useReply } from '@/providers/ReplyProvider'
import { useFeed } from '@/providers/FeedProvider' import { useFeed } from '@/providers/FeedProvider'
import { useUserTrust } from '@/providers/UserTrustProvider' import { useUserTrust } from '@/providers/UserTrustProvider'
import { useUserPreferences } from '@/providers/UserPreferencesProvider'
import client from '@/services/client.service' import client from '@/services/client.service'
import noteStatsService from '@/services/note-stats.service' import noteStatsService from '@/services/note-stats.service'
import { Filter, Event as NEvent, kinds } from 'nostr-tools' import { Filter, Event as NEvent, kinds } from 'nostr-tools'
@ -36,17 +37,17 @@ type TRootInfo =
const LIMIT = 100 const LIMIT = 100
const SHOW_COUNT = 10 const SHOW_COUNT = 10
function ReplyNoteList({ index, event, sort = 'oldest' }: { index?: number; event: NEvent; sort?: 'newest' | 'oldest' | 'top' | 'controversial' | 'most-zapped' }) { function ReplyNoteList({ index: _index, event, sort = 'oldest' }: { index?: number; event: NEvent; sort?: 'newest' | 'oldest' | 'top' | 'controversial' | 'most-zapped' }) {
console.log('[ReplyNoteList] Component rendered for event:', event.id.substring(0, 8)) console.log('[ReplyNoteList] Component rendered for event:', event.id.substring(0, 8))
const { t } = useTranslation() const { t } = useTranslation()
const { navigateToNote } = useSmartNoteNavigation() const { navigateToNote } = useSmartNoteNavigation()
const { currentIndex } = useSecondaryPage()
const { hideUntrustedInteractions, isUserTrusted } = useUserTrust() const { hideUntrustedInteractions, isUserTrusted } = useUserTrust()
const { mutePubkeySet } = useMuteList() const { mutePubkeySet } = useMuteList()
const { hideContentMentioningMutedUsers } = useContentPolicy() const { hideContentMentioningMutedUsers } = useContentPolicy()
const { relayList: userRelayList } = useNostr() const { relayList: userRelayList } = useNostr()
const { relayUrls: currentFeedRelays } = useFeed() const { relayUrls: currentFeedRelays } = useFeed()
const { showRecommendedRelaysPanel } = useUserPreferences()
const [rootInfo, setRootInfo] = useState<TRootInfo | undefined>(undefined) const [rootInfo, setRootInfo] = useState<TRootInfo | undefined>(undefined)
const { repliesMap, addReplies } = useReply() const { repliesMap, addReplies } = useReply()
@ -106,7 +107,7 @@ function ReplyNoteList({ index, event, sort = 'oldest' }: { index?: number; even
// This prevents the doom loop that was causing "too many concurrent REQS" // This prevents the doom loop that was causing "too many concurrent REQS"
const events = parentEventKeys.flatMap((id) => repliesMap.get(id)?.events || []) const events = parentEventKeys.flatMap((id) => repliesMap.get(id)?.events || [])
logger.debug('[ReplyNoteList] Processing replies:', { console.log('🔍 [ReplyNoteList] Processing replies:', {
eventId: event.id.substring(0, 8), eventId: event.id.substring(0, 8),
parentEventKeys, parentEventKeys,
eventsFromMap: events.length, eventsFromMap: events.length,
@ -115,16 +116,22 @@ function ReplyNoteList({ index, event, sort = 'oldest' }: { index?: number; even
}) })
events.forEach((evt) => { events.forEach((evt) => {
if (replyIdSet.has(evt.id)) return if (replyIdSet.has(evt.id)) {
console.log('🔍 [ReplyNoteList] Skipping duplicate event:', evt.id.substring(0, 8))
return
}
if (mutePubkeySet.has(evt.pubkey)) { if (mutePubkeySet.has(evt.pubkey)) {
console.log('🔍 [ReplyNoteList] Skipping muted user event:', evt.id.substring(0, 8), 'pubkey:', evt.pubkey.substring(0, 8))
return return
} }
if (hideContentMentioningMutedUsers && isMentioningMutedUsers(evt, mutePubkeySet)) { if (hideContentMentioningMutedUsers && isMentioningMutedUsers(evt, mutePubkeySet)) {
console.log('🔍 [ReplyNoteList] Skipping event mentioning muted users:', evt.id.substring(0, 8))
return return
} }
replyIdSet.add(evt.id) replyIdSet.add(evt.id)
replyEvents.push(evt) replyEvents.push(evt)
console.log('✅ [ReplyNoteList] Added reply event:', evt.id.substring(0, 8), 'kind:', evt.kind)
}) })
@ -169,6 +176,9 @@ function ReplyNoteList({ index, event, sort = 'oldest' }: { index?: number; even
return replyEvents.sort((a, b) => b.created_at - a.created_at) return replyEvents.sort((a, b) => b.created_at - a.created_at)
} }
}, [event.id, repliesMap, mutePubkeySet, hideContentMentioningMutedUsers, sort]) }, [event.id, repliesMap, mutePubkeySet, hideContentMentioningMutedUsers, sort])
// Debug the final replies count
console.log('📊 [ReplyNoteList] Final replies count:', replies.length)
const [timelineKey, setTimelineKey] = useState<string | undefined>(undefined) const [timelineKey, setTimelineKey] = useState<string | undefined>(undefined)
const [until, setUntil] = useState<number | undefined>(undefined) const [until, setUntil] = useState<number | undefined>(undefined)
const [loading, setLoading] = useState<boolean>(false) const [loading, setLoading] = useState<boolean>(false)
@ -226,6 +236,7 @@ function ReplyNoteList({ index, event, sort = 'oldest' }: { index?: number; even
rootInfo: root, rootInfo: root,
eventKind: event.kind eventKind: event.kind
}) })
console.log('🏗 [ReplyNoteList] Setting rootInfo:', root)
setRootInfo(root) setRootInfo(root)
} }
fetchRootEvent() fetchRootEvent()
@ -253,26 +264,23 @@ function ReplyNoteList({ index, event, sort = 'oldest' }: { index?: number; even
}, [rootInfo, onNewReply]) }, [rootInfo, onNewReply])
useEffect(() => { useEffect(() => {
console.log('[ReplyNoteList] Main useEffect triggered:', { console.log('[ReplyNoteList] Main useEffect triggered:', {
loading, loading,
hasRootInfo: !!rootInfo, hasRootInfo: !!rootInfo,
currentIndex, shouldInit: !loading && !!rootInfo,
index, rootInfo
shouldInit: !loading && !!rootInfo && currentIndex === index
}) })
if (loading || !rootInfo || currentIndex !== index) { if (loading || !rootInfo) {
console.log('[ReplyNoteList] Early return - conditions not met:', { console.log('[ReplyNoteList] Early return - conditions not met:', {
loading, loading,
hasRootInfo: !!rootInfo, hasRootInfo: !!rootInfo,
currentIndex,
index,
rootInfo rootInfo
}) })
return return
} }
console.log('[ReplyNoteList] All conditions met, starting reply fetch...') console.log('[ReplyNoteList] All conditions met, starting reply fetch...')
// Clear any existing timeout to prevent multiple simultaneous requests // Clear any existing timeout to prevent multiple simultaneous requests
if (requestTimeoutRef.current) { if (requestTimeoutRef.current) {
@ -294,31 +302,26 @@ function ReplyNoteList({ index, event, sort = 'oldest' }: { index?: number; even
try { try {
// Use current feed's relay selection - if user selected a specific relay, use only that // For replies, always use a comprehensive relay list to ensure we find replies
// Don't rely on currentFeedRelays as it might be limited to a single relay
let finalRelayUrls: string[] let finalRelayUrls: string[]
console.log('[ReplyNoteList] Current feed relays:', currentFeedRelays) console.log('[ReplyNoteList] Current feed relays:', currentFeedRelays)
if (currentFeedRelays.length > 0) { // Always build comprehensive relay list for replies to ensure we find them
// Use the current feed's relay selection (respects user's choice of single relay) const userReadRelays = userRelayList?.read || []
finalRelayUrls = currentFeedRelays.map(url => normalizeUrl(url) || url).filter(Boolean) const userWriteRelays = userRelayList?.write || []
console.log('[ReplyNoteList] Using current feed relays:', finalRelayUrls) const eventHints = client.getEventHints(event.id)
} else {
// Fallback: build comprehensive relay list only if no feed relays are set const allRelays = [
const userReadRelays = userRelayList?.read || [] ...userReadRelays.map(url => normalizeUrl(url) || url),
const userWriteRelays = userRelayList?.write || [] ...userWriteRelays.map(url => normalizeUrl(url) || url),
const eventHints = client.getEventHints(event.id) ...eventHints.map(url => normalizeUrl(url) || url),
...FAST_READ_RELAY_URLS.map(url => normalizeUrl(url) || url),
const allRelays = [ ]
...userReadRelays.map(url => normalizeUrl(url) || url),
...userWriteRelays.map(url => normalizeUrl(url) || url), finalRelayUrls = Array.from(new Set(allRelays.filter(Boolean)))
...eventHints.map(url => normalizeUrl(url) || url), console.log('[ReplyNoteList] Using comprehensive relay list for replies:', finalRelayUrls)
...FAST_READ_RELAY_URLS.map(url => normalizeUrl(url) || url),
]
finalRelayUrls = Array.from(new Set(allRelays.filter(Boolean)))
console.log('[ReplyNoteList] Using fallback relay list:', finalRelayUrls)
}
logger.debug('[ReplyNoteList] Fetching replies for event:', { logger.debug('[ReplyNoteList] Fetching replies for event:', {
eventId: event.id.substring(0, 8), eventId: event.id.substring(0, 8),
@ -384,13 +387,17 @@ function ReplyNoteList({ index, event, sort = 'oldest' }: { index?: number; even
eosed, eosed,
eventIds: evts.map(e => e.id.substring(0, 8)) eventIds: evts.map(e => e.id.substring(0, 8))
}) })
console.log('📥 [ReplyNoteList] Received events:', evts.length, 'eosed:', eosed)
if (evts.length > 0) { if (evts.length > 0) {
const regularReplies = evts.filter((evt) => isReplyNoteEvent(evt)) const regularReplies = evts.filter((evt) => isReplyNoteEvent(evt))
logger.debug('[ReplyNoteList] Filtered replies:', { console.log('🔍 [ReplyNoteList] Filtered replies:', {
replyCount: regularReplies.length, replyCount: regularReplies.length,
replyIds: regularReplies.map(r => r.id.substring(0, 8)) replyIds: regularReplies.map(r => r.id.substring(0, 8))
}) })
console.log('➕ [ReplyNoteList] Adding replies to map:', regularReplies.length)
addReplies(regularReplies) addReplies(regularReplies)
} else {
console.log('❌ [ReplyNoteList] No events received')
} }
if (eosed) { if (eosed) {
setUntil(evts.length >= LIMIT ? evts[evts.length - 1].created_at - 1 : undefined) setUntil(evts.length >= LIMIT ? evts[evts.length - 1].created_at - 1 : undefined)
@ -434,7 +441,7 @@ function ReplyNoteList({ index, event, sort = 'oldest' }: { index?: number; even
clearTimeout(requestTimeoutRef.current) clearTimeout(requestTimeoutRef.current)
} }
} }
}, [rootInfo, currentIndex, index, onNewReply]) }, [rootInfo, onNewReply, loading])
useEffect(() => { useEffect(() => {
// Only try to load more if we have no replies, not loading, have a timeline key, and haven't reached the end // Only try to load more if we have no replies, not loading, have a timeline key, and haven't reached the end
@ -531,6 +538,16 @@ function ReplyNoteList({ index, event, sort = 'oldest' }: { index?: number; even
const parentETag = getParentETag(reply) const parentETag = getParentETag(reply)
const parentEventHexId = parentETag?.[1] const parentEventHexId = parentETag?.[1]
const parentEventId = parentETag ? generateBech32IdFromETag(parentETag) : undefined const parentEventId = parentETag ? generateBech32IdFromETag(parentETag) : undefined
// Debug logging for parent event detection
logger.debug('[ReplyNoteList] Reply parent info:', {
replyId: reply.id.substring(0, 8),
parentETag,
parentEventHexId: parentEventHexId?.substring(0, 8),
parentEventId: parentEventId?.substring(0, 8),
isDifferentFromCurrent: event.id !== parentEventHexId,
currentEventId: event.id.substring(0, 8)
})
return ( return (
<div <div
ref={(el) => (replyRefs.current[reply.id] = el)} ref={(el) => (replyRefs.current[reply.id] = el)}
@ -541,12 +558,57 @@ function ReplyNoteList({ index, event, sort = 'oldest' }: { index?: number; even
event={reply} event={reply}
parentEventId={event.id !== parentEventHexId ? parentEventId : undefined} parentEventId={event.id !== parentEventHexId ? parentEventId : undefined}
onClickParent={() => { onClickParent={() => {
if (!parentEventHexId) return logger.debug('[ReplyNoteList] onClickParent called:', {
if (replies.every((r) => r.id !== parentEventHexId)) { parentEventHexId: parentEventHexId?.substring(0, 8),
navigateToNote(toNote(parentEventId ?? parentEventHexId)) parentEventId: parentEventId?.substring(0, 8),
showRecommendedRelaysPanel,
repliesCount: replies.length,
parentInReplies: !replies.every((r) => r.id !== parentEventHexId)
})
if (!parentEventHexId) {
logger.debug('[ReplyNoteList] No parentEventHexId, returning early')
return
}
// First, try to highlight the parent if it's already in the replies
if (!replies.every((r) => r.id !== parentEventHexId)) {
logger.debug('[ReplyNoteList] Parent found in replies, highlighting:', parentEventHexId.substring(0, 8))
highlightReply(parentEventHexId)
return return
} }
highlightReply(parentEventHexId)
// If parent is not in current replies, we need to fetch it
// In single-panel mode, we should expand the thread to show the parent
// rather than navigating away from the current thread
if (!showRecommendedRelaysPanel) {
// Single-panel mode: fetch and add the parent to the thread
// This will expand the current thread to show the parent
logger.debug('[ReplyNoteList] Single-panel mode: fetching parent event')
const fetchAndAddParent = async () => {
try {
logger.debug('[ReplyNoteList] Fetching parent event:', parentEventId ?? parentEventHexId)
const parentEvent = await client.fetchEvent(parentEventId ?? parentEventHexId)
if (parentEvent) {
logger.debug('[ReplyNoteList] Parent event fetched, adding to replies:', parentEvent.id.substring(0, 8))
addReplies([parentEvent])
// Highlight the parent after it's added
setTimeout(() => highlightReply(parentEvent.id), 100)
} else {
logger.debug('[ReplyNoteList] Parent event not found')
}
} catch (error) {
logger.debug('[ReplyNoteList] Failed to fetch parent event:', error)
// Fallback to navigation if fetch fails
navigateToNote(toNote(parentEventId ?? parentEventHexId))
}
}
fetchAndAddParent()
} else {
// Double-panel mode: navigate to parent in secondary panel
logger.debug('[ReplyNoteList] Double-panel mode: navigating to parent')
navigateToNote(toNote(parentEventId ?? parentEventHexId))
}
}} }}
highlight={highlightReplyId === reply.id} highlight={highlightReplyId === reply.id}
/> />

22
src/components/TrendingNotes/index.tsx

@ -10,7 +10,7 @@ import { useNostr } from '@/providers/NostrProvider'
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
import { useZap } from '@/providers/ZapProvider' import { useZap } from '@/providers/ZapProvider'
import noteStatsService from '@/services/note-stats.service' import noteStatsService from '@/services/note-stats.service'
import { BIG_RELAY_URLS, FAST_READ_RELAY_URLS } from '@/constants' import { FAST_READ_RELAY_URLS } from '@/constants'
import logger from '@/lib/logger' import logger from '@/lib/logger'
import { normalizeUrl } from '@/lib/url' import { normalizeUrl } from '@/lib/url'
@ -197,9 +197,13 @@ export default function TrendingNotes() {
if (relayList?.read) { if (relayList?.read) {
relays.push(...relayList.read) relays.push(...relayList.read)
} }
// If user has no favorites and no read relays, fallback to FAST_READ_RELAY_URLS
if (relays.length === 0) {
relays.push(...FAST_READ_RELAY_URLS)
}
} else { } else {
// User is not logged in: BIG_RELAY_URLS + FAST_READ_RELAY_URLS // User is not logged in: use FAST_READ_RELAY_URLS (includes all BIG_RELAY_URLS)
relays.push(...BIG_RELAY_URLS)
relays.push(...FAST_READ_RELAY_URLS) relays.push(...FAST_READ_RELAY_URLS)
} }
@ -278,10 +282,14 @@ export default function TrendingNotes() {
logger.debug('[TrendingNotes] Starting cache initialization with', relays.length, 'relays:', relays) logger.debug('[TrendingNotes] Starting cache initialization with', relays.length, 'relays:', relays)
// 1. Fetch top-level posts from last 24 hours - batch requests to avoid overwhelming relays // 1. Fetch top-level posts from last 24 hours from ALL relays for comprehensive statistics
// Relay list: If user logged in = favoriteRelays + user's read relays (fallback to FAST_READ_RELAY_URLS), else = FAST_READ_RELAY_URLS
const batchSize = 3 // Process 3 relays at a time const batchSize = 3 // Process 3 relays at a time
const recentEvents: NostrEvent[] = [] const recentEvents: NostrEvent[] = []
logger.debug('[TrendingNotes] Using full relay set for comprehensive statistics:', relays.length, 'relays')
logger.debug('[TrendingNotes] Relay source:', pubkey ? 'user favorites + read relays (or FAST_READ_RELAY_URLS fallback)' : 'FAST_READ_RELAY_URLS')
for (let i = 0; i < relays.length; i += batchSize) { for (let i = 0; i < relays.length; i += batchSize) {
const batch = relays.slice(i, i + batchSize) const batch = relays.slice(i, i + batchSize)
logger.debug('[TrendingNotes] Processing batch', Math.floor(i/batchSize) + 1, 'of', Math.ceil(relays.length/batchSize), 'relays:', batch) logger.debug('[TrendingNotes] Processing batch', Math.floor(i/batchSize) + 1, 'of', Math.ceil(relays.length/batchSize), 'relays:', batch)
@ -307,13 +315,13 @@ export default function TrendingNotes() {
// Add a small delay between batches to be respectful to relays // Add a small delay between batches to be respectful to relays
if (i + batchSize < relays.length) { if (i + batchSize < relays.length) {
await new Promise(resolve => setTimeout(resolve, 100)) await new Promise(resolve => setTimeout(resolve, 200))
} }
} }
allEvents.push(...recentEvents) allEvents.push(...recentEvents)
// 2. Fetch events from bookmark/pin lists (with rate limiting) // 2. Fetch events from bookmark/pin lists (with rate limiting) - use full relay list
if (listEventIds.length > 0) { if (listEventIds.length > 0) {
try { try {
const bookmarkPinEvents = await client.fetchEvents(relays, { const bookmarkPinEvents = await client.fetchEvents(relays, {
@ -326,7 +334,7 @@ export default function TrendingNotes() {
} }
} }
// 3. Fetch pin list if user is logged in // 3. Fetch pin list if user is logged in - use full relay list
if (pubkey) { if (pubkey) {
try { try {
const pinListEvent = await client.fetchPinListEvent(pubkey) const pinListEvent = await client.fetchPinListEvent(pubkey)

2
src/constants.ts

@ -73,6 +73,8 @@ export const FAST_READ_RELAY_URLS = [
'wss://theforest.nostr1.com', 'wss://theforest.nostr1.com',
'wss://orly-relay.imwald.eu', 'wss://orly-relay.imwald.eu',
'wss://nostr.wine', 'wss://nostr.wine',
'wss://nostr.land',
'wss://nostr21.com',
'wss://thecitadel.nostr1.com', 'wss://thecitadel.nostr1.com',
'wss://aggr.nostr.land' 'wss://aggr.nostr.land'
] ]

23
src/pages/primary/DiscussionsPage/index.tsx

@ -238,13 +238,13 @@ function analyzeDynamicTopics(entries: EventMapEntry[]): {
const allTopics = [...mainTopics, ...subtopics] const allTopics = [...mainTopics, ...subtopics]
// Debug logging // Debug logging (commented out to reduce console spam)
console.log('Dynamic topics analysis:', { // console.log('Dynamic topics analysis:', {
hashtagCounts: Object.fromEntries(hashtagCounts), // hashtagCounts: Object.fromEntries(hashtagCounts),
mainTopics: mainTopics.map(t => ({ id: t.id, count: t.count })), // mainTopics: mainTopics.map(t => ({ id: t.id, count: t.count })),
subtopics: subtopics.map(t => ({ id: t.id, count: t.count })), // subtopics: subtopics.map(t => ({ id: t.id, count: t.count })),
allTopics: allTopics.map(t => ({ id: t.id, count: t.count, isMainTopic: t.isMainTopic, isSubtopic: t.isSubtopic })) // allTopics: allTopics.map(t => ({ id: t.id, count: t.count, isMainTopic: t.isMainTopic, isSubtopic: t.isSubtopic }))
}) // })
return { mainTopics, subtopics, allTopics } return { mainTopics, subtopics, allTopics }
} }
@ -440,9 +440,12 @@ const DiscussionsPage = forwardRef(() => {
} }
}) })
// Analyze dynamic topics // Analyze dynamic topics only if we have new data
const dynamicTopicsAnalysis = analyzeDynamicTopics(Array.from(newEventMap.values())) let dynamicTopicsAnalysis: { mainTopics: DynamicTopic[]; subtopics: DynamicTopic[]; allTopics: DynamicTopic[] } = { mainTopics: [], subtopics: [], allTopics: [] }
setDynamicTopics(dynamicTopicsAnalysis) if (newEventMap.size > 0) {
dynamicTopicsAnalysis = analyzeDynamicTopics(Array.from(newEventMap.values()))
setDynamicTopics(dynamicTopicsAnalysis)
}
// Update event map with enhanced topic categorization // Update event map with enhanced topic categorization
const updatedEventMap = new Map<string, EventMapEntry>() const updatedEventMap = new Map<string, EventMapEntry>()

360
src/services/note-stats.service.ts

@ -1,4 +1,4 @@
import { BIG_RELAY_URLS, ExtendedKind, SEARCHABLE_RELAY_URLS, FAST_READ_RELAY_URLS } from '@/constants' import { ExtendedKind, FAST_READ_RELAY_URLS } from '@/constants'
import { getReplaceableCoordinateFromEvent, isReplaceableEvent } from '@/lib/event' import { getReplaceableCoordinateFromEvent, isReplaceableEvent } from '@/lib/event'
import { getZapInfoFromEvent } from '@/lib/event-metadata' import { getZapInfoFromEvent } from '@/lib/event-metadata'
import logger from '@/lib/logger' import logger from '@/lib/logger'
@ -29,8 +29,14 @@ class NoteStatsService {
static instance: NoteStatsService static instance: NoteStatsService
private noteStatsMap: Map<string, Partial<TNoteStats>> = new Map() private noteStatsMap: Map<string, Partial<TNoteStats>> = new Map()
private noteStatsSubscribers = new Map<string, Set<() => void>>() private noteStatsSubscribers = new Map<string, Set<() => void>>()
private processingCache = new Set<string>() // Prevent duplicate processing private processingCache = new Set<string>()
private lastProcessedTime = new Map<string, number>() // Rate limiting private lastProcessedTime = new Map<string, number>()
// Batch processing
private pendingEvents = new Set<string>()
private batchTimeout: NodeJS.Timeout | null = null
private readonly BATCH_DELAY = 1000 // 1 second batch delay
private readonly MAX_BATCH_SIZE = 10 // Process up to 10 events at once
constructor() { constructor() {
if (!NoteStatsService.instance) { if (!NoteStatsService.instance) {
@ -39,87 +45,126 @@ class NoteStatsService {
return NoteStatsService.instance return NoteStatsService.instance
} }
async fetchNoteStats(event: Event, pubkey?: string | null, favoriteRelays?: string[]) { async fetchNoteStats(event: Event, _pubkey?: string | null, _favoriteRelays?: string[]) {
const eventId = event.id const eventId = event.id
// Rate limiting: Don't process the same event more than once per 5 seconds // Rate limiting: Don't process the same event more than once per 10 seconds
const now = Date.now() const now = Date.now()
const lastProcessed = this.lastProcessedTime.get(eventId) const lastProcessed = this.lastProcessedTime.get(eventId)
if (lastProcessed && now - lastProcessed < 5000) { if (lastProcessed && now - lastProcessed < 10000) {
logger.debug('[NoteStats] Skipping duplicate fetch for event', eventId.substring(0, 8), 'too soon') logger.debug('[NoteStats] Skipping duplicate fetch for event', eventId.substring(0, 8), 'too soon')
return return
} }
// Prevent concurrent processing of the same event // Add to batch processing queue
this.pendingEvents.add(eventId)
this.lastProcessedTime.set(eventId, now)
// Clear existing timeout and set new one
if (this.batchTimeout) {
clearTimeout(this.batchTimeout)
}
this.batchTimeout = setTimeout(() => {
this.processBatch()
}, this.BATCH_DELAY)
// If we have enough events or this is urgent, process immediately
if (this.pendingEvents.size >= this.MAX_BATCH_SIZE) {
this.processBatch()
}
}
private async processBatch() {
if (this.pendingEvents.size === 0) return
const eventsToProcess = Array.from(this.pendingEvents).slice(0, this.MAX_BATCH_SIZE)
this.pendingEvents.clear()
if (this.batchTimeout) {
clearTimeout(this.batchTimeout)
this.batchTimeout = null
}
console.log('[NoteStats] Processing batch of', eventsToProcess.length, 'events')
// Process all events in the batch
await Promise.all(eventsToProcess.map(eventId => this.processSingleEvent(eventId)))
}
private async processSingleEvent(eventId: string) {
if (this.processingCache.has(eventId)) { if (this.processingCache.has(eventId)) {
logger.debug('[NoteStats] Skipping concurrent fetch for event', eventId.substring(0, 8)) logger.debug('[NoteStats] Skipping concurrent fetch for event', eventId.substring(0, 8))
return return
} }
this.processingCache.add(eventId) this.processingCache.add(eventId)
this.lastProcessedTime.set(eventId, now)
try { try {
// Get the event from cache or fetch it
const event = await this.getEventById(eventId)
if (!event) {
logger.debug('[NoteStats] Event not found:', eventId.substring(0, 8))
return
}
const oldStats = this.noteStatsMap.get(eventId) const oldStats = this.noteStatsMap.get(eventId)
let since: number | undefined let since: number | undefined
if (oldStats?.updatedAt) { if (oldStats?.updatedAt) {
since = oldStats.updatedAt since = oldStats.updatedAt
} }
// Privacy: Only use current user's relays + defaults, never connect to other users' relays
const [relayList, authorProfile] = await Promise.all([ // Use optimized relay selection - fewer relays, better performance
pubkey ? client.fetchRelayList(pubkey) : Promise.resolve({ write: [], read: [] }), const finalRelayUrls = this.getOptimizedRelayList()
client.fetchProfile(event.pubkey)
]) const replaceableCoordinate = isReplaceableEvent(event.kind)
? getReplaceableCoordinateFromEvent(event)
// Build comprehensive relay list: user's inboxes + user's favorite relays + big relays : undefined
// For anonymous users, also include fast read relays for better coverage
const allRelays = [ const filters: Filter[] = this.buildFilters(event, replaceableCoordinate, since)
...(relayList.read || []), // User's inboxes (kind 10002)
...(favoriteRelays || []), // User's favorite relays (kind 10012) const events: Event[] = []
...BIG_RELAY_URLS, // Big relays logger.debug('[NoteStats] Fetching stats for event', event.id.substring(0, 8), 'from', finalRelayUrls.length, 'relays')
...(pubkey ? [] : [...SEARCHABLE_RELAY_URLS, ...FAST_READ_RELAY_URLS]) // Fast read relays for anonymous users only
] await client.fetchEvents(finalRelayUrls, filters, {
onevent: (evt) => {
// Normalize and deduplicate relay URLs this.updateNoteStatsByEvents([evt], event.pubkey)
const normalizedRelays = allRelays events.push(evt)
}
})
logger.debug('[NoteStats] Fetched', events.length, 'events for stats')
this.noteStatsMap.set(event.id, {
...(this.noteStatsMap.get(event.id) ?? {}),
updatedAt: dayjs().unix()
})
} finally {
this.processingCache.delete(eventId)
}
}
private getOptimizedRelayList(): string[] {
// Use only FAST_READ_RELAY_URLS for optimal performance
const normalizedRelays = FAST_READ_RELAY_URLS
.map(url => normalizeUrl(url)) .map(url => normalizeUrl(url))
.filter((url): url is string => !!url) .filter((url): url is string => !!url)
const finalRelayUrls = Array.from(new Set(normalizedRelays)) return Array.from(new Set(normalizedRelays))
const relayTypes = pubkey }
? 'inboxes kind 10002 + favorites kind 10012 + big relays'
: 'big relays + fast read relays + searchable relays (anonymous user)'
logger.debug('[NoteStats] Using', finalRelayUrls.length, 'relays for stats (' + relayTypes + '):', finalRelayUrls)
const replaceableCoordinate = isReplaceableEvent(event.kind)
? getReplaceableCoordinateFromEvent(event)
: undefined
private buildFilters(event: Event, replaceableCoordinate?: string, since?: number): Filter[] {
const filters: Filter[] = [ const filters: Filter[] = [
{ {
'#e': [event.id], '#e': [event.id],
kinds: [kinds.Reaction], kinds: [kinds.Reaction, kinds.Repost, kinds.ShortTextNote, ExtendedKind.COMMENT, ExtendedKind.VOICE_COMMENT, kinds.Highlights],
limit: 100 limit: 50 // Reduced limit for better performance
},
{
'#e': [event.id],
kinds: [kinds.Repost],
limit: 100
},
{
'#e': [event.id],
kinds: [kinds.ShortTextNote, ExtendedKind.COMMENT, ExtendedKind.VOICE_COMMENT],
limit: 100
}, },
{ {
'#q': [event.id], '#q': [event.id],
kinds: [kinds.ShortTextNote, ExtendedKind.COMMENT, ExtendedKind.VOICE_COMMENT], kinds: [kinds.ShortTextNote, ExtendedKind.COMMENT, ExtendedKind.VOICE_COMMENT],
limit: 100 limit: 50
},
{
'#e': [event.id],
kinds: [kinds.Highlights],
limit: 100
} }
] ]
@ -127,110 +172,30 @@ class NoteStatsService {
filters.push( filters.push(
{ {
'#a': [replaceableCoordinate], '#a': [replaceableCoordinate],
kinds: [kinds.Reaction], kinds: [kinds.Reaction, kinds.Repost, kinds.ShortTextNote, ExtendedKind.COMMENT, ExtendedKind.VOICE_COMMENT, kinds.Highlights],
limit: 100 limit: 50
},
{
'#a': [replaceableCoordinate],
kinds: [kinds.Repost],
limit: 100
},
{
'#a': [replaceableCoordinate],
kinds: [kinds.ShortTextNote, ExtendedKind.COMMENT, ExtendedKind.VOICE_COMMENT],
limit: 100
}, },
{ {
'#q': [replaceableCoordinate], '#q': [replaceableCoordinate],
kinds: [kinds.ShortTextNote, ExtendedKind.COMMENT, ExtendedKind.VOICE_COMMENT], kinds: [kinds.ShortTextNote, ExtendedKind.COMMENT, ExtendedKind.VOICE_COMMENT],
limit: 100 limit: 50
},
{
'#a': [replaceableCoordinate],
kinds: [kinds.Highlights],
limit: 100
} }
) )
} }
if (authorProfile?.lightningAddress) {
filters.push({
'#e': [event.id],
kinds: [kinds.Zap],
limit: 100
})
if (replaceableCoordinate) {
filters.push({
'#a': [replaceableCoordinate],
kinds: [kinds.Zap],
limit: 100
})
}
}
if (pubkey) {
filters.push({
'#e': [event.id],
authors: [pubkey],
kinds: [kinds.Reaction, kinds.Repost]
})
if (replaceableCoordinate) {
filters.push({
'#a': [replaceableCoordinate],
authors: [pubkey],
kinds: [kinds.Reaction, kinds.Repost]
})
}
if (authorProfile?.lightningAddress) {
filters.push({
'#e': [event.id],
'#P': [pubkey],
kinds: [kinds.Zap]
})
if (replaceableCoordinate) {
filters.push({
'#a': [replaceableCoordinate],
'#P': [pubkey],
kinds: [kinds.Zap]
})
}
}
}
if (since) { if (since) {
filters.forEach((filter) => { filters.forEach((filter) => {
filter.since = since filter.since = since
}) })
} }
const events: Event[] = []
logger.debug('[NoteStats] Fetching stats for event', event.id.substring(0, 8), 'from', finalRelayUrls.length, 'relays') return filters
await client.fetchEvents(finalRelayUrls, filters, { }
onevent: (evt) => {
this.updateNoteStatsByEvents([evt], event.pubkey) private async getEventById(eventId: string): Promise<Event | null> {
events.push(evt) // Fetch the event
} const event = await client.fetchEvent(eventId)
}) return event || null
logger.debug('[NoteStats] Fetched', events.length, 'events for stats')
// Debug: Count events by kind
const eventsByKind = events.reduce((acc, evt) => {
acc[evt.kind] = (acc[evt.kind] || 0) + 1
return acc
}, {} as Record<number, number>)
logger.debug('[NoteStats] Events by kind:', eventsByKind)
this.noteStatsMap.set(event.id, {
...(this.noteStatsMap.get(event.id) ?? {}),
updatedAt: dayjs().unix()
})
return this.noteStatsMap.get(event.id) ?? {}
} finally {
// Clean up processing cache
this.processingCache.delete(eventId)
}
} }
subscribeNoteStats(noteId: string, callback: () => void) { subscribeNoteStats(noteId: string, callback: () => void) {
@ -257,7 +222,6 @@ class NoteStatsService {
return this.noteStatsMap.get(id) return this.noteStatsMap.get(id)
} }
addZap( addZap(
pubkey: string, pubkey: string,
eventId: string, eventId: string,
@ -283,34 +247,47 @@ class NoteStatsService {
updateNoteStatsByEvents(events: Event[], originalEventAuthor?: string) { updateNoteStatsByEvents(events: Event[], originalEventAuthor?: string) {
const updatedEventIdSet = new Set<string>() const updatedEventIdSet = new Set<string>()
events.forEach((evt) => {
let updatedEventId: string | undefined // Process events in batches for better performance
if (evt.kind === kinds.Reaction) { const batchSize = 50
updatedEventId = this.addLikeByEvent(evt, originalEventAuthor) for (let i = 0; i < events.length; i += batchSize) {
} else if (evt.kind === kinds.Repost) { const batch = events.slice(i, i + batchSize)
updatedEventId = this.addRepostByEvent(evt, originalEventAuthor) batch.forEach((evt) => {
} else if (evt.kind === kinds.Zap) { const updatedEventId = this.processEvent(evt, originalEventAuthor)
updatedEventId = this.addZapByEvent(evt, originalEventAuthor) if (updatedEventId) {
} else if (evt.kind === kinds.ShortTextNote || evt.kind === ExtendedKind.COMMENT || evt.kind === ExtendedKind.VOICE_COMMENT) { updatedEventIdSet.add(updatedEventId)
// Check if it's a reply or quote
const isQuote = this.isQuoteByEvent(evt)
if (isQuote) {
updatedEventId = this.addQuoteByEvent(evt, originalEventAuthor)
} else {
updatedEventId = this.addReplyByEvent(evt, originalEventAuthor)
} }
} else if (evt.kind === kinds.Highlights) { })
updatedEventId = this.addHighlightByEvent(evt, originalEventAuthor) }
}
if (updatedEventId) {
updatedEventIdSet.add(updatedEventId)
}
})
updatedEventIdSet.forEach((eventId) => { updatedEventIdSet.forEach((eventId) => {
this.notifyNoteStats(eventId) this.notifyNoteStats(eventId)
}) })
} }
private processEvent(evt: Event, originalEventAuthor?: string): string | undefined {
let updatedEventId: string | undefined
if (evt.kind === kinds.Reaction) {
updatedEventId = this.addLikeByEvent(evt, originalEventAuthor)
} else if (evt.kind === kinds.Repost) {
updatedEventId = this.addRepostByEvent(evt, originalEventAuthor)
} else if (evt.kind === kinds.Zap) {
updatedEventId = this.addZapByEvent(evt, originalEventAuthor)
} else if (evt.kind === kinds.ShortTextNote || evt.kind === ExtendedKind.COMMENT || evt.kind === ExtendedKind.VOICE_COMMENT) {
const isQuote = this.isQuoteByEvent(evt)
if (isQuote) {
updatedEventId = this.addQuoteByEvent(evt, originalEventAuthor)
} else {
updatedEventId = this.addReplyByEvent(evt, originalEventAuthor)
}
} else if (evt.kind === kinds.Highlights) {
updatedEventId = this.addHighlightByEvent(evt, originalEventAuthor)
}
return updatedEventId
}
private addLikeByEvent(evt: Event, originalEventAuthor?: string) { private addLikeByEvent(evt: Event, originalEventAuthor?: string) {
const targetEventId = evt.tags.findLast(tagNameEquals('e'))?.[1] const targetEventId = evt.tags.findLast(tagNameEquals('e'))?.[1]
if (!targetEventId) return if (!targetEventId) return
@ -320,9 +297,7 @@ class NoteStatsService {
const likes = old.likes || [] const likes = old.likes || []
if (likeIdSet.has(evt.id)) return if (likeIdSet.has(evt.id)) return
// Skip self-interactions - don't count likes from the original event author
if (originalEventAuthor && originalEventAuthor === evt.pubkey) { if (originalEventAuthor && originalEventAuthor === evt.pubkey) {
// console.log('[NoteStats] Skipping self-like from', evt.pubkey, 'to event', targetEventId)
return return
} }
@ -369,9 +344,7 @@ class NoteStatsService {
const reposts = old.reposts || [] const reposts = old.reposts || []
if (repostPubkeySet.has(evt.pubkey)) return if (repostPubkeySet.has(evt.pubkey)) return
// Skip self-interactions - don't count reposts from the original event author
if (originalEventAuthor && originalEventAuthor === evt.pubkey) { if (originalEventAuthor && originalEventAuthor === evt.pubkey) {
// console.log('[NoteStats] Skipping self-repost from', evt.pubkey, 'to event', eventId)
return return
} }
@ -387,9 +360,7 @@ class NoteStatsService {
const { originalEventId, senderPubkey, invoice, amount, comment } = info const { originalEventId, senderPubkey, invoice, amount, comment } = info
if (!originalEventId || !senderPubkey) return if (!originalEventId || !senderPubkey) return
// Skip self-interactions - don't count zaps from the original event author
if (originalEventAuthor && originalEventAuthor === senderPubkey) { if (originalEventAuthor && originalEventAuthor === senderPubkey) {
// console.log('[NoteStats] Skipping self-zap from', senderPubkey, 'to event', originalEventId)
return return
} }
@ -405,53 +376,38 @@ class NoteStatsService {
} }
private addReplyByEvent(evt: Event, originalEventAuthor?: string) { private addReplyByEvent(evt: Event, originalEventAuthor?: string) {
// Use the same logic as isReplyNoteEvent to identify replies
let originalEventId: string | undefined let originalEventId: string | undefined
// For kind 1111 and 1244, always consider them replies and look for parent event
if (evt.kind === ExtendedKind.COMMENT || evt.kind === ExtendedKind.VOICE_COMMENT) { if (evt.kind === ExtendedKind.COMMENT || evt.kind === ExtendedKind.VOICE_COMMENT) {
const eTag = evt.tags.find(tagNameEquals('e')) ?? evt.tags.find(tagNameEquals('E')) const eTag = evt.tags.find(tagNameEquals('e')) ?? evt.tags.find(tagNameEquals('E'))
originalEventId = eTag?.[1] originalEventId = eTag?.[1]
} } else if (evt.kind === kinds.ShortTextNote) {
// For kind 1 (ShortTextNote), check if it's actually a reply
else if (evt.kind === kinds.ShortTextNote) {
// Check for parent E tag (reply or root marker)
const parentETag = evt.tags.find(([tagName, , , marker]) => { const parentETag = evt.tags.find(([tagName, , , marker]) => {
return tagName === 'e' && (marker === 'reply' || marker === 'root') return tagName === 'e' && (marker === 'reply' || marker === 'root')
}) })
if (parentETag) { if (parentETag) {
originalEventId = parentETag[1] originalEventId = parentETag[1]
logger.debug('[NoteStats] Found reply with root/reply marker:', evt.id, '->', originalEventId)
} else { } else {
// Look for the last E tag that's not a mention
const embeddedEventIds = this.getEmbeddedNoteBech32Ids(evt)
const lastETag = evt.tags.findLast( const lastETag = evt.tags.findLast(
([tagName, tagValue, , marker]) => ([tagName, tagValue, , marker]) =>
tagName === 'e' && tagName === 'e' &&
!!tagValue && !!tagValue &&
marker !== 'mention' && marker !== 'mention'
!embeddedEventIds.includes(tagValue)
) )
if (lastETag) { if (lastETag) {
originalEventId = lastETag[1] originalEventId = lastETag[1]
console.log('[NoteStats] Found reply with last E tag:', evt.id, '->', originalEventId)
} }
} }
// Also check for parent A tag
if (!originalEventId) { if (!originalEventId) {
const aTag = evt.tags.find(tagNameEquals('a')) const aTag = evt.tags.find(tagNameEquals('a'))
if (aTag) { if (aTag) {
originalEventId = aTag[1] originalEventId = aTag[1]
console.log('[NoteStats] Found reply with A tag:', evt.id, '->', originalEventId)
} }
} }
} }
if (!originalEventId) { if (!originalEventId) return
console.log('[NoteStats] No original event ID found for potential reply:', evt.id, 'tags:', evt.tags)
return
}
const old = this.noteStatsMap.get(originalEventId) || {} const old = this.noteStatsMap.get(originalEventId) || {}
const replyIdSet = old.replyIdSet || new Set() const replyIdSet = old.replyIdSet || new Set()
@ -459,26 +415,21 @@ class NoteStatsService {
if (replyIdSet.has(evt.id)) return if (replyIdSet.has(evt.id)) return
// Skip self-interactions - don't count replies from the original event author
if (originalEventAuthor && originalEventAuthor === evt.pubkey) { if (originalEventAuthor && originalEventAuthor === evt.pubkey) {
logger.debug('[NoteStats] Skipping self-reply from', evt.pubkey, 'to event', originalEventId)
return return
} }
replyIdSet.add(evt.id) replyIdSet.add(evt.id)
replies.push({ id: evt.id, pubkey: evt.pubkey, created_at: evt.created_at }) replies.push({ id: evt.id, pubkey: evt.pubkey, created_at: evt.created_at })
this.noteStatsMap.set(originalEventId, { ...old, replyIdSet, replies }) this.noteStatsMap.set(originalEventId, { ...old, replyIdSet, replies })
logger.debug('[NoteStats] Added reply:', evt.id, 'to event:', originalEventId, 'total replies:', replies.length)
return originalEventId return originalEventId
} }
private isQuoteByEvent(evt: Event): boolean { private isQuoteByEvent(evt: Event): boolean {
// A quote has a 'q' tag (quoted event)
return evt.tags.some(tag => tag[0] === 'q' && tag[1]) return evt.tags.some(tag => tag[0] === 'q' && tag[1])
} }
private addQuoteByEvent(evt: Event, originalEventAuthor?: string) { private addQuoteByEvent(evt: Event, originalEventAuthor?: string) {
// Find the quoted event ID from 'q' tag
const quotedEventId = evt.tags.find(tag => tag[0] === 'q')?.[1] const quotedEventId = evt.tags.find(tag => tag[0] === 'q')?.[1]
if (!quotedEventId) return if (!quotedEventId) return
@ -488,9 +439,7 @@ class NoteStatsService {
if (quoteIdSet.has(evt.id)) return if (quoteIdSet.has(evt.id)) return
// Skip self-interactions - don't count quotes from the original event author
if (originalEventAuthor && originalEventAuthor === evt.pubkey) { if (originalEventAuthor && originalEventAuthor === evt.pubkey) {
// console.log('[NoteStats] Skipping self-quote from', evt.pubkey, 'to event', quotedEventId)
return return
} }
@ -501,7 +450,6 @@ class NoteStatsService {
} }
private addHighlightByEvent(evt: Event, originalEventAuthor?: string) { private addHighlightByEvent(evt: Event, originalEventAuthor?: string) {
// Find the event ID from 'e' tag
const highlightedEventId = evt.tags.find(tag => tag[0] === 'e')?.[1] const highlightedEventId = evt.tags.find(tag => tag[0] === 'e')?.[1]
if (!highlightedEventId) return if (!highlightedEventId) return
@ -511,9 +459,7 @@ class NoteStatsService {
if (highlightIdSet.has(evt.id)) return if (highlightIdSet.has(evt.id)) return
// Skip self-interactions - don't count highlights from the original event author
if (originalEventAuthor && originalEventAuthor === evt.pubkey) { if (originalEventAuthor && originalEventAuthor === evt.pubkey) {
// console.log('[NoteStats] Skipping self-highlight from', evt.pubkey, 'to event', highlightedEventId)
return return
} }
@ -522,20 +468,6 @@ class NoteStatsService {
this.noteStatsMap.set(highlightedEventId, { ...old, highlightIdSet, highlights }) this.noteStatsMap.set(highlightedEventId, { ...old, highlightIdSet, highlights })
return highlightedEventId return highlightedEventId
} }
private getEmbeddedNoteBech32Ids(event: Event): string[] {
// Simple implementation - in practice, this should match the logic in lib/event.ts
const embeddedIds: string[] = []
const content = event.content || ''
const matches = content.match(/nostr:(note1|nevent1)[a-zA-Z0-9]+/g)
if (matches) {
matches.forEach(match => {
const id = match.replace('nostr:', '')
embeddedIds.push(id)
})
}
return embeddedIds
}
} }
const instance = new NoteStatsService() const instance = new NoteStatsService()

Loading…
Cancel
Save