index.tsx raw

   1  import MuteButton from '@/components/MuteButton'
   2  import Nip05 from '@/components/Nip05'
   3  import { Button } from '@/components/ui/button'
   4  import UserAvatar from '@/components/UserAvatar'
   5  import Username from '@/components/Username'
   6  import { useFetchProfile } from '@/hooks'
   7  import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
   8  import { useMuteList } from '@/providers/MuteListProvider'
   9  import { useNostr } from '@/providers/NostrProvider'
  10  import { Loader, Lock, Unlock } from 'lucide-react'
  11  import { forwardRef, useEffect, useMemo, useRef, useState } from 'react'
  12  import { useTranslation } from 'react-i18next'
  13  import NotFoundPage from '../NotFoundPage'
  14  
  15  const MuteListPage = forwardRef(({ index }: { index?: number }, ref) => {
  16    const { t } = useTranslation()
  17    const { profile, pubkey } = useNostr()
  18    const { getMutePubkeys } = useMuteList()
  19    const mutePubkeys = useMemo(() => getMutePubkeys(), [pubkey])
  20    const [visibleMutePubkeys, setVisibleMutePubkeys] = useState<string[]>([])
  21    const bottomRef = useRef<HTMLDivElement>(null)
  22  
  23    useEffect(() => {
  24      setVisibleMutePubkeys(mutePubkeys.slice(0, 10))
  25    }, [mutePubkeys])
  26  
  27    useEffect(() => {
  28      const options = {
  29        root: null,
  30        rootMargin: '10px',
  31        threshold: 1
  32      }
  33  
  34      const observerInstance = new IntersectionObserver((entries) => {
  35        if (entries[0].isIntersecting && mutePubkeys.length > visibleMutePubkeys.length) {
  36          setVisibleMutePubkeys((prev) => [
  37            ...prev,
  38            ...mutePubkeys.slice(prev.length, prev.length + 10)
  39          ])
  40        }
  41      }, options)
  42  
  43      const currentBottomRef = bottomRef.current
  44      if (currentBottomRef) {
  45        observerInstance.observe(currentBottomRef)
  46      }
  47  
  48      return () => {
  49        if (observerInstance && currentBottomRef) {
  50          observerInstance.unobserve(currentBottomRef)
  51        }
  52      }
  53    }, [visibleMutePubkeys, mutePubkeys])
  54  
  55    if (!profile) {
  56      return <NotFoundPage />
  57    }
  58  
  59    return (
  60      <SecondaryPageLayout
  61        ref={ref}
  62        index={index}
  63        title={t("username's muted", { username: profile.username })}
  64        displayScrollToTopButton
  65      >
  66        <div className="space-y-2 px-4 pt-2">
  67          {visibleMutePubkeys.map((pubkey, index) => (
  68            <UserItem key={`${index}-${pubkey}`} pubkey={pubkey} />
  69          ))}
  70          {mutePubkeys.length > visibleMutePubkeys.length && <div ref={bottomRef} />}
  71        </div>
  72      </SecondaryPageLayout>
  73    )
  74  })
  75  MuteListPage.displayName = 'MuteListPage'
  76  export default MuteListPage
  77  
  78  function UserItem({ pubkey }: { pubkey: string }) {
  79    const { changing, getMuteType, switchToPrivateMute, switchToPublicMute } = useMuteList()
  80    const { profile } = useFetchProfile(pubkey)
  81    const muteType = useMemo(() => getMuteType(pubkey), [pubkey, getMuteType])
  82    const [switching, setSwitching] = useState(false)
  83  
  84    return (
  85      <div className="flex gap-2 items-start">
  86        <UserAvatar userId={pubkey} className="shrink-0" />
  87        <div className="w-full overflow-hidden">
  88          <Username
  89            userId={pubkey}
  90            className="font-semibold truncate max-w-full w-fit"
  91            skeletonClassName="h-4"
  92          />
  93          <Nip05 pubkey={pubkey} />
  94          <div className="truncate text-muted-foreground text-sm">{profile?.about}</div>
  95        </div>
  96        <div className="flex gap-2 items-center">
  97          {switching ? (
  98            <Button disabled variant="ghost" size="icon">
  99              <Loader className="animate-spin" />
 100            </Button>
 101          ) : muteType === 'private' ? (
 102            <Button
 103              variant="ghost"
 104              size="icon"
 105              onClick={() => {
 106                if (switching) return
 107  
 108                setSwitching(true)
 109                switchToPublicMute(pubkey).finally(() => setSwitching(false))
 110              }}
 111              disabled={changing}
 112            >
 113              <Lock className="text-green-400" />
 114            </Button>
 115          ) : muteType === 'public' ? (
 116            <Button
 117              variant="ghost"
 118              size="icon"
 119              onClick={() => {
 120                if (switching) return
 121  
 122                setSwitching(true)
 123                switchToPrivateMute(pubkey).finally(() => setSwitching(false))
 124              }}
 125              disabled={changing}
 126            >
 127              <Unlock className="text-muted-foreground" />
 128            </Button>
 129          ) : null}
 130          <MuteButton pubkey={pubkey} />
 131        </div>
 132      </div>
 133    )
 134  }
 135