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