diff --git a/src/view/com/composer/text-input/TextInput.web.tsx b/src/view/com/composer/text-input/TextInput.web.tsx index c55a2a72f7..f2eae42f35 100644 --- a/src/view/com/composer/text-input/TextInput.web.tsx +++ b/src/view/com/composer/text-input/TextInput.web.tsx @@ -17,8 +17,6 @@ import {useColorSchemeStyle} from '#/lib/hooks/useColorSchemeStyle' import {usePalette} from '#/lib/hooks/usePalette' import {blobToDataUri, isUriImage} from '#/lib/media/util' import {useActorAutocompleteFn} from '#/state/queries/actor-autocomplete' -import {useSession} from '#/state/session' -import {tagAutocompleteModel} from '#/view/com/composer/text-input/tagsAutocompleteState' import { LinkFacetMatch, suggestLinkCardUri, @@ -66,7 +64,6 @@ export const TextInput = React.forwardRef(function TextInputImpl( const autocomplete = useActorAutocompleteFn() const pal = usePalette('default') const modeClass = useColorSchemeStyle('ProseMirror-light', 'ProseMirror-dark') - const {currentAccount} = useSession() const [isDropping, setIsDropping] = React.useState(false) @@ -79,9 +76,7 @@ export const TextInput = React.forwardRef(function TextInputImpl( HTMLAttributes: { class: 'inline-tag', }, - suggestion: createTagsAutocomplete({ - model: tagAutocompleteModel({currentDid: currentAccount?.did!}), - }), + suggestion: createTagsAutocomplete(), }), Mention.configure({ HTMLAttributes: { @@ -97,7 +92,7 @@ export const TextInput = React.forwardRef(function TextInputImpl( History, Hardbreak, ], - [autocomplete, placeholder, currentAccount], + [autocomplete, placeholder], ) React.useEffect(() => { diff --git a/src/view/com/composer/text-input/tagsAutocompleteState.ts b/src/view/com/composer/text-input/tagsAutocompleteState.ts index bbf65695fd..5ce70da21f 100644 --- a/src/view/com/composer/text-input/tagsAutocompleteState.ts +++ b/src/view/com/composer/text-input/tagsAutocompleteState.ts @@ -1,3 +1,7 @@ +import React from 'react' +import Fuse from 'fuse.js' + +import {useSession} from '#/state/session' import {account} from '#/storage' export type Result = { @@ -5,31 +9,65 @@ export type Result = { } export type Model = { - search(query: string): Promise + readonly suggestions: Result[] + setQuery(query: string): void save(tag: string): void } -export function tagAutocompleteModel({ - currentDid, -}: { - currentDid: string -}): Model { - let recentTags = account.get([currentDid, 'recentTags']) || [] +export function useTagAutocomplete() { + const {currentAccount} = useSession() + const [query, setQuery] = React.useState('') + const [searchSuggestions, setSearchSuggestions] = React.useState([]) + + const search = React.useCallback( + async (_query: string) => { + // TODO actually search + // TODO debounce/abort controller + setSearchSuggestions([]) + }, + [setSearchSuggestions], + ) + + const onSetQuery = React.useCallback( + (query: string) => { + setQuery(query) + search(query) + }, + [setQuery, search], + ) + + const saveRecentTag = React.useCallback( + (tag: string) => { + if (!currentAccount) { + throw new Error('No current account') + } + const recentTags = account.get([currentAccount.did, 'recentTags']) || [] + account.set( + [currentAccount.did, 'recentTags'], + [{value: tag}, ...recentTags.filter(t => t.value !== tag)].slice(0, 40), + ) + }, + [currentAccount], + ) + + const suggestions: Result[] = React.useMemo(() => { + if (!currentAccount) { + throw new Error('No current account') + } + const recentTags = account.get([currentAccount.did, 'recentTags']) || [] + const items = [ + ...recentTags.map(t => t.value), + ...searchSuggestions.map(s => s.value), + ] + const fuse = new Fuse(items) + // search amongst mixed set of tags + const results = fuse.search(query).map(r => r.item) + return results.map(value => ({value})) + }, [currentAccount, query, searchSuggestions]) return { - async search(query: string) { - if (!query) return [{value: query}] - return [ - {value: query}, - ...recentTags.filter(t => t.value.includes(query)), - ] - }, - save(tag: string) { - recentTags = [ - {value: tag}, - ...recentTags.filter(t => t.value !== tag), - ].slice(0, 40) - account.set([currentDid, 'recentTags'], recentTags) - }, + suggestions, + setQuery: onSetQuery, + saveRecentTag, } } diff --git a/src/view/com/composer/text-input/web/Tags/view.tsx b/src/view/com/composer/text-input/web/Tags/view.tsx index 26cedbeb98..b627e9de6d 100644 --- a/src/view/com/composer/text-input/web/Tags/view.tsx +++ b/src/view/com/composer/text-input/web/Tags/view.tsx @@ -8,34 +8,17 @@ import { } from '@tiptap/suggestion' import tippy, {Instance as TippyInstance} from 'tippy.js' -// import {TagsAutocompleteModel} from 'state/models/ui/tags-autocomplete' import {usePalette} from '#/lib/hooks/usePalette' -import { - Model, - Result, -} from '#/view/com/composer/text-input/tagsAutocompleteState' +import {useTagAutocomplete} from '#/view/com/composer/text-input/tagsAutocompleteState' import {Text} from '#/view/com/util/text/Text' import {parsePunctuationFromTag} from './utils' -type ListProps = SuggestionProps & { - model: Model -} type AutocompleteRef = { onKeyDown: (props: SuggestionKeyDownProps) => boolean } -export function createTagsAutocomplete({ - model, -}: { - model: Model -}): Omit { +export function createTagsAutocomplete(): Omit { return { - /** - * This `query` param comes from the result of `findSuggestionMatch` - */ - async items({query}) { - return await model.search(query) - }, render() { let component: ReactRenderer | undefined let popup: TippyInstance[] | undefined @@ -43,10 +26,7 @@ export function createTagsAutocomplete({ return { onStart: props => { component = new ReactRenderer(Autocomplete, { - props: { - ...props, - model, - }, + props, editor: props.editor, }) @@ -93,12 +73,17 @@ export function createTagsAutocomplete({ } } -const Autocomplete = forwardRef( +const Autocomplete = forwardRef( function AutocompleteImpl(props, ref) { - const {items, command, model} = props + const {command, query} = props + const {suggestions, setQuery, saveRecentTag} = useTagAutocomplete() const pal = usePalette('default') const [selectedIndex, setSelectedIndex] = useState(0) + React.useEffect(() => { + setQuery(query) + }, [query, setQuery]) + const commit = React.useCallback( (query: string) => { const {tag, punctuation} = parsePunctuationFromTag(query) @@ -111,36 +96,36 @@ const Autocomplete = forwardRef( * only want to `commitRecentTag` with the sanitized tag. */ command({tag, punctuation}) - model.save(tag) + saveRecentTag(tag) }, - [command, model], + [command, saveRecentTag], ) const selectItem = React.useCallback( (index: number) => { - const item = items[index] + const item = suggestions[index] if (item) commit(item.value) }, - [items, commit], + [suggestions, commit], ) useImperativeHandle(ref, () => ({ onKeyDown: ({event}) => { if (event.key === 'ArrowUp') { setSelectedIndex( - (selectedIndex + props.items.length - 1) % props.items.length, + (selectedIndex + suggestions.length - 1) % suggestions.length, ) return true } if (event.key === 'ArrowDown') { - setSelectedIndex((selectedIndex + 1) % props.items.length) + setSelectedIndex((selectedIndex + 1) % suggestions.length) return true } if (event.key === 'Enter') { - if (!props.items.length) { - // no items, use whatever the user typed + if (!suggestions.length) { + // no suggestions, use whatever the user typed commit(props.query) } else { selectItem(selectedIndex) @@ -158,16 +143,16 @@ const Autocomplete = forwardRef( })) // hide entirely if no suggestions - if (!items.length) return null + if (!suggestions.length) return null return (
- {items.map(({value}, index) => { + {suggestions.map(({value}, index) => { const {tag} = parsePunctuationFromTag(value) const isSelected = selectedIndex === index const isFirst = index === 0 - const isLast = index === items.length - 1 + const isLast = index === suggestions.length - 1 return (