import { DEFAULT_FAVORITE_RELAYS } from '@/constants' import { getFavoritesFeedRelayUrls } from '@/lib/favorites-feed-relays' import { getRelaySetFromEvent } from '@/lib/event-metadata' import logger from '@/lib/logger' import { isHttpRelayUrl, isWebsocketUrl, normalizeAnyRelayUrl } from '@/lib/url' import indexedDb from '@/services/indexed-db.service' import storage from '@/services/local-storage.service' import { TFeedInfo, TFeedType } from '@/types' import { kinds } from 'nostr-tools' import { useEffect, useRef, useState, useCallback } from 'react' import { FeedContext } from './feed-context' import { useFavoriteRelays } from './FavoriteRelaysProvider' import { useNostr } from './NostrProvider' export { useFeed } from './feed-context' export type { TFeedContext } from './feed-context' export function FeedProvider({ children }: { children: React.ReactNode }) { const { pubkey, isInitialized } = useNostr() const { relaySets, favoriteRelays, blockedRelays } = useFavoriteRelays() const [relayUrls, setRelayUrls] = useState([]) const [isReady, setIsReady] = useState(false) const [feedInfo, setFeedInfo] = useState({ feedType: 'relay', id: DEFAULT_FAVORITE_RELAYS[0] }) const feedInfoRef = useRef(feedInfo) const loggedWaitingForNostrInitRef = useRef(false) const switchFeed = useCallback(async ( feedType: TFeedType, options: { activeRelaySetId?: string | null pubkey?: string | null relay?: string | null } = {} ) => { logger.debug('switchFeed called:', { feedType, options }) setIsReady(false) if (feedType === 'relay') { const normalizedUrl = normalizeAnyRelayUrl(options.relay ?? '') const isRelayFeedUrl = !!normalizedUrl && (isHttpRelayUrl(normalizedUrl) || isWebsocketUrl(normalizedUrl)) logger.debug('Relay switchFeed:', { normalizedUrl, isRelayFeedUrl, blockedRelays }) if (!isRelayFeedUrl) { logger.debug('Invalid relay URL, setting isReady to true') setIsReady(true) return } // Don't allow selecting a blocked relay as feed if (blockedRelays.includes(normalizedUrl)) { logger.warn('Cannot select blocked relay as feed:', normalizedUrl) setIsReady(true) return } const newFeedInfo = { feedType, id: normalizedUrl } logger.component('FeedProvider', 'Setting relay feed info', newFeedInfo) setFeedInfo(newFeedInfo) feedInfoRef.current = newFeedInfo setRelayUrls([normalizedUrl]) logger.component('FeedProvider', 'Set relayUrls', { relayUrls: [normalizedUrl] }) storage.setFeedInfo(newFeedInfo, pubkey) // Reset note list mode to 'posts' when switching to relay feed to ensure main content is shown storage.setNoteListMode('posts') setIsReady(true) logger.component('FeedProvider', 'Relay feed setup complete, isReady set to true') return } if (feedType === 'relays') { const relaySetId = options.activeRelaySetId ?? (relaySets.length > 0 ? relaySets[0].id : null) if (!relaySetId || !pubkey) { setIsReady(true) return } let relaySet = relaySets.find((set) => set.id === relaySetId) ?? (relaySets.length > 0 ? relaySets[0] : null) if (!relaySet) { const storedRelaySetEvent = await indexedDb.getReplaceableEvent( pubkey, kinds.Relaysets, relaySetId ) if (storedRelaySetEvent) { relaySet = getRelaySetFromEvent(storedRelaySetEvent, blockedRelays) } } if (relaySet) { const newFeedInfo = { feedType, id: relaySet.id } setFeedInfo(newFeedInfo) feedInfoRef.current = newFeedInfo setRelayUrls(relaySet.relayUrls) storage.setFeedInfo(newFeedInfo, pubkey) // Reset note list mode to 'posts' when switching to relay set to ensure main content is shown storage.setNoteListMode('posts') setIsReady(true) } setIsReady(true) return } if (feedType === 'all-favorites') { const finalRelays = getFavoritesFeedRelayUrls(favoriteRelays, blockedRelays) logger.debug('Switching to all-favorites, finalRelays:', finalRelays) const newFeedInfo = { feedType } setFeedInfo(newFeedInfo) feedInfoRef.current = newFeedInfo setRelayUrls(finalRelays) storage.setFeedInfo(newFeedInfo, pubkey) // Reset note list mode to 'posts' when switching to all-favorites to ensure main content is shown storage.setNoteListMode('posts') setIsReady(true) return } setIsReady(true) }, [pubkey, favoriteRelays, blockedRelays, relaySets]) useEffect(() => { const init = async () => { logger.debug('FeedProvider init:', { isInitialized, pubkey, favoriteRelays: favoriteRelays.length, blockedRelays: blockedRelays.length }) if (!isInitialized) { if (!loggedWaitingForNostrInitRef.current) { loggedWaitingForNostrInitRef.current = true logger.info( '[FeedProvider] Waiting for Nostr session restore before attaching feeds (home may show a loading state)' ) } return } loggedWaitingForNostrInitRef.current = false // Wait for favoriteRelays to be initialized (should have at least default relays) // If favoriteRelays is empty, it might not be initialized yet, so wait if (favoriteRelays.length === 0 && !pubkey) { // For anonymous users, favoriteRelays should be initialized from FAST_READ_RELAY_URLS // If it's still empty, something is wrong, but we'll use defaults logger.debug('FeedProvider: favoriteRelays is empty, using defaults') } const favoritesFeedRelays = getFavoritesFeedRelayUrls(favoriteRelays, blockedRelays) let feedInfo: TFeedInfo = { feedType: 'relay', id: favoritesFeedRelays[0] ?? DEFAULT_FAVORITE_RELAYS[0] } // Ensure we always have a valid relay ID if (!feedInfo.id) { feedInfo.id = DEFAULT_FAVORITE_RELAYS[0] } logger.debug('Initial feedInfo setup:', { favoritesFeedRelays, favoriteRelays, blockedRelays, feedInfo }) if (pubkey) { const storedFeedInfo = storage.getFeedInfo(pubkey) logger.debug('Stored feed info:', storedFeedInfo) if (storedFeedInfo) { feedInfo = storedFeedInfo } } // Pre-rewrite main feeds (`following`, `bookmarks`) are no longer supported; migrate persisted state. const storedFeedType = (feedInfo as { feedType?: string }).feedType const deprecatedMainFeed = storedFeedType === 'following' || storedFeedType === 'bookmarks' if (deprecatedMainFeed) { const previousMainFeed = storedFeedType const migrated: TFeedInfo = { feedType: 'all-favorites' } feedInfo = migrated if (pubkey) { storage.setFeedInfo(migrated, pubkey) } logger.info('[FeedProvider] Migrated deprecated feed type to all-favorites', { previous: previousMainFeed }) return await switchFeed('all-favorites') } if (feedInfo.feedType === 'relays') { return await switchFeed('relays', { activeRelaySetId: feedInfo.id }) } if (feedInfo.feedType === 'relay') { // Check if the stored relay is blocked, if so use first visible relay instead if (feedInfo.id && blockedRelays.includes(feedInfo.id)) { logger.component('FeedProvider', 'Stored relay is blocked, using first visible relay instead') feedInfo.id = favoritesFeedRelays[0] ?? DEFAULT_FAVORITE_RELAYS[0] } logger.component('FeedProvider', 'Initial relay setup, calling switchFeed', { relayId: feedInfo.id }) return await switchFeed('relay', { relay: feedInfo.id }) } if (feedInfo.feedType === 'all-favorites') { logger.debug('Initializing all-favorites feed') return await switchFeed('all-favorites') } } init() }, [pubkey, isInitialized, favoriteRelays, blockedRelays, switchFeed]) // Update relay URLs when favoriteRelays change and we're in all-favorites mode useEffect(() => { if (feedInfo.feedType !== 'all-favorites') return const finalRelays = getFavoritesFeedRelayUrls(favoriteRelays, blockedRelays) logger.debug('Updating relay URLs for all-favorites:', finalRelays) setRelayUrls(finalRelays) }, [feedInfo.feedType, favoriteRelays, blockedRelays]) return ( {children} ) }