MentionPopup.tsx raw

   1  import { useFetchProfile } from '@/hooks/useFetchProfile'
   2  import { Pubkey } from '@/domain'
   3  import { useMemo } from 'react'
   4  
   5  export default function MentionPopup({
   6    participants,
   7    query,
   8    onSelect,
   9    position
  10  }: {
  11    participants: string[]
  12    query: string // text after @ (lowercase)
  13    onSelect: (pubkey: string, displayName: string) => void
  14    position: { bottom: number; left: number }
  15  }) {
  16    const filtered = useMemo(() => {
  17      if (!query) return participants.slice(0, 8)
  18      return participants.filter((pk) => {
  19        const npub = Pubkey.tryFromString(pk)?.formatNpub(20) || ''
  20        return pk.toLowerCase().includes(query) || npub.toLowerCase().includes(query)
  21      }).slice(0, 8)
  22    }, [participants, query])
  23  
  24    if (filtered.length === 0) return null
  25  
  26    return (
  27      <div
  28        className="absolute z-30 bg-popover border rounded shadow-md max-h-48 overflow-y-auto w-56"
  29        style={{ bottom: position.bottom, left: position.left }}
  30      >
  31        {filtered.map((pk) => (
  32          <MentionItem key={pk} pubkey={pk} query={query} onSelect={onSelect} />
  33        ))}
  34      </div>
  35    )
  36  }
  37  
  38  function MentionItem({
  39    pubkey,
  40    query,
  41    onSelect
  42  }: {
  43    pubkey: string
  44    query: string
  45    onSelect: (pubkey: string, displayName: string) => void
  46  }) {
  47    const { profile } = useFetchProfile(pubkey)
  48    const pk = Pubkey.tryFromString(pubkey)
  49    const displayName = profile?.username || pk?.formatNpub(8) || pubkey.slice(0, 12)
  50    const npub = pk?.npub || ''
  51  
  52    // Profile-based filtering (name match)
  53    const nameMatch = !query || displayName.toLowerCase().includes(query)
  54    if (!nameMatch && !pubkey.toLowerCase().includes(query)) return null
  55  
  56    return (
  57      <button
  58        className="w-full px-3 py-1.5 text-left hover:bg-muted flex items-center gap-2 text-xs"
  59        onClick={() => onSelect(pubkey, displayName)}
  60      >
  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">{displayName}</span>
  65        <span className="text-muted-foreground text-[10px] truncate">{npub.slice(0, 16)}...</span>
  66      </button>
  67    )
  68  }
  69