Integrate Sift into desktop search
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import {useCallback} from 'react'
|
import {useCallback} from 'react'
|
||||||
|
import {View} from 'react-native'
|
||||||
import {Sift, type UseSiftReturn} from '@bsky.app/sift'
|
import {Sift, type UseSiftReturn} from '@bsky.app/sift'
|
||||||
|
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
@@ -6,17 +7,35 @@ import {type AutocompleteItem} from '#/components/Autocomplete/types'
|
|||||||
import {useOnKeyboard} from '#/components/hooks/useOnKeyboard'
|
import {useOnKeyboard} from '#/components/hooks/useOnKeyboard'
|
||||||
import {Portal} from '#/components/Portal'
|
import {Portal} from '#/components/Portal'
|
||||||
import {IS_WEB} from '#/env'
|
import {IS_WEB} from '#/env'
|
||||||
|
import {AutocompleteItemEmoji} from './AutocompleteItemEmoji'
|
||||||
|
import {AutocompleteItemProfile} from './AutocompleteItemProfile'
|
||||||
|
import {AutocompleteItemSearch} from './AutocompleteItemSearch'
|
||||||
|
|
||||||
|
function renderItem(
|
||||||
|
item: Parameters<Parameters<typeof Sift<AutocompleteItem>>[0]['render']>[0],
|
||||||
|
) {
|
||||||
|
switch (item.item.type) {
|
||||||
|
case 'profile':
|
||||||
|
return <AutocompleteItemProfile {...item} />
|
||||||
|
case 'emoji':
|
||||||
|
return <AutocompleteItemEmoji {...item} />
|
||||||
|
case 'search':
|
||||||
|
return <AutocompleteItemSearch {...item} />
|
||||||
|
default:
|
||||||
|
return <View />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function Autocomplete({
|
export function Autocomplete({
|
||||||
sift,
|
sift,
|
||||||
data,
|
data,
|
||||||
render,
|
render = renderItem,
|
||||||
onSelect,
|
onSelect,
|
||||||
onDismiss,
|
onDismiss,
|
||||||
}: {
|
}: {
|
||||||
sift: UseSiftReturn
|
sift: UseSiftReturn
|
||||||
data: AutocompleteItem[]
|
data: AutocompleteItem[]
|
||||||
render: Parameters<typeof Sift<AutocompleteItem>>[0]['render']
|
render?: Parameters<typeof Sift<AutocompleteItem>>[0]['render']
|
||||||
onSelect: (item: AutocompleteItem) => void
|
onSelect: (item: AutocompleteItem) => void
|
||||||
onDismiss: () => void
|
onDismiss: () => void
|
||||||
}) {
|
}) {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import {SiftItem} from '@bsky.app/sift'
|
import {SiftItem} from '@bsky.app/sift'
|
||||||
|
|
||||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import {type AutocompleteItemProps} from './types'
|
import {type AutocompleteItemProps} from './types'
|
||||||
@@ -11,9 +10,8 @@ export function AutocompleteItemEmoji({
|
|||||||
item,
|
item,
|
||||||
}: AutocompleteItemProps) {
|
}: AutocompleteItemProps) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const moderationOpts = useModerationOpts()
|
|
||||||
|
|
||||||
if (item.type !== 'emoji' || !moderationOpts) return null
|
if (item.type !== 'emoji') return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SiftItem
|
<SiftItem
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import {type AutocompleteItemProps} from './types'
|
|||||||
|
|
||||||
export function AutocompleteItemProfile({
|
export function AutocompleteItemProfile({
|
||||||
active,
|
active,
|
||||||
|
isFirst,
|
||||||
|
isLast,
|
||||||
props,
|
props,
|
||||||
item,
|
item,
|
||||||
}: AutocompleteItemProps) {
|
}: AutocompleteItemProps) {
|
||||||
@@ -19,9 +21,15 @@ export function AutocompleteItemProfile({
|
|||||||
<SiftItem
|
<SiftItem
|
||||||
{...props}
|
{...props}
|
||||||
style={s => [
|
style={s => [
|
||||||
a.px_md,
|
|
||||||
a.py_sm,
|
a.py_sm,
|
||||||
|
a.px_md,
|
||||||
active || s.hovered || s.pressed ? [t.atoms.bg_contrast_25] : [],
|
active || s.hovered || s.pressed ? [t.atoms.bg_contrast_25] : [],
|
||||||
|
isFirst && {
|
||||||
|
paddingTop: a.py_sm.paddingTop * 1.2,
|
||||||
|
},
|
||||||
|
isLast && {
|
||||||
|
paddingBottom: a.py_sm.paddingTop * 1.2,
|
||||||
|
},
|
||||||
]}>
|
]}>
|
||||||
<ProfileCard.Header>
|
<ProfileCard.Header>
|
||||||
<ProfileCard.Avatar
|
<ProfileCard.Avatar
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import {View} from 'react-native'
|
||||||
|
import {SiftItem} from '@bsky.app/sift'
|
||||||
|
|
||||||
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
|
import {MagnifyingGlass_Stroke2_Corner0_Rounded as MagnifyingGlassIcon} from '#/components/icons/MagnifyingGlass'
|
||||||
|
import {Text} from '#/components/Typography'
|
||||||
|
import {type AutocompleteItemProps} from './types'
|
||||||
|
|
||||||
|
export function AutocompleteItemSearch({
|
||||||
|
active,
|
||||||
|
isFirst,
|
||||||
|
isLast,
|
||||||
|
props,
|
||||||
|
item,
|
||||||
|
}: AutocompleteItemProps) {
|
||||||
|
const t = useTheme()
|
||||||
|
|
||||||
|
if (item.type !== 'search') return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SiftItem
|
||||||
|
{...props}
|
||||||
|
style={s => [
|
||||||
|
a.py_sm,
|
||||||
|
a.px_md,
|
||||||
|
a.flex_row,
|
||||||
|
a.align_center,
|
||||||
|
a.gap_sm,
|
||||||
|
active || s.hovered || s.pressed ? [t.atoms.bg_contrast_25] : [],
|
||||||
|
isFirst && {
|
||||||
|
paddingTop: a.py_sm.paddingTop * 1.2,
|
||||||
|
},
|
||||||
|
isLast && {
|
||||||
|
paddingBottom: a.py_sm.paddingTop * 1.2,
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.align_center,
|
||||||
|
{
|
||||||
|
width: 40,
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
<MagnifyingGlassIcon fill={t.atoms.text_contrast_low.color} size="xl" />
|
||||||
|
</View>
|
||||||
|
<Text style={[a.text_md, a.leading_snug]}>{item.value}</Text>
|
||||||
|
</SiftItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
export * from './Autocomplete'
|
export * from './Autocomplete'
|
||||||
export * from './AutocompleteItemEmoji'
|
export * from './AutocompleteItemEmoji'
|
||||||
export * from './AutocompleteItemProfile'
|
export * from './AutocompleteItemProfile'
|
||||||
|
export * from './types'
|
||||||
export * from './useAutocomplete'
|
export * from './useAutocomplete'
|
||||||
export * from './util'
|
export * from './util'
|
||||||
|
|||||||
@@ -24,13 +24,25 @@ export type AutocompleteEmoji = {
|
|||||||
emoji: Emoji
|
emoji: Emoji
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type AutocompleteSearch = {
|
||||||
|
key: string
|
||||||
|
type: 'search'
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
|
||||||
export type AutocompleteItem =
|
export type AutocompleteItem =
|
||||||
| AutocompleteProfile
|
| AutocompleteProfile
|
||||||
| AutocompleteTag
|
| AutocompleteTag
|
||||||
| AutocompleteEmoji
|
| AutocompleteEmoji
|
||||||
|
| AutocompleteSearch
|
||||||
|
|
||||||
export type AutocompleteItemType = AutocompleteItem['type']
|
export type AutocompleteItemType = AutocompleteItem['type']
|
||||||
|
|
||||||
export type AutocompleteItemProps = Parameters<
|
export type AutocompleteItemProps = Parameters<
|
||||||
Parameters<typeof Sift<AutocompleteItem>>[0]['render']
|
Parameters<typeof Sift<AutocompleteItem>>[0]['render']
|
||||||
>[0]
|
>[0]
|
||||||
|
|
||||||
|
export type AutocompleteApi = {
|
||||||
|
query: string
|
||||||
|
items: AutocompleteItem[]
|
||||||
|
}
|
||||||
|
|||||||
+25
-9
@@ -11,6 +11,7 @@ import {STALE} from '#/state/queries'
|
|||||||
import {DEFAULT_LOGGED_OUT_PREFERENCES} from '#/state/queries/preferences'
|
import {DEFAULT_LOGGED_OUT_PREFERENCES} from '#/state/queries/preferences'
|
||||||
import {useAgent} from '#/state/session'
|
import {useAgent} from '#/state/session'
|
||||||
import {
|
import {
|
||||||
|
type AutocompleteApi,
|
||||||
type AutocompleteItem,
|
type AutocompleteItem,
|
||||||
type AutocompleteItemType,
|
type AutocompleteItemType,
|
||||||
type AutocompleteProfile,
|
type AutocompleteProfile,
|
||||||
@@ -47,24 +48,26 @@ function useEmojiSearch() {
|
|||||||
|
|
||||||
export function useAutocomplete({
|
export function useAutocomplete({
|
||||||
type,
|
type,
|
||||||
query,
|
query: q,
|
||||||
limit,
|
limit,
|
||||||
|
showSearchFallback = false,
|
||||||
}: {
|
}: {
|
||||||
type: AutocompleteItemType
|
type: AutocompleteItemType
|
||||||
query: string
|
query: string
|
||||||
limit?: number
|
limit?: number
|
||||||
}) {
|
showSearchFallback?: boolean
|
||||||
|
}): AutocompleteApi {
|
||||||
const agent = useAgent()
|
const agent = useAgent()
|
||||||
const moderationOpts = useModerationOpts()
|
const moderationOpts = useModerationOpts()
|
||||||
const emojiSearch = useEmojiSearch()
|
const emojiSearch = useEmojiSearch()
|
||||||
|
|
||||||
return useQuery({
|
const query = useQuery({
|
||||||
staleTime: STALE.MINUTES.ONE,
|
staleTime: STALE.MINUTES.ONE,
|
||||||
queryKey: [
|
queryKey: [
|
||||||
'autocomplete',
|
'autocomplete',
|
||||||
{
|
{
|
||||||
type,
|
type,
|
||||||
query,
|
query: q,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
async queryFn() {
|
async queryFn() {
|
||||||
@@ -73,10 +76,10 @@ export function useAutocomplete({
|
|||||||
if (!query) return []
|
if (!query) return []
|
||||||
|
|
||||||
// Going from "foo" to "foo." should not clear matches.
|
// Going from "foo" to "foo." should not clear matches.
|
||||||
query = query.toLowerCase().trim().replace(/\.$/, '')
|
q = q.toLowerCase().trim().replace(/\.$/, '')
|
||||||
|
|
||||||
const res = await agent.searchActorsTypeahead({
|
const res = await agent.searchActorsTypeahead({
|
||||||
q: query,
|
q,
|
||||||
limit: limit || 8,
|
limit: limit || 8,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -87,7 +90,7 @@ export function useAutocomplete({
|
|||||||
profile,
|
profile,
|
||||||
}))
|
}))
|
||||||
} else if (type === 'emoji') {
|
} else if (type === 'emoji') {
|
||||||
const results = await emojiSearch(query, limit || 8)
|
const results = await emojiSearch(q, limit || 8)
|
||||||
return results.map(result => ({
|
return results.map(result => ({
|
||||||
key: result.item.id,
|
key: result.item.id,
|
||||||
type: 'emoji' as const,
|
type: 'emoji' as const,
|
||||||
@@ -109,7 +112,7 @@ export function useAutocomplete({
|
|||||||
|
|
||||||
if (item.type === 'profile') {
|
if (item.type === 'profile') {
|
||||||
const moderated = moderateProfileItem({
|
const moderated = moderateProfileItem({
|
||||||
query,
|
query: q,
|
||||||
item,
|
item,
|
||||||
moderationOpts: moderationOpts || DEFAULT_MOD_OPTS,
|
moderationOpts: moderationOpts || DEFAULT_MOD_OPTS,
|
||||||
})
|
})
|
||||||
@@ -119,12 +122,25 @@ export function useAutocomplete({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (showSearchFallback && q) {
|
||||||
|
results.unshift({
|
||||||
|
key: `search-${q}`,
|
||||||
|
type: 'search' as const,
|
||||||
|
value: q,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return results
|
return results
|
||||||
},
|
},
|
||||||
[query, moderationOpts],
|
[q, showSearchFallback, moderationOpts],
|
||||||
),
|
),
|
||||||
placeholderData: keepPreviousData,
|
placeholderData: keepPreviousData,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
query: q,
|
||||||
|
items: query.data || [],
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function moderateProfileItem({
|
function moderateProfileItem({
|
||||||
@@ -403,7 +403,7 @@ function AutocompleteInner({
|
|||||||
activeFacet: TapperActiveFacet
|
activeFacet: TapperActiveFacet
|
||||||
onDismiss: () => void
|
onDismiss: () => void
|
||||||
}) {
|
}) {
|
||||||
const {data} = useAutocomplete({
|
const {items} = useAutocomplete({
|
||||||
type: parseAutocompleteItemType(activeFacet.type),
|
type: parseAutocompleteItemType(activeFacet.type),
|
||||||
query: activeFacet.value,
|
query: activeFacet.value,
|
||||||
})
|
})
|
||||||
@@ -417,17 +417,17 @@ function AutocompleteInner({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (activeFacet?.type === 'emoji' && activeFacet.raw.endsWith(':')) {
|
if (activeFacet?.type === 'emoji' && activeFacet.raw.endsWith(':')) {
|
||||||
if (data?.[0]) {
|
if (items?.[0]) {
|
||||||
activeFacet.replace(data[0].value, {noTrailingSpace: true})
|
activeFacet.replace(items[0].value, {noTrailingSpace: true})
|
||||||
onDismiss()
|
onDismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [data, activeFacet])
|
}, [items, activeFacet])
|
||||||
|
|
||||||
return data && data.length ? (
|
return items && items.length ? (
|
||||||
<AutocompleteBase
|
<AutocompleteBase
|
||||||
sift={sift}
|
sift={sift}
|
||||||
data={data}
|
data={items}
|
||||||
render={props => {
|
render={props => {
|
||||||
if (props.item.type === 'profile') {
|
if (props.item.type === 'profile') {
|
||||||
return <AutocompleteItemProfile {...props} />
|
return <AutocompleteItemProfile {...props} />
|
||||||
|
|||||||
@@ -1,11 +1,18 @@
|
|||||||
import {memo} from 'react'
|
import {memo} from 'react'
|
||||||
import {ActivityIndicator, View} from 'react-native'
|
import {
|
||||||
|
ActivityIndicator,
|
||||||
|
TouchableOpacity,
|
||||||
|
View,
|
||||||
|
type ViewStyle,
|
||||||
|
} from 'react-native'
|
||||||
import {type AppBskyActorDefs} from '@atproto/api'
|
import {type AppBskyActorDefs} from '@atproto/api'
|
||||||
import {msg} from '@lingui/core/macro'
|
import {msg} from '@lingui/core/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
|
import {usePalette} from '#/lib/hooks/usePalette'
|
||||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||||
import {SearchLinkCard} from '#/view/shell/desktop/Search'
|
import {Link} from '#/view/com/util/Link'
|
||||||
|
import {Text} from '#/view/com/util/text/Text'
|
||||||
import {SearchProfileCard} from '#/screens/Search/components/SearchProfileCard'
|
import {SearchProfileCard} from '#/screens/Search/components/SearchProfileCard'
|
||||||
import {atoms as a, native} from '#/alf'
|
import {atoms as a, native} from '#/alf'
|
||||||
import * as Layout from '#/components/Layout'
|
import * as Layout from '#/components/Layout'
|
||||||
@@ -76,3 +83,54 @@ let AutocompleteResults = ({
|
|||||||
}
|
}
|
||||||
AutocompleteResults = memo(AutocompleteResults)
|
AutocompleteResults = memo(AutocompleteResults)
|
||||||
export {AutocompleteResults}
|
export {AutocompleteResults}
|
||||||
|
|
||||||
|
let SearchLinkCard = ({
|
||||||
|
label,
|
||||||
|
to,
|
||||||
|
onPress,
|
||||||
|
style,
|
||||||
|
}: {
|
||||||
|
label: string
|
||||||
|
to?: string
|
||||||
|
onPress?: () => void
|
||||||
|
style?: ViewStyle
|
||||||
|
}): React.ReactNode => {
|
||||||
|
const pal = usePalette('default')
|
||||||
|
|
||||||
|
const inner = (
|
||||||
|
<View
|
||||||
|
style={[pal.border, {paddingVertical: 16, paddingHorizontal: 12}, style]}>
|
||||||
|
<Text type="md" style={[pal.text]}>
|
||||||
|
{label}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
|
||||||
|
if (onPress) {
|
||||||
|
return (
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={onPress}
|
||||||
|
accessibilityLabel={label}
|
||||||
|
accessibilityHint="">
|
||||||
|
{inner}
|
||||||
|
</TouchableOpacity>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link href={to} asAnchor anchorNoUnderline>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
pal.border,
|
||||||
|
{paddingVertical: 16, paddingHorizontal: 12},
|
||||||
|
style,
|
||||||
|
]}>
|
||||||
|
<Text type="md" style={[pal.text]}>
|
||||||
|
{label}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
SearchLinkCard = memo(SearchLinkCard)
|
||||||
|
export {SearchLinkCard}
|
||||||
|
|||||||
@@ -1,166 +1,108 @@
|
|||||||
import {memo, useCallback, useState} from 'react'
|
import {useState} from 'react'
|
||||||
import {
|
import {View} from 'react-native'
|
||||||
ActivityIndicator,
|
import {useSift} from '@bsky.app/sift'
|
||||||
StyleSheet,
|
|
||||||
TouchableOpacity,
|
|
||||||
View,
|
|
||||||
type ViewStyle,
|
|
||||||
} from 'react-native'
|
|
||||||
import {useLingui} from '@lingui/react/macro'
|
|
||||||
import {StackActions, useNavigation} from '@react-navigation/native'
|
import {StackActions, useNavigation} from '@react-navigation/native'
|
||||||
|
|
||||||
import {usePalette} from '#/lib/hooks/usePalette'
|
|
||||||
import {type NavigationProp} from '#/lib/routes/types'
|
import {type NavigationProp} from '#/lib/routes/types'
|
||||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
|
||||||
import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete'
|
|
||||||
import {Link} from '#/view/com/util/Link'
|
|
||||||
import {Text} from '#/view/com/util/text/Text'
|
|
||||||
import {SearchProfileCard} from '#/screens/Search/components/SearchProfileCard'
|
|
||||||
import {atoms as a} from '#/alf'
|
import {atoms as a} from '#/alf'
|
||||||
|
import {
|
||||||
|
Autocomplete as AutocompleteBase,
|
||||||
|
type AutocompleteItem,
|
||||||
|
useAutocomplete,
|
||||||
|
} from '#/components/Autocomplete'
|
||||||
import {SearchInput} from '#/components/forms/SearchInput'
|
import {SearchInput} from '#/components/forms/SearchInput'
|
||||||
|
|
||||||
let SearchLinkCard = ({
|
export function DesktopSearch() {
|
||||||
label,
|
const navigation = useNavigation<NavigationProp>()
|
||||||
to,
|
const [active, setActive] = useState(false)
|
||||||
onPress,
|
const [query, setQuery] = useState<string>('')
|
||||||
style,
|
const showResults = active && !!query.length
|
||||||
}: {
|
|
||||||
label: string
|
|
||||||
to?: string
|
|
||||||
onPress?: () => void
|
|
||||||
style?: ViewStyle
|
|
||||||
}): React.ReactNode => {
|
|
||||||
const pal = usePalette('default')
|
|
||||||
|
|
||||||
const inner = (
|
const sift = useSift({
|
||||||
<View
|
offset: a.p_sm.padding,
|
||||||
style={[pal.border, {paddingVertical: 16, paddingHorizontal: 12}, style]}>
|
placement: 'bottom',
|
||||||
<Text type="md" style={[pal.text]}>
|
})
|
||||||
{label}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
|
|
||||||
if (onPress) {
|
const onFocus = () => {
|
||||||
return (
|
if (query.length) setActive(true)
|
||||||
<TouchableOpacity
|
}
|
||||||
onPress={onPress}
|
|
||||||
accessibilityLabel={label}
|
const onChangeText = (text: string) => {
|
||||||
accessibilityHint="">
|
setQuery(text)
|
||||||
{inner}
|
if (!active) {
|
||||||
</TouchableOpacity>
|
setActive(true)
|
||||||
)
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onClearText = () => {
|
||||||
|
setQuery('')
|
||||||
|
setActive(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onSubmit = () => {
|
||||||
|
if (!query.length) return
|
||||||
|
onClearText()
|
||||||
|
sift.elements.input.blur()
|
||||||
|
navigation.dispatch(StackActions.push('Search', {q: query}))
|
||||||
|
}
|
||||||
|
|
||||||
|
const onSelect = (item: AutocompleteItem) => {
|
||||||
|
if (item.type === 'profile') {
|
||||||
|
onClearText()
|
||||||
|
sift.elements.input.blur()
|
||||||
|
navigation.navigate('Profile', {name: item.profile.handle})
|
||||||
|
} else if (item.type === 'search') {
|
||||||
|
onClearText()
|
||||||
|
sift.elements.input.blur()
|
||||||
|
navigation.navigate('Search', {q: item.value})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link href={to} asAnchor anchorNoUnderline>
|
<View collapsable={false} ref={sift.refs.setAnchor}>
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
pal.border,
|
|
||||||
{paddingVertical: 16, paddingHorizontal: 12},
|
|
||||||
style,
|
|
||||||
]}>
|
|
||||||
<Text type="md" style={[pal.text]}>
|
|
||||||
{label}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</Link>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
SearchLinkCard = memo(SearchLinkCard)
|
|
||||||
export {SearchLinkCard}
|
|
||||||
|
|
||||||
export function DesktopSearch() {
|
|
||||||
const {t: l} = useLingui()
|
|
||||||
const pal = usePalette('default')
|
|
||||||
const navigation = useNavigation<NavigationProp>()
|
|
||||||
const [isActive, setIsActive] = useState<boolean>(false)
|
|
||||||
const [query, setQuery] = useState<string>('')
|
|
||||||
const {data: autocompleteData, isFetching} = useActorAutocompleteQuery(
|
|
||||||
query,
|
|
||||||
true,
|
|
||||||
)
|
|
||||||
|
|
||||||
const moderationOpts = useModerationOpts()
|
|
||||||
|
|
||||||
const onChangeText = useCallback((text: string) => {
|
|
||||||
setQuery(text)
|
|
||||||
setIsActive(text.length > 0)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const onPressCancelSearch = useCallback(() => {
|
|
||||||
setQuery('')
|
|
||||||
setIsActive(false)
|
|
||||||
}, [setQuery])
|
|
||||||
|
|
||||||
const onSubmit = useCallback(() => {
|
|
||||||
setIsActive(false)
|
|
||||||
if (!query.length) return
|
|
||||||
navigation.dispatch(StackActions.push('Search', {q: query}))
|
|
||||||
}, [query, navigation])
|
|
||||||
|
|
||||||
const onSearchProfileCardPress = useCallback(() => {
|
|
||||||
setQuery('')
|
|
||||||
setIsActive(false)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View style={[styles.container, pal.view]}>
|
|
||||||
<SearchInput
|
<SearchInput
|
||||||
value={query}
|
value={query}
|
||||||
|
onFocus={onFocus}
|
||||||
onChangeText={onChangeText}
|
onChangeText={onChangeText}
|
||||||
onClearText={onPressCancelSearch}
|
onClearText={onClearText}
|
||||||
onSubmitEditing={onSubmit}
|
onSubmitEditing={onSubmit}
|
||||||
|
{...sift.targetProps}
|
||||||
/>
|
/>
|
||||||
{query !== '' && isActive && moderationOpts && (
|
{showResults && (
|
||||||
<View
|
<Inner
|
||||||
style={[
|
query={query}
|
||||||
pal.view,
|
sift={sift}
|
||||||
pal.borderDark,
|
onSelect={onSelect}
|
||||||
styles.resultsContainer,
|
onDismiss={() => setActive(false)}
|
||||||
a.overflow_hidden,
|
|
||||||
]}>
|
|
||||||
{isFetching && !autocompleteData?.length ? (
|
|
||||||
<View style={{padding: 8}}>
|
|
||||||
<ActivityIndicator />
|
|
||||||
</View>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<SearchLinkCard
|
|
||||||
label={l`Search for "${query}"`}
|
|
||||||
to={`/search?q=${encodeURIComponent(query)}`}
|
|
||||||
style={
|
|
||||||
(autocompleteData?.length ?? 0) > 0
|
|
||||||
? {borderBottomWidth: 1}
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
{autocompleteData?.map(item => (
|
|
||||||
<SearchProfileCard
|
|
||||||
key={item.did}
|
|
||||||
profile={item}
|
|
||||||
moderationOpts={moderationOpts}
|
|
||||||
onPress={onSearchProfileCardPress}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
function Inner({
|
||||||
container: {
|
query,
|
||||||
position: 'relative',
|
sift,
|
||||||
width: '100%',
|
onSelect,
|
||||||
},
|
onDismiss,
|
||||||
resultsContainer: {
|
}: {
|
||||||
marginTop: 10,
|
query: string
|
||||||
flexDirection: 'column',
|
sift: ReturnType<typeof useSift>
|
||||||
width: '100%',
|
onSelect: (item: AutocompleteItem) => void
|
||||||
borderWidth: 1,
|
onDismiss: () => void
|
||||||
borderRadius: 6,
|
}) {
|
||||||
},
|
const {items} = useAutocomplete({
|
||||||
})
|
type: 'profile',
|
||||||
|
query,
|
||||||
|
showSearchFallback: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
return items && items.length ? (
|
||||||
|
<AutocompleteBase
|
||||||
|
sift={sift}
|
||||||
|
data={items}
|
||||||
|
onSelect={onSelect}
|
||||||
|
onDismiss={onDismiss}
|
||||||
|
/>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user