UserProfileModal.tsx raw
1 import { useChat } from '@/providers/ChatProvider'
2 import { useNostr } from '@/providers/NostrProvider'
3 import { useFetchProfile } from '@/hooks/useFetchProfile'
4 import { useSecondaryPage } from '@/PageManager'
5 import { Pubkey } from '@/domain'
6 import {
7 X,
8 ExternalLink,
9 MessageSquare,
10 ShieldPlus,
11 UserMinus,
12 Ban,
13 Copy,
14 Check
15 } from 'lucide-react'
16 import { useState } from 'react'
17 import { Button } from '../ui/button'
18
19 export default function UserProfileModal({
20 pubkeyHex,
21 onClose
22 }: {
23 pubkeyHex: string
24 onClose: () => void
25 }) {
26 const { profile } = useFetchProfile(pubkeyHex)
27 const { pubkey } = useNostr()
28 const { currentChannel, isOwnerOrMod, addMod, removeMember, blockUser, channelMods } = useChat()
29 const { push } = useSecondaryPage()
30 const [copied, setCopied] = useState(false)
31
32 const pk = Pubkey.tryFromString(pubkeyHex)
33 const npub = pk?.npub || ''
34 const displayName = profile?.username || pk?.formatNpub(8) || pubkeyHex.slice(0, 12)
35 const isOwner = currentChannel?.creator === pubkeyHex
36 const isMod = channelMods.includes(pubkeyHex)
37 const isSelf = pubkeyHex === pubkey
38
39 const handleCopy = () => {
40 navigator.clipboard.writeText(npub)
41 setCopied(true)
42 setTimeout(() => setCopied(false), 1500)
43 }
44
45 return (
46 <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50" onClick={onClose}>
47 <div
48 className="bg-background border rounded-lg w-80 overflow-hidden"
49 onClick={(e) => e.stopPropagation()}
50 >
51 {/* Banner */}
52 {profile?.banner ? (
53 <div className="h-20 bg-muted">
54 <img src={profile.banner} alt="" className="w-full h-full object-cover" />
55 </div>
56 ) : (
57 <div className="h-20 bg-gradient-to-r from-primary/20 to-primary/5" />
58 )}
59
60 {/* Avatar + name */}
61 <div className="px-4 -mt-6">
62 <div className="size-12 rounded-full border-2 border-background overflow-hidden bg-muted">
63 {profile?.avatar ? (
64 <img src={profile.avatar} alt="" className="w-full h-full object-cover" />
65 ) : (
66 <div className="w-full h-full bg-primary/20" />
67 )}
68 </div>
69 </div>
70
71 <div className="px-4 pt-1 pb-3 space-y-2">
72 <div>
73 <div className="font-semibold text-sm flex items-center gap-1">
74 {displayName}
75 {isOwner && <span className="text-[10px] text-muted-foreground">(owner)</span>}
76 {isMod && !isOwner && <span className="text-[10px] text-muted-foreground">(mod)</span>}
77 </div>
78 <button
79 onClick={handleCopy}
80 className="text-[10px] text-muted-foreground font-mono flex items-center gap-0.5 hover:text-foreground"
81 >
82 {npub.slice(0, 20)}...
83 {copied ? <Check className="size-2.5" /> : <Copy className="size-2.5" />}
84 </button>
85 </div>
86
87 {profile?.about && (
88 <p className="text-xs text-muted-foreground line-clamp-2">{profile.about}</p>
89 )}
90
91 {/* Actions */}
92 <div className="flex gap-1.5 pt-1">
93 <Button
94 variant="outline"
95 size="sm"
96 className="h-7 text-xs gap-1 flex-1"
97 onClick={() => {
98 onClose()
99 push(`/users/${pubkeyHex}`)
100 }}
101 >
102 <ExternalLink className="size-3" /> Profile
103 </Button>
104 {!isSelf && (
105 <Button
106 variant="outline"
107 size="sm"
108 className="h-7 text-xs gap-1 flex-1"
109 onClick={() => {
110 onClose()
111 push(`/dm/${pubkeyHex}`)
112 }}
113 >
114 <MessageSquare className="size-3" /> DM
115 </Button>
116 )}
117 </div>
118
119 {/* Mod actions */}
120 {isOwnerOrMod && !isSelf && !isOwner && (
121 <div className="flex gap-1.5 pt-0.5">
122 {!isMod && (
123 <Button
124 variant="outline"
125 size="sm"
126 className="h-7 text-xs gap-1 flex-1"
127 onClick={() => { addMod(pubkeyHex); onClose() }}
128 >
129 <ShieldPlus className="size-3" /> Make Mod
130 </Button>
131 )}
132 <Button
133 variant="outline"
134 size="sm"
135 className="h-7 text-xs gap-1 flex-1"
136 onClick={() => { removeMember(pubkeyHex); onClose() }}
137 >
138 <UserMinus className="size-3" /> Kick
139 </Button>
140 <Button
141 variant="outline"
142 size="sm"
143 className="h-7 text-xs gap-1 flex-1 text-destructive border-destructive/30"
144 onClick={() => { blockUser(pubkeyHex); onClose() }}
145 >
146 <Ban className="size-3" /> Block
147 </Button>
148 </div>
149 )}
150 </div>
151
152 <Button
153 variant="ghost"
154 size="icon"
155 className="absolute top-2 right-2 size-6 bg-background/80"
156 onClick={onClose}
157 >
158 <X className="size-3.5" />
159 </Button>
160 </div>
161 </div>
162 )
163 }
164