import { Separator } from '@/components/ui/separator' import { Skeleton } from '@/components/ui/skeleton' import { BIG_RELAY_URLS, COMMENT_EVENT_KIND } from '@/constants' import { cn } from '@/lib/utils' import { useDeepBrowsing } from '@/providers/DeepBrowsingProvider' import { useNostr } from '@/providers/NostrProvider' import { useNoteStats } from '@/providers/NoteStatsProvider' import client from '@/services/client.service' import storage from '@/services/local-storage.service' import { TNotificationType } from '@/types' import dayjs from 'dayjs' import { Event, kinds } from 'nostr-tools' import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import PullToRefresh from 'react-simple-pull-to-refresh' import { NotificationItem } from './NotificationItem' const LIMIT = 100 const SHOW_COUNT = 30 const NotificationList = forwardRef((_, ref) => { const { t } = useTranslation() const { pubkey } = useNostr() const { updateNoteStatsByEvents } = useNoteStats() const [notificationType, setNotificationType] = useState('all') const [lastReadTime, setLastReadTime] = useState(0) const [refreshCount, setRefreshCount] = useState(0) const [timelineKey, setTimelineKey] = useState(undefined) const [refreshing, setRefreshing] = useState(true) const [notifications, setNotifications] = useState([]) const [newNotifications, setNewNotifications] = useState([]) const [oldNotifications, setOldNotifications] = useState([]) const [showCount, setShowCount] = useState(SHOW_COUNT) const [until, setUntil] = useState(dayjs().unix()) const bottomRef = useRef(null) const filterKinds = useMemo(() => { switch (notificationType) { case 'mentions': return [kinds.ShortTextNote, COMMENT_EVENT_KIND] case 'reactions': return [kinds.Reaction, kinds.Repost] case 'zaps': return [kinds.Zap] default: return [kinds.ShortTextNote, kinds.Repost, kinds.Reaction, kinds.Zap, COMMENT_EVENT_KIND] } }, [notificationType]) useImperativeHandle( ref, () => ({ refresh: () => { if (refreshing) return setRefreshCount((count) => count + 1) } }), [refreshing] ) useEffect(() => { if (!pubkey) { setUntil(undefined) return } const init = async () => { setRefreshing(true) setNotifications([]) setShowCount(SHOW_COUNT) setLastReadTime(storage.getLastReadNotificationTime(pubkey)) const relayList = await client.fetchRelayList(pubkey) let eventCount = 0 const { closer, timelineKey } = await client.subscribeTimeline( relayList.read.length >= 4 ? relayList.read : relayList.read.concat(BIG_RELAY_URLS).slice(0, 4), { '#p': [pubkey], kinds: filterKinds, limit: LIMIT }, { onEvents: (events, eosed) => { if (eventCount > events.length) return eventCount = events.length if (events.length > 0) { setNotifications(events.filter((event) => event.pubkey !== pubkey)) } if (eosed) { setRefreshing(false) setUntil(events.length > 0 ? events[events.length - 1].created_at - 1 : undefined) updateNoteStatsByEvents(events) } }, onNew: (event) => { if (event.pubkey === pubkey) return setNotifications((oldEvents) => { const index = oldEvents.findIndex( (oldEvent) => oldEvent.created_at < event.created_at ) if (index === -1) { return [...oldEvents, event] } return [...oldEvents.slice(0, index), event, ...oldEvents.slice(index)] }) updateNoteStatsByEvents([event]) } } ) setTimelineKey(timelineKey) return closer } const promise = init() return () => { promise.then((closer) => closer?.()) } }, [pubkey, refreshCount, filterKinds]) useEffect(() => { const visibleNotifications = notifications.slice(0, showCount) const index = visibleNotifications.findIndex((event) => event.created_at <= lastReadTime) if (index === -1) { setNewNotifications(visibleNotifications) setOldNotifications([]) } else { setNewNotifications(visibleNotifications.slice(0, index)) setOldNotifications(visibleNotifications.slice(index)) } }, [notifications, lastReadTime, showCount]) const loadMore = useCallback(async () => { if (showCount < notifications.length) { setShowCount((count) => count + SHOW_COUNT) return } if (!pubkey || !timelineKey || !until || refreshing) return const newNotifications = await client.loadMoreTimeline(timelineKey, until, LIMIT) if (newNotifications.length === 0) { setUntil(undefined) return } if (newNotifications.length > 0) { setNotifications((oldNotifications) => [ ...oldNotifications, ...newNotifications.filter((event) => event.pubkey !== pubkey) ]) } setUntil(newNotifications[newNotifications.length - 1].created_at - 1) }, [pubkey, timelineKey, until, refreshing, showCount, notifications]) useEffect(() => { const options = { root: null, rootMargin: '10px', threshold: 1 } const observerInstance = new IntersectionObserver((entries) => { if (entries[0].isIntersecting) { loadMore() } }, options) const currentBottomRef = bottomRef.current if (currentBottomRef) { observerInstance.observe(currentBottomRef) } return () => { if (observerInstance && currentBottomRef) { observerInstance.unobserve(currentBottomRef) } } }, [loadMore]) return (
{ setShowCount(SHOW_COUNT) setNotificationType(type) }} /> { setRefreshCount((count) => count + 1) await new Promise((resolve) => setTimeout(resolve, 1000)) }} pullingContent="" >
{newNotifications.map((notification) => ( ))} {!!newNotifications.length && (
{t('Earlier notifications')}
)} {oldNotifications.map((notification) => ( ))}
{until || refreshing ? (
) : ( t('no more notifications') )}
) }) NotificationList.displayName = 'NotificationList' export default NotificationList function NotificationTypeSwitch({ type, setType }: { type: TNotificationType setType: (type: TNotificationType) => void }) { const { t } = useTranslation() const { deepBrowsing, lastScrollTop } = useDeepBrowsing() return (
800 ? '-translate-y-[calc(100%+12rem)]' : '' )} >
setType('all')} > {t('All')}
setType('mentions')} > {t('Mentions')}
setType('reactions')} > {t('Reactions')}
setType('zaps')} > {t('Zaps')}
) }