MentionList.tsx raw
1 import FollowingBadge from '@/components/FollowingBadge'
2 import { ScrollArea } from '@/components/ui/scroll-area'
3 import { Pubkey } from '@/domain'
4 import { cn } from '@/lib/utils'
5 import { SuggestionKeyDownProps } from '@tiptap/suggestion'
6 import { forwardRef, useEffect, useImperativeHandle, useState } from 'react'
7 import Nip05 from '../../../Nip05'
8 import { SimpleUserAvatar } from '../../../UserAvatar'
9 import { SimpleUsername } from '../../../Username'
10
11 export interface MentionListProps {
12 items: string[]
13 command: (payload: { id: string; label?: string }) => void
14 }
15
16 export interface MentionListHandle {
17 onKeyDown: (args: SuggestionKeyDownProps) => boolean
18 }
19
20 const MentionList = forwardRef<MentionListHandle, MentionListProps>((props, ref) => {
21 const [selectedIndex, setSelectedIndex] = useState<number>(0)
22
23 const selectItem = (index: number) => {
24 const item = props.items[index]
25
26 if (item) {
27 props.command({ id: item, label: Pubkey.tryFromString(item)?.formatNpub(12) ?? item.slice(0, 12) })
28 }
29 }
30
31 const upHandler = () => {
32 setSelectedIndex((selectedIndex + props.items.length - 1) % props.items.length)
33 }
34
35 const downHandler = () => {
36 setSelectedIndex((selectedIndex + 1) % props.items.length)
37 }
38
39 const enterHandler = () => {
40 selectItem(selectedIndex)
41 }
42
43 useEffect(() => {
44 setSelectedIndex(props.items.length ? 0 : -1)
45 }, [props.items])
46
47 useImperativeHandle(ref, () => ({
48 onKeyDown: ({ event }: SuggestionKeyDownProps) => {
49 if (event.key === 'ArrowUp') {
50 upHandler()
51 return true
52 }
53
54 if (event.key === 'ArrowDown') {
55 downHandler()
56 return true
57 }
58
59 if (event.key === 'Enter' && selectedIndex >= 0) {
60 enterHandler()
61 return true
62 }
63
64 return false
65 }
66 }))
67
68 if (!props.items?.length) {
69 return null
70 }
71
72 return (
73 <ScrollArea
74 className="border rounded-lg bg-background z-50 pointer-events-auto flex flex-col max-h-80 overflow-y-auto"
75 onWheel={(e) => e.stopPropagation()}
76 onTouchMove={(e) => e.stopPropagation()}
77 >
78 {props.items.map((item, index) => (
79 <button
80 className={cn(
81 'cursor-pointer text-start items-center m-1 p-2 outline-none transition-colors [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 rounded-md',
82 selectedIndex === index && 'bg-accent text-accent-foreground'
83 )}
84 key={item}
85 onClick={() => selectItem(index)}
86 onMouseEnter={() => setSelectedIndex(index)}
87 >
88 <div className="flex gap-2 w-80 items-center truncate pointer-events-none">
89 <SimpleUserAvatar userId={item} />
90 <div className="flex-1 w-0">
91 <div className="flex items-center gap-2">
92 <SimpleUsername userId={item} className="font-semibold truncate" />
93 <FollowingBadge userId={item} />
94 </div>
95 <Nip05 pubkey={Pubkey.tryFromString(item)?.hex ?? item} />
96 </div>
97 </div>
98 </button>
99 ))}
100 </ScrollArea>
101 )
102 })
103 MentionList.displayName = 'MentionList'
104 export default MentionList
105