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