index.tsx raw
1 import { useFetchEvent } from '@/hooks'
2 import { generateBech32IdFromATag, generateBech32IdFromETag } from '@/lib/tag'
3 import { useNostr } from '@/providers/NostrProvider'
4 import { useEffect, useMemo, useRef, useState } from 'react'
5 import { useTranslation } from 'react-i18next'
6 import NoteCard, { NoteCardLoadingSkeleton } from '../NoteCard'
7
8 const SHOW_COUNT = 10
9
10 export default function BookmarkList() {
11 const { t } = useTranslation()
12 const { bookmarkListEvent } = useNostr()
13 const eventIds = useMemo(() => {
14 if (!bookmarkListEvent) return []
15
16 return (
17 bookmarkListEvent.tags
18 .map((tag) =>
19 tag[0] === 'e'
20 ? generateBech32IdFromETag(tag)
21 : tag[0] === 'a'
22 ? generateBech32IdFromATag(tag)
23 : null
24 )
25 .filter(Boolean) as (`nevent1${string}` | `naddr1${string}`)[]
26 ).reverse()
27 }, [bookmarkListEvent])
28 const [showCount, setShowCount] = useState(SHOW_COUNT)
29 const bottomRef = useRef<HTMLDivElement | null>(null)
30
31 useEffect(() => {
32 const options = {
33 root: null,
34 rootMargin: '10px',
35 threshold: 0.1
36 }
37
38 const loadMore = () => {
39 if (showCount < eventIds.length) {
40 setShowCount((prev) => prev + SHOW_COUNT)
41 }
42 }
43
44 const observerInstance = new IntersectionObserver((entries) => {
45 if (entries[0].isIntersecting) {
46 loadMore()
47 }
48 }, options)
49
50 const currentBottomRef = bottomRef.current
51
52 if (currentBottomRef) {
53 observerInstance.observe(currentBottomRef)
54 }
55
56 return () => {
57 if (observerInstance && currentBottomRef) {
58 observerInstance.unobserve(currentBottomRef)
59 }
60 }
61 }, [showCount, eventIds])
62
63 if (eventIds.length === 0) {
64 return (
65 <div className="mt-2 text-sm text-center text-muted-foreground">
66 {t('no bookmarks found')}
67 </div>
68 )
69 }
70
71 return (
72 <div>
73 {eventIds.slice(0, showCount).map((eventId) => (
74 <BookmarkedNote key={eventId} eventId={eventId} />
75 ))}
76
77 {showCount < eventIds.length ? (
78 <div ref={bottomRef}>
79 <NoteCardLoadingSkeleton />
80 </div>
81 ) : (
82 <div className="text-center text-sm text-muted-foreground mt-2">
83 {t('no more bookmarks')}
84 </div>
85 )}
86 </div>
87 )
88 }
89
90 function BookmarkedNote({ eventId }: { eventId: string }) {
91 const { event, isFetching } = useFetchEvent(eventId)
92
93 if (isFetching) {
94 return <NoteCardLoadingSkeleton className="border-b" />
95 }
96
97 if (!event) {
98 return null
99 }
100
101 return <NoteCard event={event} className="w-full" />
102 }
103