import { FAST_READ_RELAY_URLS, ExtendedKind } from '@/constants' import { getReplaceableCoordinateFromEvent, isReplaceableEvent } from '@/lib/event' import { useNostr } from '@/providers/NostrProvider' import { useUserTrust } from '@/providers/UserTrustProvider' import client from '@/services/client.service' import dayjs from 'dayjs' import { Event, kinds } from 'nostr-tools' import { useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import NoteCard, { NoteCardLoadingSkeleton } from '../NoteCard' const LIMIT = 100 const SHOW_COUNT = 10 export default function QuoteList({ event, className }: { event: Event; className?: string }) { const { t } = useTranslation() const { startLogin, relayList: userRelayList } = useNostr() const { hideUntrustedInteractions, isUserTrusted } = useUserTrust() const [timelineKey, setTimelineKey] = useState(undefined) const [events, setEvents] = useState([]) const [showCount, setShowCount] = useState(SHOW_COUNT) const [hasMore, setHasMore] = useState(true) const [loading, setLoading] = useState(true) const bottomRef = useRef(null) useEffect(() => { async function init() { setLoading(true) setEvents([]) setHasMore(true) // Privacy: Only use user's own relays + defaults, never connect to other users' relays const userRelays = userRelayList?.read || [] const finalRelayUrls = Array.from(new Set(userRelays.concat(FAST_READ_RELAY_URLS))) const { closer, timelineKey } = await client.subscribeTimeline( [ { urls: finalRelayUrls, filter: { '#q': [ isReplaceableEvent(event.kind) ? getReplaceableCoordinateFromEvent(event) : event.id ], kinds: [ kinds.ShortTextNote, kinds.Highlights, kinds.LongFormArticle, ExtendedKind.COMMENT, ExtendedKind.POLL, ExtendedKind.PUBLIC_MESSAGE ], limit: LIMIT } } ], { onEvents: (events, eosed) => { if (events.length > 0) { setEvents(events) } if (eosed) { setLoading(false) setHasMore(events.length > 0) } }, onNew: (event) => { setEvents((oldEvents) => [event, ...oldEvents].sort((a, b) => b.created_at - a.created_at) ) } }, { startLogin } ) setTimelineKey(timelineKey) return closer } const promise = init() return () => { promise.then((closer) => closer()) } }, [event]) useEffect(() => { const options = { root: null, rootMargin: '10px', threshold: 0.1 } const loadMore = async () => { if (showCount < events.length) { setShowCount((prev) => prev + SHOW_COUNT) // preload more if (events.length - showCount > LIMIT / 2) { return } } if (!timelineKey || loading || !hasMore) return setLoading(true) const newEvents = await client.loadMoreTimeline( timelineKey, events.length ? events[events.length - 1].created_at - 1 : dayjs().unix(), LIMIT ) setLoading(false) if (newEvents.length === 0) { setHasMore(false) return } setEvents((oldEvents) => [...oldEvents, ...newEvents]) } const observerInstance = new IntersectionObserver((entries) => { if (entries[0].isIntersecting && hasMore) { loadMore() } }, options) const currentBottomRef = bottomRef.current if (currentBottomRef) { observerInstance.observe(currentBottomRef) } return () => { if (observerInstance && currentBottomRef) { observerInstance.unobserve(currentBottomRef) } } }, [timelineKey, loading, hasMore, events, showCount]) return (
{events.slice(0, showCount).map((event) => { if (hideUntrustedInteractions && !isUserTrusted(event.pubkey)) { return null } return })}
{hasMore || loading ? (
) : (
{t('no more notes')}
)}
) }