import RelayReviewCard from '@/components/RelayInfo/RelayReviewCard' import { Skeleton } from '@/components/ui/skeleton' import { ExtendedKind, FIRST_RELAY_RESULT_GRACE_MS } from '@/constants' import { getReplaceableCoordinateFromEvent, isReplaceableEvent } from '@/lib/event' import { getRelayUrlFromRelayReviewEvent } from '@/lib/event-metadata' import { getRelayUrlsWithFavoritesFastReadAndInbox } from '@/lib/favorites-feed-relays' import { appendCuratedReadOnlyRelays } from '@/pages/primary/SpellsPage/fauxSpellFeeds' import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' import { useNostr } from '@/providers/NostrProvider' import client from '@/services/client.service' import { Loader2 } from 'lucide-react' import type { Event } from 'nostr-tools' import { useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' const REVIEW_QUERY_LIMIT = 100 const SHOW_COUNT = 20 /** Fewer sockets + faster aggregate EOSE than full inbox stack; read-only mirrors still appended then capped. */ const EXPLORE_REVIEWS_MAX_RELAYS = 12 /** After all relays EOSE, wait longer than default so slow mirrors can flush events (default query eose is 500ms). */ const EXPLORE_REVIEWS_EOSE_TAIL_MS = 4500 function dedupeRelayReviewsNewestFirst(events: Event[]): Event[] { const sorted = [...events].sort((a, b) => b.created_at - a.created_at) const seen = new Set() const out: Event[] = [] for (const evt of sorted) { const key = isReplaceableEvent(evt.kind) ? getReplaceableCoordinateFromEvent(evt) : evt.id if (seen.has(key)) continue seen.add(key) out.push(evt) } return out } export default function ExploreRelayReviews() { const { t } = useTranslation() const { favoriteRelays, blockedRelays } = useFavoriteRelays() const { relayList } = useNostr() const relayUrls = useMemo(() => { const stacked = appendCuratedReadOnlyRelays( getRelayUrlsWithFavoritesFastReadAndInbox( favoriteRelays, blockedRelays, relayList?.read ?? [], { userWriteRelays: relayList?.write ?? [], maxRelays: EXPLORE_REVIEWS_MAX_RELAYS, applyKind1BlockedFilter: false } ), blockedRelays ) return stacked.slice(0, EXPLORE_REVIEWS_MAX_RELAYS) }, [favoriteRelays, blockedRelays, relayList]) const relayUrlsKey = useMemo(() => relayUrls.join('|'), [relayUrls]) const [loading, setLoading] = useState(true) const [events, setEvents] = useState([]) const [showCount, setShowCount] = useState(SHOW_COUNT) const bottomRef = useRef(null) const fetchGenRef = useRef(0) useEffect(() => { const gen = ++fetchGenRef.current let cancelled = false setLoading(true) setEvents([]) setShowCount(SHOW_COUNT) void (async () => { try { const raw = await client.fetchEvents( relayUrls, { kinds: [ExtendedKind.RELAY_REVIEW], limit: REVIEW_QUERY_LIMIT }, { onevent: (e) => { if (cancelled || fetchGenRef.current !== gen) return if (e.kind === ExtendedKind.RELAY_REVIEW && getRelayUrlFromRelayReviewEvent(e)) { setEvents((prev) => dedupeRelayReviewsNewestFirst([...prev, e])) } }, firstRelayResultGraceMs: FIRST_RELAY_RESULT_GRACE_MS, globalTimeout: 12_000, eoseTimeout: EXPLORE_REVIEWS_EOSE_TAIL_MS, cache: true } ) if (cancelled || fetchGenRef.current !== gen) return const withRelay = raw.filter( (e) => e.kind === ExtendedKind.RELAY_REVIEW && getRelayUrlFromRelayReviewEvent(e) ) setEvents((prev) => dedupeRelayReviewsNewestFirst([...prev, ...withRelay])) } catch { if (!cancelled && fetchGenRef.current === gen) setEvents([]) } finally { if (!cancelled && fetchGenRef.current === gen) setLoading(false) } })() return () => { cancelled = true } }, [relayUrlsKey]) useEffect(() => { const options = { root: null, rootMargin: '120px', threshold: 0 } const observer = new IntersectionObserver((entries) => { if (entries[0]?.isIntersecting && showCount < events.length) { setShowCount((prev) => prev + SHOW_COUNT) } }, options) const el = bottomRef.current if (el) observer.observe(el) return () => { if (el) observer.unobserve(el) } }, [showCount, events.length]) const visible = events.slice(0, showCount) const showInitialSkeleton = loading && events.length === 0 const showEmptyAfterLoad = !loading && events.length === 0 return (
{showInitialSkeleton ? (
{Array.from({ length: 6 }).map((_, i) => ( ))}
) : showEmptyAfterLoad ? (

{t('no relays found')}

) : ( <>
{visible.map((event) => ( ))}
{loading ? (
{t('Loading...')}
) : null} {showCount < events.length ?
: null} {!loading && showCount >= events.length ? (

{t('no more relays')}

) : null} )}
) }