3 changed files with 312 additions and 124 deletions
@ -0,0 +1,194 @@ |
|||||||
|
import { forwardRef, useEffect, useState, useCallback, useMemo } from 'react' |
||||||
|
import { useTranslation } from 'react-i18next' |
||||||
|
import { RefreshCw } from 'lucide-react' |
||||||
|
import { useNostr } from '@/providers/NostrProvider' |
||||||
|
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' |
||||||
|
import { normalizeUrl } from '@/lib/url' |
||||||
|
import { BIG_RELAY_URLS, FAST_READ_RELAY_URLS, FAST_WRITE_RELAY_URLS } from '@/constants' |
||||||
|
import client from '@/services/client.service' |
||||||
|
import { Event } from 'nostr-tools' |
||||||
|
import { kinds } from 'nostr-tools' |
||||||
|
import logger from '@/lib/logger' |
||||||
|
import NoteCard from '@/components/NoteCard' |
||||||
|
|
||||||
|
type TSimpleNoteFeedProps = { |
||||||
|
authors?: string[] |
||||||
|
kinds?: number[] |
||||||
|
limit?: number |
||||||
|
hideReplies?: boolean |
||||||
|
filterMutedNotes?: boolean |
||||||
|
customHeader?: React.ReactNode |
||||||
|
} |
||||||
|
|
||||||
|
const SimpleNoteFeed = forwardRef< |
||||||
|
{ refresh: () => void }, |
||||||
|
TSimpleNoteFeedProps |
||||||
|
>(({ |
||||||
|
authors = [], |
||||||
|
kinds: requestedKinds = [kinds.ShortTextNote, kinds.Repost, kinds.Highlights, kinds.LongFormArticle], |
||||||
|
limit = 100, |
||||||
|
hideReplies = false, |
||||||
|
filterMutedNotes = false, |
||||||
|
customHeader |
||||||
|
}, ref) => { |
||||||
|
const { t } = useTranslation() |
||||||
|
const { pubkey } = useNostr() |
||||||
|
const { favoriteRelays } = useFavoriteRelays() |
||||||
|
const [events, setEvents] = useState<Event[]>([]) |
||||||
|
const [loading, setLoading] = useState(true) |
||||||
|
const [isRefreshing, setIsRefreshing] = useState(false) |
||||||
|
|
||||||
|
// Build comprehensive relay list (same as Discussions)
|
||||||
|
const buildComprehensiveRelayList = useCallback(async () => { |
||||||
|
const myRelayList = pubkey ? await client.fetchRelayList(pubkey) : { write: [], read: [] } |
||||||
|
const allRelays = [ |
||||||
|
...(myRelayList.read || []), // User's inboxes (kind 10002)
|
||||||
|
...(myRelayList.write || []), // User's outboxes (kind 10002)
|
||||||
|
...(favoriteRelays || []), // User's favorite relays (kind 10012)
|
||||||
|
...BIG_RELAY_URLS, // Big relays
|
||||||
|
...FAST_READ_RELAY_URLS, // Fast read relays
|
||||||
|
...FAST_WRITE_RELAY_URLS // Fast write relays
|
||||||
|
] |
||||||
|
|
||||||
|
// Normalize and deduplicate relay URLs
|
||||||
|
const normalizedRelays = allRelays |
||||||
|
.map(url => normalizeUrl(url)) |
||||||
|
.filter((url): url is string => !!url) |
||||||
|
|
||||||
|
logger.debug('[SimpleNoteFeed] Using', normalizedRelays.length, 'comprehensive relays') |
||||||
|
return Array.from(new Set(normalizedRelays)) |
||||||
|
}, [pubkey, favoriteRelays]) |
||||||
|
|
||||||
|
// Fetch events using the same pattern as Discussions
|
||||||
|
const fetchEvents = useCallback(async () => { |
||||||
|
if (loading && !isRefreshing) return |
||||||
|
setLoading(true) |
||||||
|
setIsRefreshing(true) |
||||||
|
|
||||||
|
try { |
||||||
|
logger.debug('[SimpleNoteFeed] Fetching events...', { authors, kinds: requestedKinds, limit }) |
||||||
|
|
||||||
|
// Get comprehensive relay list
|
||||||
|
const allRelays = await buildComprehensiveRelayList() |
||||||
|
|
||||||
|
// Build filter
|
||||||
|
const filter: any = { |
||||||
|
kinds: requestedKinds, |
||||||
|
limit |
||||||
|
} |
||||||
|
|
||||||
|
if (authors.length > 0) { |
||||||
|
filter.authors = authors |
||||||
|
} |
||||||
|
|
||||||
|
logger.debug('[SimpleNoteFeed] Using filter:', filter) |
||||||
|
|
||||||
|
// Fetch events
|
||||||
|
const fetchedEvents = await client.fetchEvents(allRelays, [filter]) |
||||||
|
|
||||||
|
logger.debug('[SimpleNoteFeed] Fetched', fetchedEvents.length, 'events') |
||||||
|
|
||||||
|
// Filter events (basic filtering)
|
||||||
|
const filteredEvents = fetchedEvents.filter(event => { |
||||||
|
// Skip deleted events
|
||||||
|
if (event.content === '') return false |
||||||
|
|
||||||
|
// Skip replies if hideReplies is true
|
||||||
|
if (hideReplies && event.tags.some(tag => tag[0] === 'e' && tag[1])) { |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
return true |
||||||
|
}) |
||||||
|
|
||||||
|
logger.debug('[SimpleNoteFeed] Filtered to', filteredEvents.length, 'events') |
||||||
|
|
||||||
|
setEvents(filteredEvents) |
||||||
|
} catch (error) { |
||||||
|
logger.error('[SimpleNoteFeed] Error fetching events:', error) |
||||||
|
} finally { |
||||||
|
setLoading(false) |
||||||
|
setIsRefreshing(false) |
||||||
|
} |
||||||
|
}, [authors, requestedKinds, limit, hideReplies, buildComprehensiveRelayList, loading, isRefreshing]) |
||||||
|
|
||||||
|
// Initial fetch
|
||||||
|
useEffect(() => { |
||||||
|
fetchEvents() |
||||||
|
}, [authors, requestedKinds, limit, hideReplies]) |
||||||
|
|
||||||
|
// Expose refresh method
|
||||||
|
useEffect(() => { |
||||||
|
if (ref && typeof ref === 'object') { |
||||||
|
ref.current = { |
||||||
|
refresh: fetchEvents |
||||||
|
} |
||||||
|
} |
||||||
|
}, [ref, fetchEvents]) |
||||||
|
|
||||||
|
const handleRefresh = () => { |
||||||
|
fetchEvents() |
||||||
|
} |
||||||
|
|
||||||
|
if (loading && events.length === 0) { |
||||||
|
return ( |
||||||
|
<div className="min-h-screen"> |
||||||
|
{customHeader} |
||||||
|
<div className="flex items-center justify-center p-8"> |
||||||
|
<div className="text-center"> |
||||||
|
<RefreshCw className="h-8 w-8 animate-spin mx-auto mb-4" /> |
||||||
|
<p className="text-muted-foreground">{t('loading...')}</p> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
return ( |
||||||
|
<div className="min-h-screen"> |
||||||
|
{customHeader} |
||||||
|
|
||||||
|
{/* Refresh button */} |
||||||
|
<div className="flex justify-end p-4"> |
||||||
|
<button |
||||||
|
onClick={handleRefresh} |
||||||
|
disabled={isRefreshing} |
||||||
|
className="flex items-center gap-2 px-4 py-2 text-sm bg-muted hover:bg-muted/80 rounded-md disabled:opacity-50" |
||||||
|
> |
||||||
|
<RefreshCw className={`h-4 w-4 ${isRefreshing ? 'animate-spin' : ''}`} /> |
||||||
|
{isRefreshing ? t('refreshing...') : t('refresh')} |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
|
||||||
|
{/* Events list */} |
||||||
|
{events.length > 0 ? ( |
||||||
|
<div className="space-y-4"> |
||||||
|
{events.map((event) => ( |
||||||
|
<NoteCard |
||||||
|
key={event.id} |
||||||
|
className="w-full" |
||||||
|
event={event} |
||||||
|
filterMutedNotes={filterMutedNotes} |
||||||
|
/> |
||||||
|
))} |
||||||
|
</div> |
||||||
|
) : ( |
||||||
|
<div className="flex justify-center w-full mt-8"> |
||||||
|
<div className="text-center"> |
||||||
|
<p className="text-muted-foreground mb-4">{t('no notes found')}</p> |
||||||
|
<button |
||||||
|
onClick={handleRefresh} |
||||||
|
className="px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90" |
||||||
|
> |
||||||
|
{t('reload notes')} |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
)} |
||||||
|
</div> |
||||||
|
) |
||||||
|
}) |
||||||
|
|
||||||
|
SimpleNoteFeed.displayName = 'SimpleNoteFeed' |
||||||
|
|
||||||
|
export default SimpleNoteFeed |
||||||
Loading…
Reference in new issue