import { Button } from '@/components/ui/button' import { PICTURE_EVENT_KIND } from '@/constants' import { isReplyNoteEvent } from '@/lib/event' import { checkAlgoRelay } from '@/lib/relay' import { cn } from '@/lib/utils' import { useMuteList } from '@/providers/MuteListProvider' import { useNostr } from '@/providers/NostrProvider' import { useScreenSize } from '@/providers/ScreenSizeProvider' import client from '@/services/client.service' import storage from '@/services/storage.service' import { TNoteListMode } from '@/types' import dayjs from 'dayjs' import { Event, Filter, kinds } from 'nostr-tools' import { ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import PullToRefresh from 'react-simple-pull-to-refresh' import NoteCard from '../NoteCard' import PictureNoteCard from '../PictureNoteCard' const NORMAL_RELAY_LIMIT = 100 const ALGO_RELAY_LIMIT = 500 const PICTURE_NOTE_LIMIT = 30 export default function NoteList({ relayUrls, filter = {}, className, filterMutedNotes = true }: { relayUrls: string[] filter?: Filter className?: string filterMutedNotes?: boolean }) { const { t } = useTranslation() const { isLargeScreen } = useScreenSize() const { signEvent, checkLogin } = useNostr() const { mutePubkeys } = useMuteList() const [refreshCount, setRefreshCount] = useState(0) const [timelineKey, setTimelineKey] = useState(undefined) const [events, setEvents] = useState([]) const [newEvents, setNewEvents] = useState([]) const [hasMore, setHasMore] = useState(true) const [refreshing, setRefreshing] = useState(true) const [listMode, setListMode] = useState(() => storage.getNoteListMode()) const bottomRef = useRef(null) const isPictures = useMemo(() => listMode === 'pictures', [listMode]) const noteFilter = useMemo(() => { if (isPictures) { return { kinds: [PICTURE_EVENT_KIND], limit: PICTURE_NOTE_LIMIT, ...filter } } return { kinds: [kinds.ShortTextNote, kinds.Repost, PICTURE_EVENT_KIND], limit: NORMAL_RELAY_LIMIT, ...filter } }, [JSON.stringify(filter), isPictures]) useEffect(() => { if (relayUrls.length === 0) return async function init() { setRefreshing(true) setEvents([]) setNewEvents([]) setHasMore(true) const relayInfos = await client.fetchRelayInfos(relayUrls) const areAlgoRelays = relayInfos.every((relayInfo) => checkAlgoRelay(relayInfo)) const filter = areAlgoRelays ? { ...noteFilter, limit: ALGO_RELAY_LIMIT } : noteFilter let eventCount = 0 const { closer, timelineKey } = await client.subscribeTimeline( [...relayUrls], filter, { onEvents: (events, eosed) => { if (eventCount > events.length) return eventCount = events.length if (events.length > 0) { setEvents(events) } if (areAlgoRelays) { setHasMore(false) } if (eosed) { setRefreshing(false) setHasMore(events.length > 0) } }, onNew: (event) => { setNewEvents((oldEvents) => [event, ...oldEvents].sort((a, b) => b.created_at - a.created_at) ) } }, { signer: async (evt) => { const signedEvt = await checkLogin(() => signEvent(evt)) return signedEvt ?? null }, needSort: !areAlgoRelays } ) setTimelineKey(timelineKey) return closer } const promise = init() return () => { promise.then((closer) => closer()) } }, [JSON.stringify(relayUrls), noteFilter, refreshCount]) const loadMore = useCallback(async () => { if (!timelineKey || refreshing || !hasMore) return const newEvents = await client.loadMoreTimeline( timelineKey, events.length ? events[events.length - 1].created_at - 1 : dayjs().unix(), noteFilter.limit ) if (newEvents.length === 0) { setHasMore(false) return } setEvents((oldEvents) => [...oldEvents, ...newEvents]) }, [timelineKey, refreshing, hasMore, events, noteFilter]) useEffect(() => { if (refreshing) return const options = { root: null, rootMargin: '10px', threshold: 0.1 } 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) } } }, [refreshing, loadMore]) const showNewEvents = () => { setEvents((oldEvents) => [...newEvents, ...oldEvents]) setNewEvents([]) } const eventFilter = (event: Event) => { return ( (!filterMutedNotes || !mutePubkeys.includes(event.pubkey)) && (listMode !== 'posts' || !isReplyNoteEvent(event)) ) } return (
{ setListMode(listMode) storage.setNoteListMode(listMode) }} /> { setRefreshCount((count) => count + 1) await new Promise((resolve) => setTimeout(resolve, 1000)) }} pullingContent="" >
{newEvents.filter(eventFilter).length > 0 && (
)} {isPictures ? ( ) : (
{events.filter(eventFilter).map((event) => ( ))}
)}
{hasMore || refreshing ? (
{t('loading...')}
) : events.length ? ( t('no more notes') ) : (
)}
) } function ListModeSwitch({ listMode, setListMode }: { listMode: TNoteListMode setListMode: (listMode: TNoteListMode) => void }) { const { t } = useTranslation() return (
setListMode('posts')} > {t('Notes')}
setListMode('postsAndReplies')} > {t('Notes & Replies')}
setListMode('pictures')} > {t('Pictures')}
) } function PictureNoteCardMasonry({ events, columnCount, className }: { events: Event[] columnCount: 2 | 3 className?: string }) { const columns = useMemo(() => { const newColumns: ReactNode[][] = Array.from({ length: columnCount }, () => []) events.forEach((event, i) => { newColumns[i % columnCount].push( ) }) return newColumns }, [events, columnCount]) return (
{columns.map((column, i) => (
{column}
))}
) }