Improve autocomplete
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user