index.tsx raw

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