MemberListPanel.tsx raw
1 import { useChat } from '@/providers/ChatProvider'
2 import { useFetchProfile } from '@/hooks/useFetchProfile'
3 import { useSecondaryPage } from '@/PageManager'
4 import { Pubkey } from '@/domain'
5 import { X, MessageSquare, Shield, Crown } from 'lucide-react'
6 import { Button } from '../ui/button'
7
8 export default function MemberListPanel({ onClose }: { onClose: () => void }) {
9 const { currentChannel, channelParticipants, channelMods } = useChat()
10
11 if (!currentChannel) return null
12
13 // Sort: owner first, then mods, then everyone else
14 const sorted = [...channelParticipants].sort((a, b) => {
15 const aOwner = a === currentChannel.creator ? 0 : 1
16 const bOwner = b === currentChannel.creator ? 0 : 1
17 if (aOwner !== bOwner) return aOwner - bOwner
18 const aMod = channelMods.includes(a) ? 0 : 1
19 const bMod = channelMods.includes(b) ? 0 : 1
20 return aMod - bMod
21 })
22
23 return (
24 <div className="absolute inset-y-0 right-0 z-20 w-56 bg-background border-l overflow-y-auto">
25 <div className="flex items-center justify-between p-2 border-b">
26 <span className="text-xs font-semibold">Members ({sorted.length})</span>
27 <Button variant="ghost" size="icon" className="size-6" onClick={onClose}>
28 <X className="size-3.5" />
29 </Button>
30 </div>
31 <div className="py-1">
32 {sorted.map((pk) => (
33 <MemberItem
34 key={pk}
35 pubkey={pk}
36 isOwner={pk === currentChannel.creator}
37 isMod={channelMods.includes(pk)}
38 />
39 ))}
40 </div>
41 </div>
42 )
43 }
44
45 function MemberItem({
46 pubkey,
47 isOwner,
48 isMod
49 }: {
50 pubkey: string
51 isOwner: boolean
52 isMod: boolean
53 }) {
54 const { profile } = useFetchProfile(pubkey)
55 const { push } = useSecondaryPage()
56 const pk = Pubkey.tryFromString(pubkey)
57 const displayName = profile?.username || pk?.formatNpub(8) || pubkey.slice(0, 12)
58
59 return (
60 <div className="group flex items-center gap-2 px-2 py-1 hover:bg-muted/50 text-xs">
61 <div className="size-5 rounded-full bg-muted overflow-hidden shrink-0">
62 {profile?.avatar && <img src={profile.avatar} alt="" className="w-full h-full object-cover" />}
63 </div>
64 <span className="font-medium truncate flex-1">{displayName}</span>
65 {isOwner && <span title="Owner"><Crown className="size-3 text-primary shrink-0" /></span>}
66 {isMod && !isOwner && <span title="Mod"><Shield className="size-3 text-muted-foreground shrink-0" /></span>}
67 <button
68 className="opacity-0 group-hover:opacity-100 text-muted-foreground hover:text-foreground shrink-0"
69 onClick={() => push(`/dm/${pubkey}`)}
70 title="Send DM"
71 >
72 <MessageSquare className="size-3" />
73 </button>
74 </div>
75 )
76 }
77