Good spot, working autcomplete and emojis

This commit is contained in:
Eric Bailey
2026-04-01 09:13:45 -05:00
parent 00a6207fd8
commit 7cbb11228a
9 changed files with 120 additions and 6 deletions
@@ -0,0 +1,29 @@
import {SiftItem} from '@bsky.app/sift'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {atoms as a, useTheme} from '#/alf'
import {Text} from '#/components/Typography'
import {type AutocompleteItemProps} from './types'
export function AutocompleteItemEmoji({
active,
props,
item,
}: AutocompleteItemProps) {
const t = useTheme()
const moderationOpts = useModerationOpts()
if (item.type !== 'emoji' || !moderationOpts) return null
return (
<SiftItem
{...props}
style={s => [
a.px_md,
a.py_sm,
active || s.hovered || s.pressed ? [t.atoms.bg_contrast_25] : [],
]}>
<Text>{item.value}</Text>
</SiftItem>
)
}
+1
View File
@@ -1,4 +1,5 @@
export * from './Autocomplete'
export * from './AutocompleteItemEmoji'
export * from './AutocompleteItemProfile'
export * from './useAutocomplete'
export * from './util'
@@ -1,8 +1,11 @@
import {useCallback} from 'react'
import {moderateProfile, type ModerationOpts} from '@atproto/api'
import {type Emoji} from '@emoji-mart/data'
import {keepPreviousData, useQuery} from '@tanstack/react-query'
import Fuse from 'fuse.js'
import {isJustAMute, moduiContainsHideableOffense} from '#/lib/moderation'
import {useGetEmojis} from '#/lib/useGetEmojis'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {STALE} from '#/state/queries'
import {DEFAULT_LOGGED_OUT_PREFERENCES} from '#/state/queries/preferences'
@@ -18,6 +21,30 @@ const DEFAULT_MOD_OPTS = {
prefs: DEFAULT_LOGGED_OUT_PREFERENCES.moderationPrefs,
}
/*
* Lazily loaded Fuse instance for emoji search. Built once on first search,
* then reused for all subsequent searches.
*/
let emojiFuseInstance: Fuse<Emoji> | null = null
function useEmojiSearch() {
const getEmojis = useGetEmojis()
return useCallback(
async (query: string, limit: number = 8) => {
if (!emojiFuseInstance) {
const data = await getEmojis()
emojiFuseInstance = new Fuse(Object.values(data.emojis), {
keys: ['search'],
threshold: 0.3,
})
}
return emojiFuseInstance.search(query, {limit})
},
[getEmojis],
)
}
export function useAutocomplete({
type,
query,
@@ -29,6 +56,7 @@ export function useAutocomplete({
}) {
const agent = useAgent()
const moderationOpts = useModerationOpts()
const emojiSearch = useEmojiSearch()
return useQuery({
staleTime: STALE.MINUTES.ONE,
@@ -58,6 +86,14 @@ export function useAutocomplete({
value: '@' + profile.handle,
profile,
}))
} else if (type === 'emoji') {
const results = await emojiSearch(query, limit || 8)
return results.map(result => ({
key: result.item.id,
type: 'emoji' as const,
value: result.item.skins[0].native,
emoji: result.item.skins[0].native,
}))
}
return []
+20
View File
@@ -1,3 +1,9 @@
/**
* TODO
*
* Native
* - make sure we limit the height of autocomplete so it doesn't go off screen
*/
import {
useCallback,
useEffect,
@@ -36,6 +42,7 @@ import {
import {normalizeTextStyles} from '#/alf/typography'
import {
Autocomplete as AutocompleteBase,
AutocompleteItemEmoji,
AutocompleteItemProfile,
parseAutocompleteItemType,
useAutocomplete,
@@ -371,6 +378,7 @@ export function Composer({
])}
onBlur={e => {
rest.onBlur?.(e)
setActiveFacet(null)
}}
onKeyPress={IS_WEB ? onKeyPressWeb : undefined}
onScroll={e => {
@@ -429,6 +437,15 @@ function AutocompleteInner({
useOnKeyboard('keyboardDidShow', updatePosition)
useOnKeyboard('keyboardDidHide', updatePosition)
useEffect(() => {
if (activeFacet?.type === 'emoji' && activeFacet.raw.endsWith(':')) {
if (data?.[0]) {
activeFacet.replace(data[0].value, {noTrailingSpace: true})
onDismiss()
}
}
}, [data, activeFacet])
return data && data.length ? (
<AutocompleteBase
sift={sift}
@@ -437,6 +454,9 @@ function AutocompleteInner({
if (props.item.type === 'profile') {
return <AutocompleteItemProfile {...props} />
}
if (props.item.type === 'emoji') {
return <AutocompleteItemEmoji {...props} />
}
return <View />
}}
onSelect={item => {
+5
View File
@@ -0,0 +1,5 @@
import Emojis, {type EmojiMartData} from '@emoji-mart/data'
export async function getEmojis(): Promise<EmojiMartData> {
return Emojis as EmojiMartData
}
+5
View File
@@ -0,0 +1,5 @@
import {type EmojiMartData} from '@emoji-mart/data'
export async function getEmojis(): Promise<EmojiMartData> {
return (await import('@emoji-mart/data')).default as EmojiMartData
}
+12
View File
@@ -0,0 +1,12 @@
import {useCallback} from 'react'
import {getEmojis} from './getEmojis'
let emojis: Awaited<ReturnType<typeof getEmojis>> | null = null
export function useGetEmojis() {
return useCallback(async () => {
emojis ??= await getEmojis()
return emojis
}, [])
}