import { useFetchEvent } from '@/hooks' import { toNote } from '@/lib/link' import { tagNameEquals } from '@/lib/tag' import { useSecondaryPage } from '@/PageManager' import { useNostr } from '@/providers/NostrProvider' import client from '@/services/client.service' import dayjs from 'dayjs' import { Heart, MessageCircle, Repeat } from 'lucide-react' import { Event, kinds, nip19, validateEvent } from 'nostr-tools' import { useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { FormattedTimestamp } from '../FormattedTimestamp' import UserAvatar from '../UserAvatar' import PullToRefresh from 'react-simple-pull-to-refresh' const LIMIT = 50 export default function NotificationList() { const { t } = useTranslation() const { pubkey } = useNostr() const [timelineKey, setTimelineKey] = useState(undefined) const [refreshCount, setRefreshCount] = useState(0) const [initialized, setInitialized] = useState(false) const [notifications, setNotifications] = useState([]) const [until, setUntil] = useState(dayjs().unix()) const bottomRef = useRef(null) useEffect(() => { if (!pubkey) { setUntil(undefined) return } const init = async () => { const relayList = await client.fetchRelayList(pubkey) const { closer, timelineKey } = await client.subscribeTimeline( relayList.read.length >= 4 ? relayList.read : relayList.read.concat(client.getDefaultRelayUrls()).slice(0, 4), { '#p': [pubkey], kinds: [kinds.ShortTextNote, kinds.Repost, kinds.Reaction], limit: LIMIT }, { onEvents: (events, eosed) => { setNotifications(events.filter((event) => event.pubkey !== pubkey)) setUntil(events.length >= LIMIT ? events[events.length - 1].created_at - 1 : undefined) if (eosed) { setInitialized(true) } }, onNew: (event) => { if (event.pubkey === pubkey) return setNotifications((oldEvents) => [event, ...oldEvents]) } } ) setTimelineKey(timelineKey) return closer } const promise = init() return () => { promise.then((closer) => closer?.()) } }, [pubkey, refreshCount]) useEffect(() => { if (!initialized) return 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) } } }, [until, initialized, timelineKey]) const loadMore = async () => { if (!pubkey || !timelineKey || !until) return const notifications = await client.loadMoreTimeline(timelineKey, until, LIMIT) if (notifications.length === 0) { setUntil(undefined) return } if (notifications.length > 0) { setNotifications((oldNotifications) => [...oldNotifications, ...notifications]) } setUntil(notifications[notifications.length - 1].created_at - 1) } return ( new Promise((resolve) => { setRefreshCount((pre) => pre + 1) setTimeout(resolve, 1000) }) } pullingContent="" >
{notifications.map((notification) => ( ))}
{until ?
{t('loading...')}
: t('no more notifications')}
) } function NotificationItem({ notification }: { notification: Event }) { if (notification.kind === kinds.Reaction) { return } if (notification.kind === kinds.ShortTextNote) { return } if (notification.kind === kinds.Repost) { return } return null } function ReactionNotification({ notification }: { notification: Event }) { const { push } = useSecondaryPage() const bech32Id = useMemo(() => { const eTag = notification.tags.findLast(tagNameEquals('e')) const pTag = notification.tags.find(tagNameEquals('p')) const eventId = eTag?.[1] const author = pTag?.[1] return eventId ? nip19.neventEncode(author ? { id: eventId, author } : { id: eventId }) : undefined }, [notification]) const { event } = useFetchEvent(bech32Id) if (!event || !bech32Id || event.kind !== kinds.ShortTextNote) return null return (
push(toNote(bech32Id))} >
) } function ReplyNotification({ notification }: { notification: Event }) { const { push } = useSecondaryPage() return (
push(toNote(notification))} >
) } function RepostNotification({ notification }: { notification: Event }) { const { push } = useSecondaryPage() const event = useMemo(() => { try { const event = JSON.parse(notification.content) as Event const isValid = validateEvent(event) if (!isValid) return null client.addEventToCache(event) return event } catch { return null } }, [notification.content]) if (!event) return null return (
push(toNote(event))} >
) } function ContentPreview({ event }: { event?: Event }) { if (!event || event.kind !== kinds.ShortTextNote) return null return
{event.content}
}