diff --git a/src/components/NoteStats/index.tsx b/src/components/NoteStats/index.tsx index a429339..51a962d 100644 --- a/src/components/NoteStats/index.tsx +++ b/src/components/NoteStats/index.tsx @@ -1,6 +1,7 @@ import { cn } from '@/lib/utils' import { useNostr } from '@/providers/NostrProvider' import { useScreenSize } from '@/providers/ScreenSizeProvider' +import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' import noteStatsService from '@/services/note-stats.service' import { ExtendedKind } from '@/constants' import { getRootEventHexId } from '@/lib/event' @@ -33,6 +34,7 @@ export default function NoteStats({ }) { const { isSmallScreen } = useScreenSize() const { pubkey } = useNostr() + const { favoriteRelays } = useFavoriteRelays() const [loading, setLoading] = useState(false) // Hide repost button for discussion events and replies to discussions @@ -59,7 +61,7 @@ export default function NoteStats({ useEffect(() => { if (!fetchIfNotExisting) return setLoading(true) - noteStatsService.fetchNoteStats(event, pubkey).finally(() => setLoading(false)) + noteStatsService.fetchNoteStats(event, pubkey, favoriteRelays).finally(() => setLoading(false)) }, [event, fetchIfNotExisting]) if (isSmallScreen) { diff --git a/src/components/QuoteList/index.tsx b/src/components/QuoteList/index.tsx index 43c91fc..2ab2176 100644 --- a/src/components/QuoteList/index.tsx +++ b/src/components/QuoteList/index.tsx @@ -15,7 +15,7 @@ const SHOW_COUNT = 10 export default function QuoteList({ event, className }: { event: Event; className?: string }) { const { t } = useTranslation() - const { startLogin, relayList: userRelayList } = useNostr() + const { relayList: userRelayList } = useNostr() const { hideUntrustedInteractions, isUserTrusted } = useUserTrust() const [timelineKey, setTimelineKey] = useState(undefined) const [events, setEvents] = useState([]) @@ -90,8 +90,7 @@ export default function QuoteList({ event, className }: { event: Event; classNam [event, ...oldEvents].sort((a, b) => b.created_at - a.created_at) ) } - }, - { startLogin } + } ) setTimelineKey(timelineKey) return closer diff --git a/src/components/TrendingNotes/index.tsx b/src/components/TrendingNotes/index.tsx index 029cc8b..803fab5 100644 --- a/src/components/TrendingNotes/index.tsx +++ b/src/components/TrendingNotes/index.tsx @@ -49,8 +49,19 @@ export default function TrendingNotes() { const [selectedHashtag, setSelectedHashtag] = useState(null) const [popularHashtags, setPopularHashtags] = useState([]) const [cacheEvents, setCacheEvents] = useState([]) + const [cacheLoading, setCacheLoading] = useState(false) const bottomRef = useRef(null) + // Debug: Track cacheEvents changes + useEffect(() => { + console.log('[TrendingNotes] cacheEvents state changed:', cacheEvents.length, 'events') + }, [cacheEvents]) + + // Debug: Track cacheLoading changes + useEffect(() => { + console.log('[TrendingNotes] cacheLoading state changed:', cacheLoading) + }, [cacheLoading]) + // Extract event IDs from bookmark and pin lists (kinds 10003 and 10001) const listEventIds = useMemo(() => { @@ -228,16 +239,29 @@ export default function TrendingNotes() { if (cachedCustomEvents && (now - cachedCustomEvents.timestamp) < CACHE_DURATION) { // If cache is valid, set cacheEvents to ALL events from cache const allEvents = cachedCustomEvents.events.map(item => item.event) + console.log('[TrendingNotes] Using existing cache - loading', allEvents.length, 'events') setCacheEvents(allEvents) + setCacheLoading(false) // Ensure loading state is cleared return } isInitializing = true + setCacheLoading(true) const relays = getRelays // This is already a value from useMemo + // Set a timeout to prevent infinite loading + const timeoutId = setTimeout(() => { + console.log('[TrendingNotes] Cache initialization timeout - forcing completion') + isInitializing = false + setCacheLoading(false) + }, 180000) // 3 minute timeout + // Prevent running if we have no relays if (relays.length === 0) { + console.log('[TrendingNotes] No relays available, skipping cache initialization') + clearTimeout(timeoutId) isInitializing = false + setCacheLoading(false) return } @@ -245,12 +269,15 @@ export default function TrendingNotes() { const allEvents: NostrEvent[] = [] const twentyFourHoursAgo = Math.floor(Date.now() / 1000) - 24 * 60 * 60 + console.log('[TrendingNotes] Starting cache initialization with', relays.length, 'relays:', relays) + // 1. Fetch top-level posts from last 24 hours - batch requests to avoid overwhelming relays const batchSize = 3 // Process 3 relays at a time const recentEvents: NostrEvent[] = [] for (let i = 0; i < relays.length; i += batchSize) { const batch = relays.slice(i, i + batchSize) + console.log('[TrendingNotes] Processing batch', Math.floor(i/batchSize) + 1, 'of', Math.ceil(relays.length/batchSize), 'relays:', batch) const batchPromises = batch.map(async (relay) => { try { const events = await client.fetchEvents([relay], { @@ -258,6 +285,7 @@ export default function TrendingNotes() { since: twentyFourHoursAgo, limit: 500 }) + console.log('[TrendingNotes] Fetched', events.length, 'events from relay', relay) return events } catch (error) { console.warn(`[TrendingNotes] Error fetching from relay ${relay}:`, error) @@ -266,7 +294,9 @@ export default function TrendingNotes() { }) const batchResults = await Promise.all(batchPromises) - recentEvents.push(...batchResults.flat()) + const batchEvents = batchResults.flat() + recentEvents.push(...batchEvents) + console.log('[TrendingNotes] Batch completed, total events so far:', recentEvents.length) // Add a small delay between batches to be respectful to relays if (i + batchSize < relays.length) { @@ -358,21 +388,31 @@ export default function TrendingNotes() { // Fetch stats for events in batches with longer delays const eventsNeedingStats = filteredEvents.filter(event => !noteStatsService.getNoteStats(event.id)) + console.log('[TrendingNotes] Need to fetch stats for', eventsNeedingStats.length, 'events') if (eventsNeedingStats.length > 0) { - const batchSize = 5 // Reduced batch size + const batchSize = 10 // Increased batch size to speed up + const totalBatches = Math.ceil(eventsNeedingStats.length / batchSize) + console.log('[TrendingNotes] Fetching stats in', totalBatches, 'batches') + for (let i = 0; i < eventsNeedingStats.length; i += batchSize) { const batch = eventsNeedingStats.slice(i, i + batchSize) + const batchNum = Math.floor(i / batchSize) + 1 + console.log('[TrendingNotes] Fetching stats batch', batchNum, 'of', totalBatches) + await Promise.all(batch.map(event => - noteStatsService.fetchNoteStats(event, undefined).catch(() => {}) + noteStatsService.fetchNoteStats(event, undefined, favoriteRelays).catch(() => {}) )) + if (i + batchSize < eventsNeedingStats.length) { - await new Promise(resolve => setTimeout(resolve, 500)) // Increased delay + await new Promise(resolve => setTimeout(resolve, 200)) // Reduced delay } } + console.log('[TrendingNotes] Stats fetching completed') } // Score events + console.log('[TrendingNotes] Scoring', filteredEvents.length, 'events') const scoredEvents = filteredEvents.map((event) => { const stats = noteStatsService.getNoteStats(event.id) let score = 0 @@ -398,6 +438,7 @@ export default function TrendingNotes() { }) // Update cache + console.log('[TrendingNotes] Updating cache with', scoredEvents.length, 'scored events') cachedCustomEvents = { events: scoredEvents, timestamp: now, @@ -407,11 +448,14 @@ export default function TrendingNotes() { // Store ALL events from the cache for hashtag analysis // This includes all events from relays, not just the trending ones + console.log('[TrendingNotes] Cache initialization complete - storing', filteredEvents.length, 'events') setCacheEvents(filteredEvents) } catch (error) { console.error('[TrendingNotes] Error initializing cache:', error) } finally { + clearTimeout(timeoutId) isInitializing = false + setCacheLoading(false) } } @@ -431,6 +475,7 @@ export default function TrendingNotes() { } else if (activeTab === 'relays') { // "on your relays" tab: use cache events from user's relays sourceEvents = cacheEvents + console.log('[TrendingNotes] Relays tab - cacheEvents.length:', cacheEvents.length, 'cacheLoading:', cacheLoading) } else if (activeTab === 'hashtags') { // Hashtags tab: use cache events for hashtag analysis sourceEvents = cacheEvents.length > 0 ? cacheEvents : trendingNotes @@ -584,11 +629,15 @@ export default function TrendingNotes() { setSortOrder('most-popular') } else if (activeTab === 'relays') { setSortOrder('most-popular') + // If cache is empty and not loading, log the issue for debugging + if (cacheEvents.length === 0 && !cacheLoading && !isInitializing) { + console.log('[TrendingNotes] Relays tab selected but cache is empty - this should not happen if cache initialization completed') + } } else if (activeTab === 'hashtags') { setSortOrder('most-popular') setSelectedHashtag(null) } - }, [activeTab, pubkey]) + }, [activeTab, pubkey, cacheEvents.length, cacheLoading]) // Handle case where bookmarks tab is not available useEffect(() => { @@ -747,6 +796,14 @@ export default function TrendingNotes() { )} + + {/* Show loading message for relays tab when cache is loading */} + {activeTab === 'relays' && cacheLoading && cacheEvents.length === 0 && ( +
+ Loading trending notes from your relays... +
+ )} + {filteredEvents.map((event) => ( ))} @@ -756,7 +813,16 @@ export default function TrendingNotes() { activeTab === 'relays' || activeTab === 'hashtags' ? cacheEvents.length : trendingNotes.length - if (showCount < currentDataLength || loading) { + // Show loading if: + // 1. General loading state is true + // 2. For relays/hashtags tabs, if cache is loading + // 3. If we haven't reached the end of available data + const shouldShowLoading = loading || + (activeTab === 'relays' && cacheLoading) || + (activeTab === 'hashtags' && cacheLoading) || + showCount < currentDataLength + + if (shouldShowLoading) { return (
diff --git a/src/services/note-stats.service.ts b/src/services/note-stats.service.ts index e50c9d0..5a68aee 100644 --- a/src/services/note-stats.service.ts +++ b/src/services/note-stats.service.ts @@ -1,7 +1,8 @@ -import { BIG_RELAY_URLS, ExtendedKind } from '@/constants' +import { BIG_RELAY_URLS, ExtendedKind, SEARCHABLE_RELAY_URLS } from '@/constants' import { getReplaceableCoordinateFromEvent, isReplaceableEvent } from '@/lib/event' import { getZapInfoFromEvent } from '@/lib/event-metadata' import { getEmojiInfosFromEmojiTags, tagNameEquals } from '@/lib/tag' +import { normalizeUrl } from '@/lib/url' import client from '@/services/client.service' import { TEmoji } from '@/types' import dayjs from 'dayjs' @@ -35,7 +36,7 @@ class NoteStatsService { return NoteStatsService.instance } - async fetchNoteStats(event: Event, pubkey?: string | null) { + async fetchNoteStats(event: Event, pubkey?: string | null, favoriteRelays?: string[]) { const oldStats = this.noteStatsMap.get(event.id) let since: number | undefined if (oldStats?.updatedAt) { @@ -47,6 +48,26 @@ class NoteStatsService { client.fetchProfile(event.pubkey) ]) + // Build comprehensive relay list: user's inboxes + user's favorite relays + big relays + // For anonymous users, also include fast read relays for better coverage + const allRelays = [ + ...(relayList.read || []), // User's inboxes (kind 10002) + ...(favoriteRelays || []), // User's favorite relays (kind 10012) + ...BIG_RELAY_URLS, // Big relays + ...(pubkey ? [] : SEARCHABLE_RELAY_URLS) // Fast read relays for anonymous users only + ] + + // Normalize and deduplicate relay URLs + const normalizedRelays = allRelays + .map(url => normalizeUrl(url)) + .filter((url): url is string => !!url) + + const finalRelayUrls = Array.from(new Set(normalizedRelays)) + const relayTypes = pubkey + ? 'inboxes kind 10002 + favorites kind 10012 + big relays' + : 'big relays + searchable relays (anonymous user)' + console.log('[NoteStats] Using', finalRelayUrls.length, 'relays for stats (' + relayTypes + '):', finalRelayUrls) + const replaceableCoordinate = isReplaceableEvent(event.kind) ? getReplaceableCoordinateFromEvent(event) : undefined @@ -163,12 +184,21 @@ class NoteStatsService { }) } const events: Event[] = [] - await client.fetchEvents([...relayList.read, ...BIG_RELAY_URLS].slice(0, 5), filters, { + console.log('[NoteStats] Fetching stats for event', event.id, 'from', finalRelayUrls.length, 'relays') + await client.fetchEvents(finalRelayUrls, filters, { onevent: (evt) => { this.updateNoteStatsByEvents([evt]) events.push(evt) } }) + console.log('[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) + console.log('[NoteStats] Events by kind:', eventsByKind) this.noteStatsMap.set(event.id, { ...(this.noteStatsMap.get(event.id) ?? {}), updatedAt: dayjs().unix() @@ -345,6 +375,7 @@ class NoteStatsService { }) if (parentETag) { originalEventId = parentETag[1] + console.log('[NoteStats] Found reply with root/reply marker:', evt.id, '->', originalEventId) } else { // Look for the last E tag that's not a mention const embeddedEventIds = this.getEmbeddedNoteBech32Ids(evt) @@ -355,17 +386,26 @@ class NoteStatsService { marker !== 'mention' && !embeddedEventIds.includes(tagValue) ) - originalEventId = lastETag?.[1] + if (lastETag) { + originalEventId = lastETag[1] + console.log('[NoteStats] Found reply with last E tag:', evt.id, '->', originalEventId) + } } // Also check for parent A tag if (!originalEventId) { const aTag = evt.tags.find(tagNameEquals('a')) - originalEventId = aTag?.[1] + if (aTag) { + originalEventId = aTag[1] + console.log('[NoteStats] Found reply with A tag:', evt.id, '->', originalEventId) + } } } - if (!originalEventId) return + if (!originalEventId) { + console.log('[NoteStats] No original event ID found for potential reply:', evt.id, 'tags:', evt.tags) + return + } const old = this.noteStatsMap.get(originalEventId) || {} const replyIdSet = old.replyIdSet || new Set() @@ -376,6 +416,7 @@ class NoteStatsService { replyIdSet.add(evt.id) replies.push({ id: evt.id, pubkey: evt.pubkey, created_at: evt.created_at }) this.noteStatsMap.set(originalEventId, { ...old, replyIdSet, replies }) + console.log('[NoteStats] Added reply:', evt.id, 'to event:', originalEventId, 'total replies:', replies.length) return originalEventId }