import type { Editor } from '@tiptap/core' import { formatNpub, userIdToPubkey } from '@/lib/pubkey' import { cn } from '@/lib/utils' import { SuggestionKeyDownProps } from '@tiptap/suggestion' import { forwardRef, useEffect, useImperativeHandle, useState } from 'react' import { useTranslation } from 'react-i18next' import Nip05 from '../../../Nip05' import { SimpleUserAvatar } from '../../../UserAvatar' import { SimpleUsername } from '../../../Username' import type { PickerSearchMode } from '@/services/mention-event-search.service' import { NEVENT_NADDR_PICKER_ID } from './constants' export type MentionListItem = string | { id: string; mode?: PickerSearchMode } export interface MentionListProps { items: MentionListItem[] command: (payload: { id: string; label?: string; mode?: PickerSearchMode }) => void /** When provided, selection is controlled by parent (e.g. for plain textarea @-mentions). */ selectedIndex?: number onSelectIndex?: (index: number) => void /** When provided, used to detect if we're inside a dialog (for z-index). */ editor?: Editor } export interface MentionListHandle { onKeyDown: (args: SuggestionKeyDownProps) => boolean } const MentionList = forwardRef((props, ref) => { const { t } = useTranslation() const items = props.items ?? [] const inDialog = Boolean(props.editor?.view?.dom?.closest?.('[role="dialog"]')) const [internalIndex, setInternalIndex] = useState(0) const isControlled = props.selectedIndex !== undefined const selectedIndex = isControlled ? props.selectedIndex! : internalIndex const setSelectedIndex = isControlled ? (n: number) => props.onSelectIndex?.(n) : setInternalIndex const getItemId = (item: MentionListItem): string => typeof item === 'string' ? item : item.id const getItemMode = (item: MentionListItem): PickerSearchMode | undefined => typeof item === 'object' && item && 'mode' in item ? item.mode : undefined const selectItem = (index: number) => { const item = items[index] if (item) { const id = getItemId(item) const label = id === NEVENT_NADDR_PICKER_ID ? t('Search for event or address…') : formatNpub(id) props.command({ id, label, mode: getItemMode(item) }) } } const upHandler = () => { if (!items.length) return setSelectedIndex((selectedIndex + items.length - 1) % items.length) } const downHandler = () => { if (!items.length) return setSelectedIndex((selectedIndex + 1) % items.length) } const enterHandler = () => { selectItem(selectedIndex) } useEffect(() => { if (!isControlled) { setInternalIndex(items.length ? 0 : -1) } }, [items, isControlled]) useImperativeHandle(ref, () => ({ onKeyDown: ({ event }: SuggestionKeyDownProps) => { if (event.key === 'ArrowUp') { upHandler() return true } if (event.key === 'ArrowDown') { downHandler() return true } if (event.key === 'Enter' && selectedIndex >= 0) { enterHandler() return true } return false } })) if (!items.length) { return null } return (
e.stopPropagation()} onTouchMove={(e: React.TouchEvent) => e.stopPropagation()} > {items.map((item, index) => ( ))}
) }) MentionList.displayName = 'MentionList' export default MentionList