import NormalFeed from '@/components/NormalFeed' import type { TNoteListRef } from '@/components/NoteList' import RelayInfo from '@/components/RelayInfo' import SearchInput from '@/components/SearchInput' import { useFetchRelayInfo, useRelayPageFeedPolicy } from '@/hooks' import type { TPrimaryPageName } from '@/PageManager' import { SINGLE_RELAY_KINDLESS_REQ_LIMIT } from '@/constants' import { canonicalRelaySessionKey, isLocalNetworkUrl, normalizeRelayUrlForPage } from '@/lib/url' import { useCurrentRelays } from '@/providers/CurrentRelaysProvider' import { useKindFilterOrDefaults } from '@/providers/KindFilterProvider' import client from '@/services/client.service' import type { TFeedSubRequest } from '@/types' import { kinds, type Event } from 'nostr-tools' import { forwardRef, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { AlexandriaEventsSearchEmptyCta } from '@/components/AlexandriaEventsSearchEmptyCta' import { buildAlexandriaEventsSearchUrlFromNotesQuery } from '@/lib/alexandria-events-search-url' import { kindsForSingleRelayBrowse } from '@/lib/single-relay-browse-kinds' import { stableFeedKindKey } from '@/features/feed/descriptor' import NotFound from '../NotFound' const Relay = forwardRef< TNoteListRef, { url?: string className?: string hostPrimaryPageName?: TPrimaryPageName alexandriaEmptyUrl?: string | null alexandriaNotFoundHref?: string | null } >(function Relay( { url, className, hostPrimaryPageName, alexandriaEmptyUrl = null, alexandriaNotFoundHref = null }, ref ) { const { t } = useTranslation() useRelayPageFeedPolicy() const { addRelayUrls, removeRelayUrls } = useCurrentRelays() const { showKinds } = useKindFilterOrDefaults() const normalizedUrl = useMemo(() => (url ? normalizeRelayUrlForPage(url) : undefined), [url]) const { relayInfo } = useFetchRelayInfo(normalizedUrl) const [searchInput, setSearchInput] = useState('') const [debouncedInput, setDebouncedInput] = useState(searchInput) /** After explicit-kinds REQ EOSEs empty, retry kindless `{ limit }` once (document/specialty relays). */ const [kindlessBrowseFallback, setKindlessBrowseFallback] = useState(false) const internalNoteListRef = useRef(null) const noteListRef = ref ?? internalNoteListRef useEffect(() => { if (normalizedUrl) { addRelayUrls([normalizedUrl]) return () => { removeRelayUrls([normalizedUrl]) } } }, [normalizedUrl]) useEffect(() => { const handler = setTimeout(() => { setDebouncedInput(searchInput) }, 1000) return () => { clearTimeout(handler) } }, [searchInput]) // Listen for refresh events when user publishes to this relay useEffect(() => { if (!normalizedUrl) return const handleRelayRefresh = (event: CustomEvent) => { const { relayUrl } = event.detail if (canonicalRelaySessionKey(relayUrl) === canonicalRelaySessionKey(normalizedUrl)) { if (noteListRef && typeof noteListRef !== 'function') { noteListRef.current?.refresh() } } } window.addEventListener('relay-refresh-needed', handleRelayRefresh as EventListener) return () => { window.removeEventListener('relay-refresh-needed', handleRelayRefresh as EventListener) } }, [normalizedUrl, noteListRef]) useEffect(() => { setKindlessBrowseFallback(false) }, [normalizedUrl]) /** Default browse: explicit kinds (many strfry / small relays never return a useful kindless global REQ). */ const relayBrowseKindsKey = useMemo(() => stableFeedKindKey(showKinds), [showKinds]) const relayBrowseKinds = useMemo( () => (normalizedUrl ? kindsForSingleRelayBrowse(normalizedUrl, showKinds) : [kinds.ShortTextNote]), [relayBrowseKindsKey, showKinds, normalizedUrl] ) const onSingleRelayBrowseEmpty = useCallback(() => { setKindlessBrowseFallback(true) }, []) const relayFeedSubRequests = useMemo(() => { if (!normalizedUrl) return [] const q = debouncedInput.trim() if (q) { return [ { urls: [normalizedUrl], filter: { search: q, limit: SINGLE_RELAY_KINDLESS_REQ_LIMIT } } ] } if (kindlessBrowseFallback) { return [ { urls: [normalizedUrl], filter: { limit: SINGLE_RELAY_KINDLESS_REQ_LIMIT } } ] } return [ { urls: [normalizedUrl], filter: { kinds: [...relayBrowseKinds], limit: SINGLE_RELAY_KINDLESS_REQ_LIMIT } } ] }, [normalizedUrl, debouncedInput, relayBrowseKindsKey, kindlessBrowseFallback]) const allowKindlessRelayExplore = debouncedInput.trim().length > 0 /** When we know delivery relays, drop rows that never arrived from this feed’s relay (stale cache / mis-tagged). */ const relaySeenMatchKey = useMemo( () => (normalizedUrl ? canonicalRelaySessionKey(normalizedUrl) : ''), [normalizedUrl] ) const shouldHideEventNotFromThisRelay = useCallback( (ev: Event) => { if (allowKindlessRelayExplore) { return false } if (!relaySeenMatchKey) return false // LAN/loopback: REQ already targets this relay; "seen on" often lists another URL first // (favorites merge, localhost vs 127.0.0.1, etc.) — hiding would empty the relay-only feed. if (normalizedUrl && isLocalNetworkUrl(normalizedUrl)) return false const seen = client.getSeenEventRelayUrls(ev.id) if (seen.length === 0) return false return !seen.some((u) => canonicalRelaySessionKey(u) === relaySeenMatchKey) }, [relaySeenMatchKey, normalizedUrl, allowKindlessRelayExplore] ) const alexandriaFeedEmptyUrl = useMemo(() => { const q = debouncedInput.trim() if (q) return buildAlexandriaEventsSearchUrlFromNotesQuery(q) return alexandriaEmptyUrl }, [debouncedInput, alexandriaEmptyUrl]) if (!normalizedUrl) { return ( {alexandriaNotFoundHref ? : null} ) } return (
{relayInfo?.supported_nips?.includes(50) && (
setSearchInput(e.target.value)} placeholder={t('Search')} />
)}
) }) Relay.displayName = 'Relay' export default Relay