Good spot, working autcomplete and emojis
This commit is contained in:
+2
-1
@@ -88,7 +88,7 @@
|
|||||||
"@bsky.app/expo-image-crop-tool": "^0.5.0",
|
"@bsky.app/expo-image-crop-tool": "^0.5.0",
|
||||||
"@bsky.app/expo-translate-text": "^0.2.9",
|
"@bsky.app/expo-translate-text": "^0.2.9",
|
||||||
"@bsky.app/react-native-mmkv": "2.12.5",
|
"@bsky.app/react-native-mmkv": "2.12.5",
|
||||||
"@bsky.app/sift": "^0.2.4",
|
"@bsky.app/sift": "^0.2.5",
|
||||||
"@bsky.app/tapper": "^0.4.0",
|
"@bsky.app/tapper": "^0.4.0",
|
||||||
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
|
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
|
||||||
"@emoji-mart/data": "^1.2.1",
|
"@emoji-mart/data": "^1.2.1",
|
||||||
@@ -181,6 +181,7 @@
|
|||||||
"expo-web-browser": "~15.0.10",
|
"expo-web-browser": "~15.0.10",
|
||||||
"fast-deep-equal": "^3.1.3",
|
"fast-deep-equal": "^3.1.3",
|
||||||
"fast-text-encoding": "^1.0.6",
|
"fast-text-encoding": "^1.0.6",
|
||||||
|
"fuse.js": "^7.1.0",
|
||||||
"hls.js": "^1.6.2",
|
"hls.js": "^1.6.2",
|
||||||
"idb-keyval": "^6.2.2",
|
"idb-keyval": "^6.2.2",
|
||||||
"js-sha256": "^0.9.0",
|
"js-sha256": "^0.9.0",
|
||||||
|
|||||||
@@ -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,4 +1,5 @@
|
|||||||
export * from './Autocomplete'
|
export * from './Autocomplete'
|
||||||
|
export * from './AutocompleteItemEmoji'
|
||||||
export * from './AutocompleteItemProfile'
|
export * from './AutocompleteItemProfile'
|
||||||
export * from './useAutocomplete'
|
export * from './useAutocomplete'
|
||||||
export * from './util'
|
export * from './util'
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import {useCallback} from 'react'
|
import {useCallback} from 'react'
|
||||||
import {moderateProfile, type ModerationOpts} from '@atproto/api'
|
import {moderateProfile, type ModerationOpts} from '@atproto/api'
|
||||||
|
import {type Emoji} from '@emoji-mart/data'
|
||||||
import {keepPreviousData, useQuery} from '@tanstack/react-query'
|
import {keepPreviousData, useQuery} from '@tanstack/react-query'
|
||||||
|
import Fuse from 'fuse.js'
|
||||||
|
|
||||||
import {isJustAMute, moduiContainsHideableOffense} from '#/lib/moderation'
|
import {isJustAMute, moduiContainsHideableOffense} from '#/lib/moderation'
|
||||||
|
import {useGetEmojis} from '#/lib/useGetEmojis'
|
||||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||||
import {STALE} from '#/state/queries'
|
import {STALE} from '#/state/queries'
|
||||||
import {DEFAULT_LOGGED_OUT_PREFERENCES} from '#/state/queries/preferences'
|
import {DEFAULT_LOGGED_OUT_PREFERENCES} from '#/state/queries/preferences'
|
||||||
@@ -18,6 +21,30 @@ const DEFAULT_MOD_OPTS = {
|
|||||||
prefs: DEFAULT_LOGGED_OUT_PREFERENCES.moderationPrefs,
|
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({
|
export function useAutocomplete({
|
||||||
type,
|
type,
|
||||||
query,
|
query,
|
||||||
@@ -29,6 +56,7 @@ export function useAutocomplete({
|
|||||||
}) {
|
}) {
|
||||||
const agent = useAgent()
|
const agent = useAgent()
|
||||||
const moderationOpts = useModerationOpts()
|
const moderationOpts = useModerationOpts()
|
||||||
|
const emojiSearch = useEmojiSearch()
|
||||||
|
|
||||||
return useQuery({
|
return useQuery({
|
||||||
staleTime: STALE.MINUTES.ONE,
|
staleTime: STALE.MINUTES.ONE,
|
||||||
@@ -58,6 +86,14 @@ export function useAutocomplete({
|
|||||||
value: '@' + profile.handle,
|
value: '@' + profile.handle,
|
||||||
profile,
|
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 []
|
return []
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
/**
|
||||||
|
* TODO
|
||||||
|
*
|
||||||
|
* Native
|
||||||
|
* - make sure we limit the height of autocomplete so it doesn't go off screen
|
||||||
|
*/
|
||||||
import {
|
import {
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
@@ -36,6 +42,7 @@ import {
|
|||||||
import {normalizeTextStyles} from '#/alf/typography'
|
import {normalizeTextStyles} from '#/alf/typography'
|
||||||
import {
|
import {
|
||||||
Autocomplete as AutocompleteBase,
|
Autocomplete as AutocompleteBase,
|
||||||
|
AutocompleteItemEmoji,
|
||||||
AutocompleteItemProfile,
|
AutocompleteItemProfile,
|
||||||
parseAutocompleteItemType,
|
parseAutocompleteItemType,
|
||||||
useAutocomplete,
|
useAutocomplete,
|
||||||
@@ -371,6 +378,7 @@ export function Composer({
|
|||||||
])}
|
])}
|
||||||
onBlur={e => {
|
onBlur={e => {
|
||||||
rest.onBlur?.(e)
|
rest.onBlur?.(e)
|
||||||
|
setActiveFacet(null)
|
||||||
}}
|
}}
|
||||||
onKeyPress={IS_WEB ? onKeyPressWeb : undefined}
|
onKeyPress={IS_WEB ? onKeyPressWeb : undefined}
|
||||||
onScroll={e => {
|
onScroll={e => {
|
||||||
@@ -429,6 +437,15 @@ function AutocompleteInner({
|
|||||||
useOnKeyboard('keyboardDidShow', updatePosition)
|
useOnKeyboard('keyboardDidShow', updatePosition)
|
||||||
useOnKeyboard('keyboardDidHide', 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 ? (
|
return data && data.length ? (
|
||||||
<AutocompleteBase
|
<AutocompleteBase
|
||||||
sift={sift}
|
sift={sift}
|
||||||
@@ -437,6 +454,9 @@ function AutocompleteInner({
|
|||||||
if (props.item.type === 'profile') {
|
if (props.item.type === 'profile') {
|
||||||
return <AutocompleteItemProfile {...props} />
|
return <AutocompleteItemProfile {...props} />
|
||||||
}
|
}
|
||||||
|
if (props.item.type === 'emoji') {
|
||||||
|
return <AutocompleteItemEmoji {...props} />
|
||||||
|
}
|
||||||
return <View />
|
return <View />
|
||||||
}}
|
}}
|
||||||
onSelect={item => {
|
onSelect={item => {
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import Emojis, {type EmojiMartData} from '@emoji-mart/data'
|
||||||
|
|
||||||
|
export async function getEmojis(): Promise<EmojiMartData> {
|
||||||
|
return Emojis as EmojiMartData
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}, [])
|
||||||
|
}
|
||||||
@@ -2413,10 +2413,10 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@bsky.app/react-native-mmkv/-/react-native-mmkv-2.12.5.tgz#eb17d31a6158c74393f617a1763ac223ff3f83a6"
|
resolved "https://registry.yarnpkg.com/@bsky.app/react-native-mmkv/-/react-native-mmkv-2.12.5.tgz#eb17d31a6158c74393f617a1763ac223ff3f83a6"
|
||||||
integrity sha512-3vUz1nQY1DiKIPAWRkpp5ZGxH5f2G6Ui0UuQuEYjYv81xx1qFcSzS9KQ2sHcOKYdkOM9amWV2Q8TQCxt1lrAHg==
|
integrity sha512-3vUz1nQY1DiKIPAWRkpp5ZGxH5f2G6Ui0UuQuEYjYv81xx1qFcSzS9KQ2sHcOKYdkOM9amWV2Q8TQCxt1lrAHg==
|
||||||
|
|
||||||
"@bsky.app/sift@^0.2.4":
|
"@bsky.app/sift@^0.2.5":
|
||||||
version "0.2.4"
|
version "0.2.5"
|
||||||
resolved "https://registry.yarnpkg.com/@bsky.app/sift/-/sift-0.2.4.tgz#cfdc67d5236b3fb4d26b9b5d482420d3f804cfe1"
|
resolved "https://registry.yarnpkg.com/@bsky.app/sift/-/sift-0.2.5.tgz#6790c71f65fe4aee421f43769ab0f6b0904f0b83"
|
||||||
integrity sha512-2tUoKhTjULMPgqhIUiNaNPyyTh2vPhOMRT/2S1HWhalEsBNeMeoNqUs7KWgOtgcDtT4breGkHmR1XOo2tzYmgA==
|
integrity sha512-ImuEEp0eNoTspjjUaBfZe4QPTs/DdtMxOq2fMNvyIBz844/uDstkgTvuM4WJ1RCw5+mT0ThiovNYItuyigU3eQ==
|
||||||
|
|
||||||
"@bsky.app/tapper@^0.4.0":
|
"@bsky.app/tapper@^0.4.0":
|
||||||
version "0.4.0"
|
version "0.4.0"
|
||||||
@@ -9991,6 +9991,11 @@ functions-have-names@^1.2.2, functions-have-names@^1.2.3:
|
|||||||
resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834"
|
resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834"
|
||||||
integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==
|
integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==
|
||||||
|
|
||||||
|
fuse.js@^7.1.0:
|
||||||
|
version "7.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/fuse.js/-/fuse.js-7.1.0.tgz#306228b4befeee11e05b027087c2744158527d09"
|
||||||
|
integrity sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ==
|
||||||
|
|
||||||
gensync@^1.0.0-beta.2:
|
gensync@^1.0.0-beta.2:
|
||||||
version "1.0.0-beta.2"
|
version "1.0.0-beta.2"
|
||||||
resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"
|
resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"
|
||||||
@@ -14579,7 +14584,7 @@ react-test-renderer@19.1.0:
|
|||||||
react-is "^19.1.0"
|
react-is "^19.1.0"
|
||||||
scheduler "^0.26.0"
|
scheduler "^0.26.0"
|
||||||
|
|
||||||
react-textarea-autosize@^8.5.9:
|
react-textarea-autosize@^8.5.3:
|
||||||
version "8.5.9"
|
version "8.5.9"
|
||||||
resolved "https://registry.yarnpkg.com/react-textarea-autosize/-/react-textarea-autosize-8.5.9.tgz#ab8627b09aa04d8a2f45d5b5cd94c84d1d4a8893"
|
resolved "https://registry.yarnpkg.com/react-textarea-autosize/-/react-textarea-autosize-8.5.9.tgz#ab8627b09aa04d8a2f45d5b5cd94c84d1d4a8893"
|
||||||
integrity sha512-U1DGlIQN5AwgjTyOEnI1oCcMuEr1pv1qOtklB2l4nyMGbHzWrI0eFsYK0zos2YWqAolJyG0IWJaqWmWj5ETh0A==
|
integrity sha512-U1DGlIQN5AwgjTyOEnI1oCcMuEr1pv1qOtklB2l4nyMGbHzWrI0eFsYK0zos2YWqAolJyG0IWJaqWmWj5ETh0A==
|
||||||
|
|||||||
Reference in New Issue
Block a user