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