Browse Source

remove relay.band api

imwald
Silberengel 3 days ago
parent
commit
233b42a87f
  1. 43
      src/PageManager.tsx
  2. 144
      src/components/TrendingNotes/index.tsx
  3. 59
      src/services/client.service.ts

43
src/PageManager.tsx

@ -566,7 +566,7 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) {
const savedFeedStateRef = useRef<Map<TPrimaryPageName, { const savedFeedStateRef = useRef<Map<TPrimaryPageName, {
tab?: string, tab?: string,
discussionsState?: { selectedTopic: string, timeSpan: '30days' | '90days' | 'all' }, discussionsState?: { selectedTopic: string, timeSpan: '30days' | '90days' | 'all' },
trendingTab?: 'nostr' | 'relays' | 'hashtags' trendingTab?: 'relays' | 'hashtags'
}>>(new Map()) }>>(new Map())
const currentTabStateRef = useRef<Map<TPrimaryPageName, string>>(new Map()) // Track current tab state for each page const currentTabStateRef = useRef<Map<TPrimaryPageName, string>>(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 // 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 // Save state (tab, discussions, trending) if any exists
if (currentTab || discussionsState || trendingTab) { 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') { if (savedFeedState?.trendingTab && savedPrimaryPage === 'search') {
const tab = (savedFeedState.trendingTab as string) === 'nostr' ? 'relays' : savedFeedState.trendingTab
logger.info('PageManager: Restoring trending tab', { logger.info('PageManager: Restoring trending tab', {
page: savedPrimaryPage, page: savedPrimaryPage,
trendingTab: savedFeedState.trendingTab trendingTab: tab
}) })
window.dispatchEvent(new CustomEvent('restorePageTab', { 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 // Restore trending tab for search page
if (savedFeedState?.trendingTab && currentPrimaryPage === 'search') { if (savedFeedState?.trendingTab && currentPrimaryPage === 'search') {
const tab = (savedFeedState.trendingTab as string) === 'nostr' ? 'relays' : savedFeedState.trendingTab
logger.info('PageManager: Browser back - Restoring trending tab', { logger.info('PageManager: Browser back - Restoring trending tab', {
page: currentPrimaryPage, page: currentPrimaryPage,
trendingTab: savedFeedState.trendingTab trendingTab: tab
}) })
window.dispatchEvent(new CustomEvent('restorePageTab', { 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)
} }
} }
}, [secondaryStack.length, currentPrimaryPage]) }, [secondaryStack.length, currentPrimaryPage])
@ -1167,7 +1169,7 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) {
// Save tab state before navigating // Save tab state before navigating
const currentTab = currentTabStateRef.current.get(currentPrimaryPage) 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)) { if (currentPrimaryPage && (currentTab || trendingTab)) {
logger.info('PageManager: Desktop - Saving page state', { logger.info('PageManager: Desktop - Saving page state', {
@ -1251,14 +1253,15 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) {
// Restore trending tab for search page // Restore trending tab for search page
if (savedFeedState?.trendingTab && currentPrimaryPage === 'search') { if (savedFeedState?.trendingTab && currentPrimaryPage === 'search') {
const tab = (savedFeedState.trendingTab as string) === 'nostr' ? 'relays' : savedFeedState.trendingTab
logger.info('PageManager: Desktop - Restoring trending tab', { logger.info('PageManager: Desktop - Restoring trending tab', {
page: currentPrimaryPage, page: currentPrimaryPage,
trendingTab: savedFeedState.trendingTab trendingTab: tab
}) })
window.dispatchEvent(new CustomEvent('restorePageTab', { 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)
} }
} else if (secondaryStack.length > 1) { } else if (secondaryStack.length > 1) {
// Pop from stack directly instead of using history.go(-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 // Restore trending tab for search page
if (savedFeedState?.trendingTab && currentPrimaryPage === 'search') { if (savedFeedState?.trendingTab && currentPrimaryPage === 'search') {
const tab = (savedFeedState.trendingTab as string) === 'nostr' ? 'relays' : savedFeedState.trendingTab
logger.info('PageManager: Mobile/Single-pane - Restoring trending tab', { logger.info('PageManager: Mobile/Single-pane - Restoring trending tab', {
page: currentPrimaryPage, page: currentPrimaryPage,
trendingTab: savedFeedState.trendingTab trendingTab: tab
}) })
window.dispatchEvent(new CustomEvent('restorePageTab', { 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)
} }
return return
} }
@ -1360,14 +1364,15 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) {
// Restore trending tab for search page // Restore trending tab for search page
if (savedFeedState?.trendingTab && currentPrimaryPage === 'search') { if (savedFeedState?.trendingTab && currentPrimaryPage === 'search') {
const tab = (savedFeedState.trendingTab as string) === 'nostr' ? 'relays' : savedFeedState.trendingTab
logger.info('PageManager: Desktop - Restoring trending tab', { logger.info('PageManager: Desktop - Restoring trending tab', {
page: currentPrimaryPage, page: currentPrimaryPage,
trendingTab: savedFeedState.trendingTab trendingTab: tab
}) })
window.dispatchEvent(new CustomEvent('restorePageTab', { 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)
} }
} else { } else {
window.history.go(-1) window.history.go(-1)

144
src/components/TrendingNotes/index.tsx

@ -27,7 +27,7 @@ let cachedCustomEvents: {
// Flag to prevent concurrent initialization // Flag to prevent concurrent initialization
let isInitializing = false let isInitializing = false
type TrendingTab = 'nostr' | 'relays' | 'hashtags' type TrendingTab = 'relays' | 'hashtags'
type SortOrder = 'newest' | 'oldest' | 'most-popular' | 'least-popular' type SortOrder = 'newest' | 'oldest' | 'most-popular' | 'least-popular'
type HashtagFilter = 'popular' type HashtagFilter = 'popular'
@ -38,9 +38,6 @@ export default function TrendingNotes() {
const { pubkey, relayList } = useNostr() const { pubkey, relayList } = useNostr()
const { favoriteRelays } = useFavoriteRelays() const { favoriteRelays } = useFavoriteRelays()
const { zapReplyThreshold } = useZap() const { zapReplyThreshold } = useZap()
const [nostrEvents, setNostrEvents] = useState<NostrEvent[]>([])
const [nostrLoading, setNostrLoading] = useState(false)
const [nostrError, setNostrError] = useState<string | null>(null)
const [showCount, setShowCount] = useState(SHOW_COUNT) const [showCount, setShowCount] = useState(SHOW_COUNT)
const [activeTab, setActiveTab] = useState<TrendingTab>('relays') const [activeTab, setActiveTab] = useState<TrendingTab>('relays')
const [sortOrder, setSortOrder] = useState<SortOrder>('most-popular') const [sortOrder, setSortOrder] = useState<SortOrder>('most-popular')
@ -50,23 +47,11 @@ export default function TrendingNotes() {
const [cacheEvents, setCacheEvents] = useState<NostrEvent[]>([]) const [cacheEvents, setCacheEvents] = useState<NostrEvent[]>([])
const [cacheLoading, setCacheLoading] = useState(false) const [cacheLoading, setCacheLoading] = useState(false)
const bottomRef = useRef<HTMLDivElement>(null) const bottomRef = useRef<HTMLDivElement>(null)
const isFetchingNostrRef = useRef(false)
const hasUserClickedNostrTabRef = useRef(false)
// Listen for tab restoration from PageManager // Listen for tab restoration from PageManager
useEffect(() => { useEffect(() => {
const handleRestore = (e: CustomEvent<{ page: string, tab: string }>) => { const handleRestore = (e: CustomEvent<{ page: string, tab: string }>) => {
if (e.detail.page === 'search' && e.detail.tab && ['nostr', 'relays', 'hashtags'].includes(e.detail.tab)) { if (e.detail.page === 'search' && e.detail.tab && ['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
setActiveTab(e.detail.tab as TrendingTab) setActiveTab(e.detail.tab as TrendingTab)
} }
} }
@ -74,48 +59,6 @@ export default function TrendingNotes() {
return () => window.removeEventListener('restorePageTab', handleRestore as EventListener) 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 // Debug: Track cacheEvents changes
useEffect(() => { useEffect(() => {
logger.debug('[TrendingNotes] cacheEvents state changed:', cacheEvents.length, 'events') 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) // Calculate popular hashtags from cache events (all events from relays)
const calculatePopularHashtags = useMemo(() => { 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
const eventsToAnalyze = cacheEvents.length > 0 ? cacheEvents : nostrEvents
if (eventsToAnalyze.length === 0) { if (eventsToAnalyze.length === 0) {
return [] return []
@ -178,7 +120,7 @@ export default function TrendingNotes() {
logger.debug('[TrendingNotes] calculatePopularHashtags - eventsWithHashtags:', eventsWithHashtags) logger.debug('[TrendingNotes] calculatePopularHashtags - eventsWithHashtags:', eventsWithHashtags)
return result return result
}, [cacheEvents, nostrEvents, activeTab, hashtagFilter, pubkey]) }, [cacheEvents, activeTab, hashtagFilter, pubkey])
// Get relays based on user login status // Get relays based on user login status
const getRelays = useMemo(() => { const getRelays = useMemo(() => {
@ -214,15 +156,6 @@ export default function TrendingNotes() {
setPopularHashtags(calculatePopularHashtags) setPopularHashtags(calculatePopularHashtags)
}, [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 // Initialize cache only once on mount
useEffect(() => { useEffect(() => {
const initializeCache = async () => { const initializeCache = async () => {
@ -436,7 +369,7 @@ export default function TrendingNotes() {
// Compute filtered events without slicing (for pagination length check) // Compute filtered events without slicing (for pagination length check)
const relaysFilteredEventsAll = useMemo(() => { const relaysFilteredEventsAll = useMemo(() => {
const idSet = new Set<string>() const idSet = new Set<string>()
const sourceEvents = cacheEvents.length > 0 ? cacheEvents : nostrEvents const sourceEvents = cacheEvents
const filtered = sourceEvents.filter((evt) => { const filtered = sourceEvents.filter((evt) => {
if (isEventDeleted(evt)) return false if (isEventDeleted(evt)) return false
@ -519,7 +452,6 @@ export default function TrendingNotes() {
return filtered return filtered
}, [ }, [
cacheEvents, cacheEvents,
nostrEvents,
hideUntrustedNotes, hideUntrustedNotes,
isEventDeleted, isEventDeleted,
isUserTrusted, isUserTrusted,
@ -536,11 +468,8 @@ export default function TrendingNotes() {
}, [relaysFilteredEventsAll, showCount]) }, [relaysFilteredEventsAll, showCount])
const filteredEvents = useMemo(() => { const filteredEvents = useMemo(() => {
if (activeTab === 'nostr') {
return nostrEvents.slice(0, showCount)
}
return relaysFilteredEvents return relaysFilteredEvents
}, [activeTab, nostrEvents, showCount, relaysFilteredEvents]) }, [relaysFilteredEvents])
@ -565,12 +494,7 @@ export default function TrendingNotes() {
useEffect(() => { useEffect(() => {
// For relays/hashtags tabs, use the filtered length (before slicing) const totalLength = relaysFilteredEventsAll.length
// For nostr tab, use the raw events length
const totalLength =
activeTab === 'nostr'
? nostrEvents.length
: relaysFilteredEventsAll.length
if (showCount >= totalLength) return if (showCount >= totalLength) return
@ -597,7 +521,7 @@ export default function TrendingNotes() {
observerInstance.unobserve(currentBottomRef) observerInstance.unobserve(currentBottomRef)
} }
} }
}, [activeTab, nostrEvents.length, relaysFilteredEventsAll.length, showCount, cacheLoading, nostrLoading]) }, [relaysFilteredEventsAll.length, showCount, cacheLoading])
return ( return (
<div className="min-h-screen"> <div className="min-h-screen">
@ -638,22 +562,6 @@ export default function TrendingNotes() {
> >
hashtags hashtags
</button> </button>
<button
onClick={() => {
hasUserClickedNostrTabRef.current = true
setActiveTab('nostr')
window.dispatchEvent(new CustomEvent('pageTabChanged', {
detail: { page: 'search', tab: 'nostr' }
}))
}}
className={`px-3 py-1 text-sm rounded-md transition-colors ${
activeTab === 'nostr'
? 'bg-primary text-primary-foreground'
: 'bg-muted hover:bg-muted/80 text-muted-foreground'
}`}
>
on Nostr
</button>
</div> </div>
</div> </div>
@ -738,19 +646,6 @@ export default function TrendingNotes() {
)} )}
</div> </div>
{/* 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 && (
<div className="text-center text-sm text-muted-foreground mt-8 px-4 py-2 bg-muted/50 rounded-md mx-4">
{t('The nostr.band relay appears to be temporarily out of service. Please try again later.')}
</div>
)}
{/* Show loading message for nostr tab (only if not in error state) */}
{activeTab === 'nostr' && nostrLoading && nostrEvents.length === 0 && !nostrError && (
<div className="text-center text-sm text-muted-foreground mt-8">
Loading trending notes from nostr.band...
</div>
)}
{/* Show loading message for relays tab when cache is loading */} {/* Show loading message for relays tab when cache is loading */}
{activeTab === 'relays' && cacheLoading && cacheEvents.length === 0 && ( {activeTab === 'relays' && cacheLoading && cacheEvents.length === 0 && (
<div className="text-center text-sm text-muted-foreground mt-8"> <div className="text-center text-sm text-muted-foreground mt-8">
@ -762,28 +657,11 @@ export default function TrendingNotes() {
<NoteCard key={event.id} className="w-full" event={event} /> <NoteCard key={event.id} className="w-full" event={event} />
))} ))}
{/* Show error message at the end for nostr tab timeout (only if there are events) */}
{activeTab === 'nostr' && nostrError === 'timeout' && !nostrLoading && filteredEvents.length > 0 && (
<div className="text-center text-sm text-muted-foreground mt-4 px-4 py-2 bg-muted/50 rounded-md mx-4">
{t('The nostr.band relay appears to be temporarily out of service. Please try again later.')}
</div>
)}
{(() => { {(() => {
const totalAvailableLength = const actualAvailableLength = relaysFilteredEventsAll.length
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 shouldShowLoading = const shouldShowLoading =
(activeTab === 'nostr' && nostrLoading) || cacheLoading ||
((activeTab === 'relays' || activeTab === 'hashtags') && cacheLoading) ||
showCount < actualAvailableLength showCount < actualAvailableLength
if (shouldShowLoading) { if (shouldShowLoading) {

59
src/services/client.service.ts

@ -26,7 +26,6 @@ import {
nip19, nip19,
Relay, Relay,
SimplePool, SimplePool,
validateEvent,
VerifiedEvent VerifiedEvent
} from 'nostr-tools' } from 'nostr-tools'
import { AbstractRelay } from 'nostr-tools/abstract-relay' import { AbstractRelay } from 'nostr-tools/abstract-relay'
@ -62,8 +61,6 @@ class ClientService extends EventTarget {
this.fetchEventsFromBigRelays.bind(this), this.fetchEventsFromBigRelays.bind(this),
{ cache: false, batchScheduleFn: (callback) => setTimeout(callback, 50) } { cache: false, batchScheduleFn: (callback) => setTimeout(callback, 50) }
) )
private trendingNotesCache: NEvent[] | null = null
private userIndex = new FlexSearch.Index({ private userIndex = new FlexSearch.Index({
tokenize: 'forward' tokenize: 'forward'
}) })
@ -1183,61 +1180,6 @@ class ClientService extends EventTarget {
return this.eventDataLoader.load(id) 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<never>((_, 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) { addEventToCache(event: NEvent) {
// Remove relayStatuses before caching (it's metadata for logging, not part of the event) // Remove relayStatuses before caching (it's metadata for logging, not part of the event)
const cleanEvent = { ...event } as NEvent const cleanEvent = { ...event } as NEvent
@ -1657,7 +1599,6 @@ class ClientService extends EventTarget {
this.relayListRequestCache.clear() this.relayListRequestCache.clear()
this.eventDataLoader.clearAll() this.eventDataLoader.clearAll()
this.replaceableEventFromBigRelaysDataloader.clearAll() this.replaceableEventFromBigRelaysDataloader.clearAll()
this.trendingNotesCache = null
this.followingFavoriteRelaysCache?.clear() this.followingFavoriteRelaysCache?.clear()
logger.info('[ClientService] In-memory caches cleared') logger.info('[ClientService] In-memory caches cleared')
} }

Loading…
Cancel
Save