diff --git a/src/components/NoteList/index.tsx b/src/components/NoteList/index.tsx index d25bce2c..475ec745 100644 --- a/src/components/NoteList/index.tsx +++ b/src/components/NoteList/index.tsx @@ -219,7 +219,10 @@ const NoteList = forwardRef( if (!subRequests.length) { logger.warn('[NoteList] subRequests is empty, not initializing') - return + setLoading(false) + setEvents([]) + // Return a no-op closer function to satisfy the cleanup function + return () => {} } async function init() { @@ -236,26 +239,61 @@ const NoteList = forwardRef( setHasMore(true) consecutiveEmptyRef.current = 0 // Reset counter on refresh - const mappedSubRequests = subRequests.map(({ urls, filter }) => ({ - urls, - filter: useFilterAsIs - ? { ...filter, limit: filter.limit ?? (areAlgoRelays ? ALGO_LIMIT : LIMIT) } + const mappedSubRequests = subRequests.map(({ urls, filter }) => { + // CRITICAL: Always ensure filter has kinds - relays require this to return events + const defaultKinds = showKinds.length > 0 ? showKinds : [kinds.ShortTextNote] + const finalFilter = useFilterAsIs + ? { + ...filter, + // If filter doesn't have kinds, add them (required for relay queries) + kinds: filter.kinds && filter.kinds.length > 0 ? filter.kinds : defaultKinds, + limit: filter.limit ?? (areAlgoRelays ? ALGO_LIMIT : LIMIT) + } : { ...filter, // If showKinds is empty, default to kind 1 (ShortTextNote) only - kinds: showKinds.length > 0 ? showKinds : [kinds.ShortTextNote], + kinds: defaultKinds, limit: areAlgoRelays ? ALGO_LIMIT : LIMIT } - })) + + // CRITICAL: Validate filter has kinds before subscribing + if (!finalFilter.kinds || finalFilter.kinds.length === 0) { + logger.error('[NoteList] Filter missing kinds! Using default', { + originalFilter: filter, + showKinds, + useFilterAsIs + }) + finalFilter.kinds = [kinds.ShortTextNote] + } + + return { urls, filter: finalFilter } + }) logger.debug('[NoteList] Subscribing with filters', { subRequestCount: mappedSubRequests.length, filters: mappedSubRequests.map(({ urls, filter }) => ({ urls: urls.slice(0, 2), // Log first 2 URLs kinds: filter.kinds, - limit: filter.limit + limit: filter.limit, + hasKinds: !!(filter.kinds && filter.kinds.length > 0) })) }) + + // CRITICAL: Validate all filters have kinds before subscribing + const invalidFilters = mappedSubRequests.filter(({ filter }) => !filter.kinds || filter.kinds.length === 0) + if (invalidFilters.length > 0) { + logger.error('[NoteList] CRITICAL: Some filters are missing kinds!', { + invalidCount: invalidFilters.length, + totalCount: mappedSubRequests.length, + showKinds, + useFilterAsIs + }) + // Don't subscribe with invalid filters - this would return no events + setLoading(false) + setEvents([]) + // Return a no-op closer function to satisfy the cleanup function + return () => {} + } logger.info('[NoteList] About to call subscribeTimeline', { mappedSubRequestsCount: mappedSubRequests.length @@ -265,7 +303,15 @@ const NoteList = forwardRef( let timelineKey: string | undefined try { - const result = await client.subscribeTimeline( + // Add timeout wrapper to prevent subscribeTimeline from hanging indefinitely + const timeoutPromise = new Promise((_, reject) => { + setTimeout(() => { + reject(new Error('subscribeTimeline timeout after 5 seconds')) + }, 5000) // 5 second timeout + }) + + const result = await Promise.race([ + client.subscribeTimeline( mappedSubRequests, { onEvents: (events: Event[], eosed: boolean) => { @@ -394,9 +440,11 @@ const NoteList = forwardRef( needSort: !areAlgoRelays, useCache: false // Main feeds should always fetch fresh from relays, not use cache } - ) - closer = result.closer - timelineKey = result.timelineKey + ), + timeoutPromise + ]) + closer = result.closer + timelineKey = result.timelineKey logger.info('[NoteList] subscribeTimeline completed', { hasTimelineKey: !!timelineKey, hasCloser: !!closer @@ -409,13 +457,15 @@ const NoteList = forwardRef( stack: error instanceof Error ? error.stack : undefined }) setLoading(false) - throw error + // Return a no-op closer function instead of throwing - allows cleanup to work + // The error is already logged, no need to crash the component + return () => {} } } const promise = init() return () => { - promise.then((closer) => closer()) + promise.then((closer) => closer?.()) } }, [subRequestsKey, refreshCount, showKinds, showKind1OPs, showKind1Replies, showKind1111, useFilterAsIs]) diff --git a/src/pages/primary/NoteListPage/RelaysFeed.tsx b/src/pages/primary/NoteListPage/RelaysFeed.tsx index 316e4019..6d073a34 100644 --- a/src/pages/primary/NoteListPage/RelaysFeed.tsx +++ b/src/pages/primary/NoteListPage/RelaysFeed.tsx @@ -2,8 +2,10 @@ import NormalFeed from '@/components/NormalFeed' import { checkAlgoRelay } from '@/lib/relay' import logger from '@/lib/logger' import { useFeed } from '@/providers/FeedProvider' +import { useKindFilter } from '@/providers/KindFilterProvider' import relayInfoService from '@/services/relay-info.service' -import React, { useEffect, useMemo, useState } from 'react' +import { kinds } from 'nostr-tools' +import React, { useEffect, useMemo, useState, useRef } from 'react' export default function RelaysFeed({ setSubHeader @@ -12,30 +14,32 @@ export default function RelaysFeed({ }) { logger.debug('RelaysFeed component rendering') const { feedInfo, relayUrls } = useFeed() - const [isReady, setIsReady] = useState(false) + const { showKinds } = useKindFilter() const [areAlgoRelays, setAreAlgoRelays] = useState(false) + const relayInfoFetchedRef = useRef(false) // Debug logging logger.debug('RelaysFeed debug:', { feedInfo, - relayUrls, - isReady + relayUrls: relayUrls.length, + showKinds: showKinds.length }) + // Fetch relay info in background (non-blocking) - don't wait for it to render useEffect(() => { + // Only fetch once per relayUrls change + if (relayInfoFetchedRef.current || relayUrls.length === 0) { + return + } + const init = async () => { - // If relayUrls is empty, we can't initialize the feed - if (relayUrls.length === 0) { - logger.debug('RelaysFeed: relayUrls is empty, not initializing') - setIsReady(false) - return - } + relayInfoFetchedRef.current = true - // Add timeout to prevent hanging if getRelayInfos is slow + // Add aggressive timeout to prevent hanging (reduced from 5s to 2s) const timeoutPromise = new Promise((_, reject) => { setTimeout(() => { - reject(new Error('getRelayInfos timeout after 5 seconds')) - }, 5000) + reject(new Error('getRelayInfos timeout after 2 seconds')) + }, 2000) }) try { @@ -43,39 +47,64 @@ export default function RelaysFeed({ relayInfoService.getRelayInfos(relayUrls), timeoutPromise ]) - setAreAlgoRelays(relayInfos.every((relayInfo) => checkAlgoRelay(relayInfo))) - setIsReady(true) - logger.debug('RelaysFeed: Initialized successfully', { + const areAlgo = relayInfos.every((relayInfo) => checkAlgoRelay(relayInfo)) + setAreAlgoRelays(areAlgo) + logger.debug('RelaysFeed: Relay info fetched successfully', { relayCount: relayUrls.length, - areAlgoRelays: relayInfos.every((relayInfo) => checkAlgoRelay(relayInfo)) + areAlgoRelays: areAlgo }) } catch (error) { - logger.warn('RelaysFeed: Failed to get relay infos, proceeding anyway', { + logger.debug('RelaysFeed: Failed to get relay infos (non-blocking)', { error: error instanceof Error ? error.message : String(error), - relayUrls + relayUrls: relayUrls.length }) - // Proceed anyway - we can still show the feed even without relay info + // Default to false - feed will work without this info setAreAlgoRelays(false) - setIsReady(true) } } - init() + + // Don't await - let it run in background + init().catch((err) => { + logger.debug('RelaysFeed: Unhandled error in init', { error: err }) + setAreAlgoRelays(false) + }) }, [relayUrls]) - // Memoize subRequests before any early returns to avoid Rules of Hooks violation - const subRequests = useMemo(() => [{ urls: relayUrls, filter: {} }], [relayUrls]) + // Reset fetch flag when relayUrls change + useEffect(() => { + relayInfoFetchedRef.current = false + }, [relayUrls]) - if (!isReady) { + // Early returns for invalid feed types + if (feedInfo.feedType !== 'relay' && feedInfo.feedType !== 'relays' && feedInfo.feedType !== 'all-favorites') { return null } - if (feedInfo.feedType !== 'relay' && feedInfo.feedType !== 'relays' && feedInfo.feedType !== 'all-favorites') { + // CRITICAL: Don't render feed if relayUrls is empty - this would cause subscription to fail + if (relayUrls.length === 0) { + logger.debug('RelaysFeed: relayUrls is empty, not rendering feed') return null } + + // CRITICAL: Provide proper filter with default kinds - NoteList requires kinds in filter + // Use showKinds from KindFilterProvider if available, otherwise default to kind 1 + const defaultKinds = showKinds.length > 0 ? showKinds : [kinds.ShortTextNote] + + // Memoize subRequests with proper filter - this ensures NoteList gets valid filter + const subRequests = useMemo(() => { + return [{ + urls: relayUrls, + filter: { + kinds: defaultKinds + } + }] + }, [relayUrls, defaultKinds]) + logger.component('RelaysFeed', 'Rendering NormalFeed', { subRequests: subRequests.length, relayUrls: relayUrls.length, - areAlgoRelays + areAlgoRelays, + filterKinds: subRequests[0]?.filter?.kinds?.length || 0 }) return (