Improve autocomplete

This commit is contained in:
Eric Bailey
2024-10-22 16:03:17 -05:00
parent 91d50e70dc
commit 43409aaba6
3 changed files with 82 additions and 64 deletions
@@ -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(() => {
@@ -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<Result[]>
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<Result[]>([])
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,
}
}
@@ -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<Result> & {
model: Model
}
type AutocompleteRef = {
onKeyDown: (props: SuggestionKeyDownProps) => boolean
}
export function createTagsAutocomplete({
model,
}: {
model: Model
}): Omit<SuggestionOptions, 'editor'> {
export function createTagsAutocomplete(): Omit<SuggestionOptions, 'editor'> {
return {
/**
* This `query` param comes from the result of `findSuggestionMatch`
*/
async items({query}) {
return await model.search(query)
},
render() {
let component: ReactRenderer<AutocompleteRef> | 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<AutocompleteRef, ListProps>(
const Autocomplete = forwardRef<AutocompleteRef, SuggestionProps>(
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<AutocompleteRef, ListProps>(
* 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<AutocompleteRef, ListProps>(
}))
// hide entirely if no suggestions
if (!items.length) return null
if (!suggestions.length) return null
return (
<div className="items">
<View style={[pal.borderDark, pal.view, styles.container]}>
{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 (
<Pressable