mirror of
https://github.com/tooot-app/app
synced 2025-06-05 22:19:13 +02:00
Fix search debouncing
This commit is contained in:
@ -86,12 +86,7 @@ const TabLocal = React.memo(
|
|||||||
accessibilityLabel={t('common.search.accessibilityLabel')}
|
accessibilityLabel={t('common.search.accessibilityLabel')}
|
||||||
accessibilityHint={t('common.search.accessibilityHint')}
|
accessibilityHint={t('common.search.accessibilityHint')}
|
||||||
content='Search'
|
content='Search'
|
||||||
onPress={() =>
|
onPress={() => navigation.navigate('Tab-Local', { screen: 'Tab-Shared-Search' })}
|
||||||
navigation.navigate('Tab-Local', {
|
|
||||||
screen: 'Tab-Shared-Search',
|
|
||||||
params: { text: undefined }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
|
@ -50,12 +50,7 @@ const TabPublic = React.memo(
|
|||||||
accessibilityLabel={t('common.search.accessibilityLabel')}
|
accessibilityLabel={t('common.search.accessibilityLabel')}
|
||||||
accessibilityHint={t('common.search.accessibilityHint')}
|
accessibilityHint={t('common.search.accessibilityHint')}
|
||||||
content='Search'
|
content='Search'
|
||||||
onPress={() =>
|
onPress={() => navigation.navigate('Tab-Public', { screen: 'Tab-Shared-Search' })}
|
||||||
navigation.navigate('Tab-Public', {
|
|
||||||
screen: 'Tab-Shared-Search',
|
|
||||||
params: { text: undefined }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
|
114
src/screens/Tabs/Shared/Search/Empty.tsx
Normal file
114
src/screens/Tabs/Shared/Search/Empty.tsx
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
import ComponentHashtag from '@components/Hashtag'
|
||||||
|
import ComponentSeparator from '@components/Separator'
|
||||||
|
import CustomText from '@components/Text'
|
||||||
|
import { useTrendsQuery } from '@utils/queryHooks/trends'
|
||||||
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
|
import React from 'react'
|
||||||
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
|
import { StyleSheet, TextInput, View } from 'react-native'
|
||||||
|
import { Circle } from 'react-native-animated-spinkit'
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
isLoading: boolean
|
||||||
|
inputRef: React.RefObject<TextInput>
|
||||||
|
setSearchTerm: React.Dispatch<React.SetStateAction<string>>
|
||||||
|
}
|
||||||
|
|
||||||
|
const SearchEmpty: React.FC<Props> = ({ isLoading, inputRef, setSearchTerm }) => {
|
||||||
|
const { colors } = useTheme()
|
||||||
|
const { t } = useTranslation('screenTabs')
|
||||||
|
|
||||||
|
const trendsTags = useTrendsQuery({ type: 'tags' })
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={{ paddingVertical: StyleConstants.Spacing.Global.PagePadding }}>
|
||||||
|
{isLoading ? (
|
||||||
|
<View style={{ flex: 1, alignItems: 'center' }}>
|
||||||
|
<Circle size={StyleConstants.Font.Size.M * 1.25} color={colors.secondary} />
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<View style={{ paddingHorizontal: StyleConstants.Spacing.Global.PagePadding }}>
|
||||||
|
<CustomText
|
||||||
|
fontStyle='S'
|
||||||
|
style={{
|
||||||
|
marginBottom: StyleConstants.Spacing.L,
|
||||||
|
color: colors.primaryDefault
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Trans
|
||||||
|
i18nKey='screenTabs:shared.search.empty.general'
|
||||||
|
components={{
|
||||||
|
bold: <CustomText fontWeight='Bold' />
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</CustomText>
|
||||||
|
<CustomText
|
||||||
|
style={[styles.emptyAdvanced, { color: colors.primaryDefault }]}
|
||||||
|
fontWeight='Bold'
|
||||||
|
>
|
||||||
|
{t('shared.search.empty.advanced.header')}
|
||||||
|
</CustomText>
|
||||||
|
<CustomText style={[styles.emptyAdvanced, { color: colors.primaryDefault }]}>
|
||||||
|
<CustomText style={{ color: colors.secondary }}>@username@domain</CustomText>
|
||||||
|
{' '}
|
||||||
|
{t('shared.search.empty.advanced.example.account')}
|
||||||
|
</CustomText>
|
||||||
|
<CustomText style={[styles.emptyAdvanced, { color: colors.primaryDefault }]}>
|
||||||
|
<CustomText style={{ color: colors.secondary }}>#example</CustomText>
|
||||||
|
{' '}
|
||||||
|
{t('shared.search.empty.advanced.example.hashtag')}
|
||||||
|
</CustomText>
|
||||||
|
<CustomText style={[styles.emptyAdvanced, { color: colors.primaryDefault }]}>
|
||||||
|
<CustomText style={{ color: colors.secondary }}>URL</CustomText>
|
||||||
|
{' '}
|
||||||
|
{t('shared.search.empty.advanced.example.statusLink')}
|
||||||
|
</CustomText>
|
||||||
|
<CustomText style={[styles.emptyAdvanced, { color: colors.primaryDefault }]}>
|
||||||
|
<CustomText style={{ color: colors.secondary }}>URL</CustomText>
|
||||||
|
{' '}
|
||||||
|
{t('shared.search.empty.advanced.example.accountLink')}
|
||||||
|
</CustomText>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<CustomText
|
||||||
|
style={{
|
||||||
|
color: colors.primaryDefault,
|
||||||
|
marginTop: StyleConstants.Spacing.M,
|
||||||
|
paddingHorizontal: StyleConstants.Spacing.Global.PagePadding
|
||||||
|
}}
|
||||||
|
fontWeight='Bold'
|
||||||
|
>
|
||||||
|
{t('shared.search.empty.trending.tags')}
|
||||||
|
</CustomText>
|
||||||
|
<View>
|
||||||
|
{trendsTags.data?.map((tag, index) => {
|
||||||
|
const hashtag = tag as Mastodon.Tag
|
||||||
|
return (
|
||||||
|
<React.Fragment key={index}>
|
||||||
|
{index !== 0 ? <ComponentSeparator /> : null}
|
||||||
|
<ComponentHashtag
|
||||||
|
hashtag={hashtag}
|
||||||
|
onPress={() => {
|
||||||
|
inputRef.current?.setNativeProps({ text: `#${hashtag.name}` })
|
||||||
|
setSearchTerm(`#${hashtag.name}`)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</React.Fragment>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</View>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
emptyAdvanced: {
|
||||||
|
marginBottom: StyleConstants.Spacing.S
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default SearchEmpty
|
@ -6,31 +6,22 @@ import CustomText from '@components/Text'
|
|||||||
import TimelineDefault from '@components/Timeline/Default'
|
import TimelineDefault from '@components/Timeline/Default'
|
||||||
import { TabSharedStackScreenProps } from '@utils/navigation/navigators'
|
import { TabSharedStackScreenProps } from '@utils/navigation/navigators'
|
||||||
import { useSearchQuery } from '@utils/queryHooks/search'
|
import { useSearchQuery } from '@utils/queryHooks/search'
|
||||||
import { useTrendsQuery } from '@utils/queryHooks/trends'
|
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import { debounce } from 'lodash'
|
import { debounce } from 'lodash'
|
||||||
import React, { useEffect } from 'react'
|
import React, { useEffect, useRef, useState } from 'react'
|
||||||
import { Trans, useTranslation } from 'react-i18next'
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
import {
|
import { KeyboardAvoidingView, Platform, SectionList, TextInput, View } from 'react-native'
|
||||||
KeyboardAvoidingView,
|
import SearchEmpty from './Empty'
|
||||||
Platform,
|
|
||||||
SectionList,
|
|
||||||
StyleSheet,
|
|
||||||
TextInput,
|
|
||||||
View
|
|
||||||
} from 'react-native'
|
|
||||||
import { Circle } from 'react-native-animated-spinkit'
|
|
||||||
|
|
||||||
const TabSharedSearch: React.FC<TabSharedStackScreenProps<'Tab-Shared-Search'>> = ({
|
const TabSharedSearch: React.FC<TabSharedStackScreenProps<'Tab-Shared-Search'>> = ({
|
||||||
navigation,
|
navigation
|
||||||
route: {
|
|
||||||
params: { text }
|
|
||||||
}
|
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation('screenTabs')
|
const { t } = useTranslation('screenTabs')
|
||||||
const { colors, mode } = useTheme()
|
const { colors, mode } = useTheme()
|
||||||
|
|
||||||
|
const inputRef = useRef<TextInput>(null)
|
||||||
|
const [searchTerm, setSearchTerm] = useState<string>('')
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
navigation.setOptions({
|
navigation.setOptions({
|
||||||
...(Platform.OS === 'ios'
|
...(Platform.OS === 'ios'
|
||||||
@ -56,6 +47,7 @@ const TabSharedSearch: React.FC<TabSharedStackScreenProps<'Tab-Shared-Search'>>
|
|||||||
defaultValue={t('shared.search.header.prefix')}
|
defaultValue={t('shared.search.header.prefix')}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
|
ref={inputRef}
|
||||||
accessibilityRole='search'
|
accessibilityRole='search'
|
||||||
keyboardAppearance={mode}
|
keyboardAppearance={mode}
|
||||||
style={{
|
style={{
|
||||||
@ -65,15 +57,18 @@ const TabSharedSearch: React.FC<TabSharedStackScreenProps<'Tab-Shared-Search'>>
|
|||||||
paddingLeft: StyleConstants.Spacing.XS
|
paddingLeft: StyleConstants.Spacing.XS
|
||||||
}}
|
}}
|
||||||
autoFocus
|
autoFocus
|
||||||
value={text}
|
onChangeText={debounce(
|
||||||
onChangeText={debounce((text: string) => navigation.setParams({ text }), 1000, {
|
text => {
|
||||||
trailing: true
|
setSearchTerm(text)
|
||||||
})}
|
refetch()
|
||||||
|
},
|
||||||
|
1000,
|
||||||
|
{ trailing: true }
|
||||||
|
)}
|
||||||
autoCapitalize='none'
|
autoCapitalize='none'
|
||||||
autoCorrect={false}
|
autoCorrect={false}
|
||||||
clearButtonMode='always'
|
clearButtonMode='always'
|
||||||
keyboardType='web-search'
|
keyboardType='web-search'
|
||||||
onSubmitEditing={({ nativeEvent: { text } }) => navigation.setParams({ text })}
|
|
||||||
placeholder={t('shared.search.header.placeholder')}
|
placeholder={t('shared.search.header.placeholder')}
|
||||||
placeholderTextColor={colors.secondary}
|
placeholderTextColor={colors.secondary}
|
||||||
returnKeyType='search'
|
returnKeyType='search'
|
||||||
@ -82,25 +77,23 @@ const TabSharedSearch: React.FC<TabSharedStackScreenProps<'Tab-Shared-Search'>>
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}, [text, mode])
|
}, [mode])
|
||||||
|
|
||||||
const trendsTags = useTrendsQuery({ type: 'tags' })
|
|
||||||
|
|
||||||
const mapKeyToTranslations = {
|
const mapKeyToTranslations = {
|
||||||
accounts: t('shared.search.sections.accounts'),
|
accounts: t('shared.search.sections.accounts'),
|
||||||
hashtags: t('shared.search.sections.hashtags'),
|
hashtags: t('shared.search.sections.hashtags'),
|
||||||
statuses: t('shared.search.sections.statuses')
|
statuses: t('shared.search.sections.statuses')
|
||||||
}
|
}
|
||||||
const { status, data } = useSearchQuery<
|
const { isLoading, data, refetch } = useSearchQuery<
|
||||||
{
|
{
|
||||||
title: string
|
title: string
|
||||||
translation: string
|
translation: string
|
||||||
data: any[]
|
data: any[]
|
||||||
}[]
|
}[]
|
||||||
>({
|
>({
|
||||||
term: text,
|
term: searchTerm,
|
||||||
options: {
|
options: {
|
||||||
enabled: text !== undefined,
|
enabled: !!searchTerm.length,
|
||||||
select: data =>
|
select: data =>
|
||||||
Object.keys(data as Mastodon.Results)
|
Object.keys(data as Mastodon.Results)
|
||||||
.map(key => ({
|
.map(key => ({
|
||||||
@ -110,6 +103,7 @@ const TabSharedSearch: React.FC<TabSharedStackScreenProps<'Tab-Shared-Search'>>
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
data: data[key]
|
data: data[key]
|
||||||
}))
|
}))
|
||||||
|
.filter(d => d.data.length)
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
if (!a.data.length) {
|
if (!a.data.length) {
|
||||||
return 1
|
return 1
|
||||||
@ -122,88 +116,6 @@ const TabSharedSearch: React.FC<TabSharedStackScreenProps<'Tab-Shared-Search'>>
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const listEmpty = () => {
|
|
||||||
return (
|
|
||||||
<View style={{ paddingVertical: StyleConstants.Spacing.Global.PagePadding }}>
|
|
||||||
{status === 'loading' ? (
|
|
||||||
<View style={{ flex: 1, alignItems: 'center' }}>
|
|
||||||
<Circle size={StyleConstants.Font.Size.M * 1.25} color={colors.secondary} />
|
|
||||||
</View>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<View style={{ paddingHorizontal: StyleConstants.Spacing.Global.PagePadding }}>
|
|
||||||
<CustomText
|
|
||||||
fontStyle='S'
|
|
||||||
style={{
|
|
||||||
marginBottom: StyleConstants.Spacing.L,
|
|
||||||
color: colors.primaryDefault
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Trans
|
|
||||||
i18nKey='screenTabs:shared.search.empty.general'
|
|
||||||
components={{
|
|
||||||
bold: <CustomText fontWeight='Bold' />
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</CustomText>
|
|
||||||
<CustomText
|
|
||||||
style={[styles.emptyAdvanced, { color: colors.primaryDefault }]}
|
|
||||||
fontWeight='Bold'
|
|
||||||
>
|
|
||||||
{t('shared.search.empty.advanced.header')}
|
|
||||||
</CustomText>
|
|
||||||
<CustomText style={[styles.emptyAdvanced, { color: colors.primaryDefault }]}>
|
|
||||||
<CustomText style={{ color: colors.secondary }}>@username@domain</CustomText>
|
|
||||||
{' '}
|
|
||||||
{t('shared.search.empty.advanced.example.account')}
|
|
||||||
</CustomText>
|
|
||||||
<CustomText style={[styles.emptyAdvanced, { color: colors.primaryDefault }]}>
|
|
||||||
<CustomText style={{ color: colors.secondary }}>#example</CustomText>
|
|
||||||
{' '}
|
|
||||||
{t('shared.search.empty.advanced.example.hashtag')}
|
|
||||||
</CustomText>
|
|
||||||
<CustomText style={[styles.emptyAdvanced, { color: colors.primaryDefault }]}>
|
|
||||||
<CustomText style={{ color: colors.secondary }}>URL</CustomText>
|
|
||||||
{' '}
|
|
||||||
{t('shared.search.empty.advanced.example.statusLink')}
|
|
||||||
</CustomText>
|
|
||||||
<CustomText style={[styles.emptyAdvanced, { color: colors.primaryDefault }]}>
|
|
||||||
<CustomText style={{ color: colors.secondary }}>URL</CustomText>
|
|
||||||
{' '}
|
|
||||||
{t('shared.search.empty.advanced.example.accountLink')}
|
|
||||||
</CustomText>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<CustomText
|
|
||||||
style={{
|
|
||||||
color: colors.primaryDefault,
|
|
||||||
marginTop: StyleConstants.Spacing.M,
|
|
||||||
paddingHorizontal: StyleConstants.Spacing.Global.PagePadding
|
|
||||||
}}
|
|
||||||
fontWeight='Bold'
|
|
||||||
>
|
|
||||||
{t('shared.search.empty.trending.tags')}
|
|
||||||
</CustomText>
|
|
||||||
<View>
|
|
||||||
{trendsTags.data?.map((tag, index) => {
|
|
||||||
const hashtag = tag as Mastodon.Tag
|
|
||||||
return (
|
|
||||||
<React.Fragment key={index}>
|
|
||||||
{index !== 0 ? <ComponentSeparator /> : null}
|
|
||||||
<ComponentHashtag
|
|
||||||
hashtag={hashtag}
|
|
||||||
onPress={() => navigation.setParams({ text: `#${hashtag.name}` })}
|
|
||||||
/>
|
|
||||||
</React.Fragment>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</View>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<KeyboardAvoidingView
|
<KeyboardAvoidingView
|
||||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||||
@ -211,6 +123,7 @@ const TabSharedSearch: React.FC<TabSharedStackScreenProps<'Tab-Shared-Search'>>
|
|||||||
>
|
>
|
||||||
<SectionList
|
<SectionList
|
||||||
style={{ minHeight: '100%' }}
|
style={{ minHeight: '100%' }}
|
||||||
|
sections={data || []}
|
||||||
renderItem={({ item, section }: { item: any; section: any }) => {
|
renderItem={({ item, section }: { item: any; section: any }) => {
|
||||||
switch (section.title) {
|
switch (section.title) {
|
||||||
case 'accounts':
|
case 'accounts':
|
||||||
@ -224,8 +137,9 @@ const TabSharedSearch: React.FC<TabSharedStackScreenProps<'Tab-Shared-Search'>>
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
stickySectionHeadersEnabled
|
stickySectionHeadersEnabled
|
||||||
sections={data || []}
|
ListEmptyComponent={
|
||||||
ListEmptyComponent={listEmpty()}
|
<SearchEmpty isLoading={isLoading} inputRef={inputRef} setSearchTerm={setSearchTerm} />
|
||||||
|
}
|
||||||
keyboardShouldPersistTaps='always'
|
keyboardShouldPersistTaps='always'
|
||||||
renderSectionHeader={({ section: { translation } }) => (
|
renderSectionHeader={({ section: { translation } }) => (
|
||||||
<View
|
<View
|
||||||
@ -257,7 +171,7 @@ const TabSharedSearch: React.FC<TabSharedStackScreenProps<'Tab-Shared-Search'>>
|
|||||||
<CustomText fontStyle='S' style={{ textAlign: 'center', color: colors.secondary }}>
|
<CustomText fontStyle='S' style={{ textAlign: 'center', color: colors.secondary }}>
|
||||||
<Trans
|
<Trans
|
||||||
i18nKey='screenTabs:shared.search.notFound'
|
i18nKey='screenTabs:shared.search.notFound'
|
||||||
values={{ searchTerm: text, type: translation }}
|
values={{ searchTerm, type: translation }}
|
||||||
components={{
|
components={{
|
||||||
bold: <CustomText fontWeight='Bold' />
|
bold: <CustomText fontWeight='Bold' />
|
||||||
}}
|
}}
|
||||||
@ -274,10 +188,4 @@ const TabSharedSearch: React.FC<TabSharedStackScreenProps<'Tab-Shared-Search'>>
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
emptyAdvanced: {
|
|
||||||
marginBottom: StyleConstants.Spacing.S
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
export default TabSharedSearch
|
export default TabSharedSearch
|
@ -104,7 +104,7 @@ export type TabSharedStackParamList = {
|
|||||||
'Tab-Shared-History': {
|
'Tab-Shared-History': {
|
||||||
id: Mastodon.Status['id']
|
id: Mastodon.Status['id']
|
||||||
}
|
}
|
||||||
'Tab-Shared-Search': { text: string | undefined }
|
'Tab-Shared-Search': undefined
|
||||||
'Tab-Shared-Toot': {
|
'Tab-Shared-Toot': {
|
||||||
toot: Mastodon.Status
|
toot: Mastodon.Status
|
||||||
rootQueryKey?: QueryKeyTimeline
|
rootQueryKey?: QueryKeyTimeline
|
||||||
|
Reference in New Issue
Block a user