diff --git a/src/PageManager.tsx b/src/PageManager.tsx index 1b5d83c8..27ca9ada 100644 --- a/src/PageManager.tsx +++ b/src/PageManager.tsx @@ -566,7 +566,7 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { const savedFeedStateRef = useRef>(new Map()) const currentTabStateRef = useRef>(new Map()) // Track current tab state for each page @@ -598,7 +598,7 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { } // Get trending tab if on search page - const trendingTab = currentTabStateRef.current.get('search') as 'nostr' | 'relays' | 'hashtags' | undefined + const trendingTab = currentTabStateRef.current.get('search') as 'relays' | 'hashtags' | undefined // Save state (tab, discussions, trending) if any exists if (currentTab || discussionsState || trendingTab) { @@ -654,16 +654,17 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { })) } - // Restore trending tab for search page + // Restore trending tab for search page (map legacy 'nostr' to 'relays') if (savedFeedState?.trendingTab && savedPrimaryPage === 'search') { + const tab = (savedFeedState.trendingTab as string) === 'nostr' ? 'relays' : savedFeedState.trendingTab logger.info('PageManager: Restoring trending tab', { page: savedPrimaryPage, - trendingTab: savedFeedState.trendingTab + trendingTab: tab }) window.dispatchEvent(new CustomEvent('restorePageTab', { - detail: { page: 'search', tab: savedFeedState.trendingTab } + detail: { page: 'search', tab } })) - currentTabStateRef.current.set('search', savedFeedState.trendingTab) + currentTabStateRef.current.set('search', tab) } } } @@ -1118,14 +1119,15 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { // Restore trending tab for search page if (savedFeedState?.trendingTab && currentPrimaryPage === 'search') { - logger.info('PageManager: Browser back - Restoring trending tab', { - page: currentPrimaryPage, - trendingTab: savedFeedState.trendingTab + const tab = (savedFeedState.trendingTab as string) === 'nostr' ? 'relays' : savedFeedState.trendingTab + logger.info('PageManager: Browser back - Restoring trending tab', { + page: currentPrimaryPage, + trendingTab: tab }) - window.dispatchEvent(new CustomEvent('restorePageTab', { - detail: { page: 'search', tab: savedFeedState.trendingTab } + window.dispatchEvent(new CustomEvent('restorePageTab', { + detail: { page: 'search', tab } })) - currentTabStateRef.current.set('search', savedFeedState.trendingTab) + currentTabStateRef.current.set('search', tab) } } }, [secondaryStack.length, currentPrimaryPage]) @@ -1167,7 +1169,7 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { // Save tab state before navigating const currentTab = currentTabStateRef.current.get(currentPrimaryPage) - const trendingTab = currentTabStateRef.current.get('search') as 'nostr' | 'relays' | 'hashtags' | undefined + const trendingTab = currentTabStateRef.current.get('search') as 'relays' | 'hashtags' | undefined if (currentPrimaryPage && (currentTab || trendingTab)) { logger.info('PageManager: Desktop - Saving page state', { @@ -1251,14 +1253,15 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { // Restore trending tab for search page if (savedFeedState?.trendingTab && currentPrimaryPage === 'search') { - logger.info('PageManager: Desktop - Restoring trending tab', { - page: currentPrimaryPage, - trendingTab: savedFeedState.trendingTab + const tab = (savedFeedState.trendingTab as string) === 'nostr' ? 'relays' : savedFeedState.trendingTab + logger.info('PageManager: Desktop - Restoring trending tab', { + page: currentPrimaryPage, + trendingTab: tab }) - window.dispatchEvent(new CustomEvent('restorePageTab', { - detail: { page: 'search', tab: savedFeedState.trendingTab } + window.dispatchEvent(new CustomEvent('restorePageTab', { + detail: { page: 'search', tab } })) - currentTabStateRef.current.set('search', savedFeedState.trendingTab) + currentTabStateRef.current.set('search', tab) } } else if (secondaryStack.length > 1) { // Pop from stack directly instead of using history.go(-1) @@ -1319,14 +1322,15 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { // Restore trending tab for search page if (savedFeedState?.trendingTab && currentPrimaryPage === 'search') { - logger.info('PageManager: Mobile/Single-pane - Restoring trending tab', { - page: currentPrimaryPage, - trendingTab: savedFeedState.trendingTab + const tab = (savedFeedState.trendingTab as string) === 'nostr' ? 'relays' : savedFeedState.trendingTab + logger.info('PageManager: Mobile/Single-pane - Restoring trending tab', { + page: currentPrimaryPage, + trendingTab: tab }) - window.dispatchEvent(new CustomEvent('restorePageTab', { - detail: { page: 'search', tab: savedFeedState.trendingTab } + window.dispatchEvent(new CustomEvent('restorePageTab', { + detail: { page: 'search', tab } })) - currentTabStateRef.current.set('search', savedFeedState.trendingTab) + currentTabStateRef.current.set('search', tab) } return } @@ -1360,14 +1364,15 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { // Restore trending tab for search page if (savedFeedState?.trendingTab && currentPrimaryPage === 'search') { - logger.info('PageManager: Desktop - Restoring trending tab', { - page: currentPrimaryPage, - trendingTab: savedFeedState.trendingTab + const tab = (savedFeedState.trendingTab as string) === 'nostr' ? 'relays' : savedFeedState.trendingTab + logger.info('PageManager: Desktop - Restoring trending tab', { + page: currentPrimaryPage, + trendingTab: tab }) - window.dispatchEvent(new CustomEvent('restorePageTab', { - detail: { page: 'search', tab: savedFeedState.trendingTab } + window.dispatchEvent(new CustomEvent('restorePageTab', { + detail: { page: 'search', tab } })) - currentTabStateRef.current.set('search', savedFeedState.trendingTab) + currentTabStateRef.current.set('search', tab) } } else { window.history.go(-1) diff --git a/src/components/TrendingNotes/index.tsx b/src/components/TrendingNotes/index.tsx index 0e7e36dd..e5fa1fe2 100644 --- a/src/components/TrendingNotes/index.tsx +++ b/src/components/TrendingNotes/index.tsx @@ -27,7 +27,7 @@ let cachedCustomEvents: { // Flag to prevent concurrent initialization let isInitializing = false -type TrendingTab = 'nostr' | 'relays' | 'hashtags' +type TrendingTab = 'relays' | 'hashtags' type SortOrder = 'newest' | 'oldest' | 'most-popular' | 'least-popular' type HashtagFilter = 'popular' @@ -38,9 +38,6 @@ export default function TrendingNotes() { const { pubkey, relayList } = useNostr() const { favoriteRelays } = useFavoriteRelays() const { zapReplyThreshold } = useZap() - const [nostrEvents, setNostrEvents] = useState([]) - const [nostrLoading, setNostrLoading] = useState(false) - const [nostrError, setNostrError] = useState(null) const [showCount, setShowCount] = useState(SHOW_COUNT) const [activeTab, setActiveTab] = useState('relays') const [sortOrder, setSortOrder] = useState('most-popular') @@ -50,23 +47,11 @@ export default function TrendingNotes() { const [cacheEvents, setCacheEvents] = useState([]) const [cacheLoading, setCacheLoading] = useState(false) const bottomRef = useRef(null) - const isFetchingNostrRef = useRef(false) - const hasUserClickedNostrTabRef = useRef(false) // Listen for tab restoration from PageManager useEffect(() => { const handleRestore = (e: CustomEvent<{ page: string, tab: string }>) => { - if (e.detail.page === 'search' && e.detail.tab && ['nostr', 'relays', 'hashtags'].includes(e.detail.tab)) { - // If restoring to 'nostr' tab, mark it as clicked and clear events to force a fresh load - if (e.detail.tab === 'nostr') { - hasUserClickedNostrTabRef.current = true - // Clear any existing events and error state to force a fresh load (only for API tab) - setNostrEvents([]) - setNostrError(null) - } - // For 'relays' and 'hashtags' tabs, just set the active tab - // The cache should already be loaded, but if it's empty, the initialization useEffect will handle it - // Then set the active tab - this will trigger the useEffect that loads the feed + if (e.detail.page === 'search' && e.detail.tab && ['relays', 'hashtags'].includes(e.detail.tab)) { setActiveTab(e.detail.tab as TrendingTab) } } @@ -74,48 +59,6 @@ export default function TrendingNotes() { return () => window.removeEventListener('restorePageTab', handleRestore as EventListener) }, []) - // Load Nostr.band trending feed only when user explicitly clicks the nostr tab - useEffect(() => { - const loadTrending = async () => { - // Prevent concurrent fetches - if (isFetchingNostrRef.current) { - return - } - - try { - isFetchingNostrRef.current = true - setNostrLoading(true) - setNostrError(null) - const events = await client.fetchTrendingNotes() - setNostrEvents(events) - setNostrError(null) - } catch (error) { - if (error instanceof Error && error.message === 'TIMEOUT') { - setNostrError('timeout') - logger.warn('nostr.band API request timed out after 5 seconds') - } else { - logger.warn('Failed to load nostr.band trending notes', error as Error) - setNostrError(null) // Other errors are handled silently (empty array) - } - } finally { - setNostrLoading(false) - isFetchingNostrRef.current = false - } - } - - // Only fetch if user has explicitly clicked the nostr tab AND it's currently active - if (activeTab === 'nostr' && hasUserClickedNostrTabRef.current && nostrEvents.length === 0 && !nostrLoading && !nostrError && !isFetchingNostrRef.current) { - loadTrending() - } - }, [activeTab, nostrEvents.length, nostrLoading, nostrError]) - - // Reset error when switching away from nostr tab - useEffect(() => { - if (activeTab !== 'nostr') { - setNostrError(null) - } - }, [activeTab]) - // Debug: Track cacheEvents changes useEffect(() => { logger.debug('[TrendingNotes] cacheEvents state changed:', cacheEvents.length, 'events') @@ -131,10 +74,9 @@ export default function TrendingNotes() { // Calculate popular hashtags from cache events (all events from relays) const calculatePopularHashtags = useMemo(() => { - logger.debug('[TrendingNotes] calculatePopularHashtags - cacheEvents.length:', cacheEvents.length, 'nostrEvents.length:', nostrEvents.length) + logger.debug('[TrendingNotes] calculatePopularHashtags - cacheEvents.length:', cacheEvents.length) - // Use cache events if available, otherwise fallback to trending notes - const eventsToAnalyze = cacheEvents.length > 0 ? cacheEvents : nostrEvents + const eventsToAnalyze = cacheEvents if (eventsToAnalyze.length === 0) { return [] @@ -178,7 +120,7 @@ export default function TrendingNotes() { logger.debug('[TrendingNotes] calculatePopularHashtags - eventsWithHashtags:', eventsWithHashtags) return result - }, [cacheEvents, nostrEvents, activeTab, hashtagFilter, pubkey]) + }, [cacheEvents, activeTab, hashtagFilter, pubkey]) // Get relays based on user login status const getRelays = useMemo(() => { @@ -214,15 +156,6 @@ export default function TrendingNotes() { setPopularHashtags(calculatePopularHashtags) }, [calculatePopularHashtags]) - // Fallback: populate cacheEvents from nostrEvents if cache is empty - useEffect(() => { - if (activeTab === 'hashtags' && cacheEvents.length === 0 && nostrEvents.length > 0) { - logger.debug('[TrendingNotes] Fallback: populating cacheEvents from nostrEvents') - setCacheEvents(nostrEvents) - } - }, [activeTab, cacheEvents.length, nostrEvents]) - - // Initialize cache only once on mount useEffect(() => { const initializeCache = async () => { @@ -436,7 +369,7 @@ export default function TrendingNotes() { // Compute filtered events without slicing (for pagination length check) const relaysFilteredEventsAll = useMemo(() => { const idSet = new Set() - const sourceEvents = cacheEvents.length > 0 ? cacheEvents : nostrEvents + const sourceEvents = cacheEvents const filtered = sourceEvents.filter((evt) => { if (isEventDeleted(evt)) return false @@ -519,7 +452,6 @@ export default function TrendingNotes() { return filtered }, [ cacheEvents, - nostrEvents, hideUntrustedNotes, isEventDeleted, isUserTrusted, @@ -536,11 +468,8 @@ export default function TrendingNotes() { }, [relaysFilteredEventsAll, showCount]) const filteredEvents = useMemo(() => { - if (activeTab === 'nostr') { - return nostrEvents.slice(0, showCount) - } return relaysFilteredEvents - }, [activeTab, nostrEvents, showCount, relaysFilteredEvents]) + }, [relaysFilteredEvents]) @@ -565,12 +494,7 @@ export default function TrendingNotes() { useEffect(() => { - // For relays/hashtags tabs, use the filtered length (before slicing) - // For nostr tab, use the raw events length - const totalLength = - activeTab === 'nostr' - ? nostrEvents.length - : relaysFilteredEventsAll.length + const totalLength = relaysFilteredEventsAll.length if (showCount >= totalLength) return @@ -597,7 +521,7 @@ export default function TrendingNotes() { observerInstance.unobserve(currentBottomRef) } } - }, [activeTab, nostrEvents.length, relaysFilteredEventsAll.length, showCount, cacheLoading, nostrLoading]) + }, [relaysFilteredEventsAll.length, showCount, cacheLoading]) return (
@@ -638,22 +562,6 @@ export default function TrendingNotes() { > hashtags -
@@ -738,19 +646,6 @@ export default function TrendingNotes() { )} - {/* Show error message for nostr tab timeout (show instead of loading when error occurs, only if no events) */} - {activeTab === 'nostr' && nostrError === 'timeout' && !nostrLoading && filteredEvents.length === 0 && ( -
- {t('The nostr.band relay appears to be temporarily out of service. Please try again later.')} -
- )} - - {/* Show loading message for nostr tab (only if not in error state) */} - {activeTab === 'nostr' && nostrLoading && nostrEvents.length === 0 && !nostrError && ( -
- Loading trending notes from nostr.band... -
- )} {/* Show loading message for relays tab when cache is loading */} {activeTab === 'relays' && cacheLoading && cacheEvents.length === 0 && (
@@ -762,28 +657,11 @@ export default function TrendingNotes() { ))} - {/* Show error message at the end for nostr tab timeout (only if there are events) */} - {activeTab === 'nostr' && nostrError === 'timeout' && !nostrLoading && filteredEvents.length > 0 && ( -
- {t('The nostr.band relay appears to be temporarily out of service. Please try again later.')} -
- )} - {(() => { - const totalAvailableLength = - activeTab === 'nostr' - ? nostrEvents.length - : cacheEvents.length - - // For relays/hashtags tabs, we need to check the filtered length, not raw cache length - // because filtering might reduce the available items - const actualAvailableLength = activeTab === 'nostr' - ? totalAvailableLength - : relaysFilteredEventsAll.length + const actualAvailableLength = relaysFilteredEventsAll.length const shouldShowLoading = - (activeTab === 'nostr' && nostrLoading) || - ((activeTab === 'relays' || activeTab === 'hashtags') && cacheLoading) || + cacheLoading || showCount < actualAvailableLength if (shouldShowLoading) { diff --git a/src/services/client.service.ts b/src/services/client.service.ts index d7110939..9f2f5975 100644 --- a/src/services/client.service.ts +++ b/src/services/client.service.ts @@ -26,7 +26,6 @@ import { nip19, Relay, SimplePool, - validateEvent, VerifiedEvent } from 'nostr-tools' import { AbstractRelay } from 'nostr-tools/abstract-relay' @@ -62,8 +61,6 @@ class ClientService extends EventTarget { this.fetchEventsFromBigRelays.bind(this), { cache: false, batchScheduleFn: (callback) => setTimeout(callback, 50) } ) - private trendingNotesCache: NEvent[] | null = null - private userIndex = new FlexSearch.Index({ tokenize: 'forward' }) @@ -1183,61 +1180,6 @@ class ClientService extends EventTarget { return this.eventDataLoader.load(id) } - async fetchTrendingNotes() { - if (this.trendingNotesCache) { - return this.trendingNotesCache - } - - try { - // Create a timeout promise that rejects after 5 seconds - const timeoutPromise = new Promise((_, reject) => { - setTimeout(() => { - reject(new Error('TIMEOUT')) - }, 5000) - }) - - // Race between the fetch and timeout - const response = await Promise.race([ - fetch('https://api.nostr.band/v0/trending/notes'), - timeoutPromise - ]) - - if (!response.ok) { - throw new Error(`HTTP ${response.status}`) - } - - const data = await response.json() - const events: NEvent[] = [] - for (const note of data.notes ?? []) { - if (validateEvent(note.event)) { - events.push(note.event) - this.addEventToCache(note.event) - if (note.relays?.length) { - note.relays.map((r: string) => { - try { - const relay = new Relay(r) - this.trackEventSeenOn(note.event.id, relay) - } catch { - return null - } - }) - } - } - } - this.trendingNotesCache = events - return this.trendingNotesCache - } catch (error) { - // Re-throw timeout errors so the component can handle them - // Don't cache on timeout - let the component handle the error state - if (error instanceof Error && error.message === 'TIMEOUT') { - throw error - } - // For other errors, return empty array and cache it (existing behavior) - this.trendingNotesCache = [] - return [] - } - } - addEventToCache(event: NEvent) { // Remove relayStatuses before caching (it's metadata for logging, not part of the event) const cleanEvent = { ...event } as NEvent @@ -1657,7 +1599,6 @@ class ClientService extends EventTarget { this.relayListRequestCache.clear() this.eventDataLoader.clearAll() this.replaceableEventFromBigRelaysDataloader.clearAll() - this.trendingNotesCache = null this.followingFavoriteRelaysCache?.clear() logger.info('[ClientService] In-memory caches cleared') }