index.tsx raw
1 import { useSecondaryPage } from '@/PageManager'
2 import { useStuffStatsById } from '@/hooks/useStuffStatsById'
3 import { toProfile } from '@/lib/link'
4 import { useScreenSize } from '@/providers/ScreenSizeProvider'
5 import { useUserTrust } from '@/providers/UserTrustProvider'
6 import { Event } from 'nostr-tools'
7 import { useEffect, useMemo, useRef, useState } from 'react'
8 import { useTranslation } from 'react-i18next'
9 import Emoji from '../Emoji'
10 import { FormattedTimestamp } from '../FormattedTimestamp'
11 import Nip05 from '../Nip05'
12 import UserAvatar from '../UserAvatar'
13 import Username from '../Username'
14 import { useStuff } from '@/hooks/useStuff'
15
16 const SHOW_COUNT = 20
17
18 export default function ReactionList({ stuff }: { stuff: Event | string }) {
19 const { t } = useTranslation()
20 const { push } = useSecondaryPage()
21 const { isSmallScreen } = useScreenSize()
22 const { hideUntrustedInteractions, isUserTrusted } = useUserTrust()
23 const { stuffKey } = useStuff(stuff)
24 const noteStats = useStuffStatsById(stuffKey)
25 const filteredLikes = useMemo(() => {
26 return (noteStats?.likes ?? [])
27 .filter((like) => !hideUntrustedInteractions || isUserTrusted(like.pubkey))
28 .sort((a, b) => b.created_at - a.created_at)
29 }, [noteStats, stuffKey, hideUntrustedInteractions, isUserTrusted])
30
31 const [showCount, setShowCount] = useState(SHOW_COUNT)
32 const bottomRef = useRef<HTMLDivElement | null>(null)
33
34 useEffect(() => {
35 if (!bottomRef.current || filteredLikes.length <= showCount) return
36 const obs = new IntersectionObserver(
37 ([entry]) => {
38 if (entry.isIntersecting) setShowCount((c) => c + SHOW_COUNT)
39 },
40 { rootMargin: '10px', threshold: 0.1 }
41 )
42 obs.observe(bottomRef.current)
43 return () => obs.disconnect()
44 }, [filteredLikes.length, showCount])
45
46 return (
47 <div className="min-h-[80vh]">
48 {filteredLikes.slice(0, showCount).map((like) => (
49 <div
50 key={like.id}
51 className="px-4 py-3 border-b transition-colors clickable flex items-center gap-3"
52 onClick={() => push(toProfile(like.pubkey))}
53 >
54 <div className="w-6 flex flex-col items-center">
55 <Emoji
56 emoji={like.emoji}
57 classNames={{
58 text: 'text-xl'
59 }}
60 />
61 </div>
62
63 <UserAvatar userId={like.pubkey} size="medium" className="shrink-0" />
64
65 <div className="flex-1 w-0">
66 <Username
67 userId={like.pubkey}
68 className="text-sm font-semibold text-muted-foreground hover:text-foreground max-w-fit truncate"
69 skeletonClassName="h-3"
70 />
71 <div className="flex items-center gap-1 text-sm text-muted-foreground">
72 <Nip05 pubkey={like.pubkey} append="ยท" />
73 <FormattedTimestamp
74 timestamp={like.created_at}
75 className="shrink-0"
76 short={isSmallScreen}
77 />
78 </div>
79 </div>
80 </div>
81 ))}
82
83 <div ref={bottomRef} />
84
85 <div className="text-sm mt-2 text-center text-muted-foreground">
86 {filteredLikes.length > 0 ? t('No more reactions') : t('No reactions yet')}
87 </div>
88 </div>
89 )
90 }
91