import {forwardRef, useEffect, useImperativeHandle, useState} from 'react' import {Pressable, View} from 'react-native' import {type AppBskyActorDefs, type ModerationOpts} from '@atproto/api' import {Trans} from '@lingui/macro' import {ReactRenderer} from '@tiptap/react' import { type SuggestionKeyDownProps, type SuggestionOptions, type SuggestionProps, } from '@tiptap/suggestion' import tippy, {type Instance as TippyInstance} from 'tippy.js' import {useModerationOpts} from '#/state/preferences/moderation-opts' import {type ActorAutocompleteFn} from '#/state/queries/actor-autocomplete' import {atoms as a, useTheme} from '#/alf' import * as ProfileCard from '#/components/ProfileCard' import {Text} from '#/components/Typography' interface MentionListRef { onKeyDown: (props: SuggestionKeyDownProps) => boolean } export interface AutocompleteRef { maybeClose: () => boolean } export function createSuggestion({ autocomplete, autocompleteRef, }: { autocomplete: ActorAutocompleteFn autocompleteRef: React.Ref }): Omit { return { async items({query}) { const suggestions = await autocomplete({query}) return suggestions.slice(0, 8) }, render: () => { let component: ReactRenderer | undefined let popup: TippyInstance[] | undefined const hide = () => { popup?.[0]?.destroy() component?.destroy() } return { onStart: props => { component = new ReactRenderer(MentionList, { props: {...props, autocompleteRef, hide}, editor: props.editor, }) if (!props.clientRect) { return } // @ts-ignore getReferenceClientRect doesnt like that clientRect can return null -prf popup = tippy('body', { getReferenceClientRect: props.clientRect, appendTo: () => document.body, content: component.element, showOnCreate: true, interactive: true, trigger: 'manual', placement: 'bottom-start', }) }, onUpdate(props) { component?.updateProps(props) if (!props.clientRect) { return } popup?.[0]?.setProps({ // @ts-ignore getReferenceClientRect doesnt like that clientRect can return null -prf getReferenceClientRect: props.clientRect, }) }, onKeyDown(props) { if (props.event.key === 'Escape') { return false } return component?.ref?.onKeyDown(props) || false }, onExit() { hide() }, } }, } } const MentionList = forwardRef< MentionListRef, SuggestionProps & { autocompleteRef: React.Ref hide: () => void } >(function MentionListImpl({items, command, hide, autocompleteRef}, ref) { const [selectedIndex, setSelectedIndex] = useState(0) const t = useTheme() const moderationOpts = useModerationOpts() const selectItem = (index: number) => { const item = items[index] if (item) { command({id: item.handle}) } } const upHandler = () => { setSelectedIndex((selectedIndex + items.length - 1) % items.length) } const downHandler = () => { setSelectedIndex((selectedIndex + 1) % items.length) } const enterHandler = () => { selectItem(selectedIndex) } useEffect(() => setSelectedIndex(0), [items]) useImperativeHandle(autocompleteRef, () => ({ maybeClose: () => { hide() return true }, })) useImperativeHandle(ref, () => ({ onKeyDown: ({event}) => { if (event.key === 'ArrowUp') { upHandler() return true } if (event.key === 'ArrowDown') { downHandler() return true } if (event.key === 'Enter' || event.key === 'Tab') { enterHandler() return true } return false }, })) if (!moderationOpts) return null return (
{items.length > 0 ? ( items.map((item, index) => { const isSelected = selectedIndex === index return ( selectItem(index)} onHover={() => setSelectedIndex(index)} moderationOpts={moderationOpts} /> ) }) ) : ( No result )}
) }) function AutocompleteProfileCard({ profile, isSelected, onPress, onHover, moderationOpts, }: { profile: AppBskyActorDefs.ProfileViewBasic isSelected: boolean onPress: () => void onHover: () => void moderationOpts: ModerationOpts }) { const t = useTheme() return ( ) }