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