diff options
Diffstat (limited to 'src/view/screens')
-rw-r--r-- | src/view/screens/Search.tsx | 1 | ||||
-rw-r--r-- | src/view/screens/Search.web.tsx | 76 | ||||
-rw-r--r-- | src/view/screens/Search/Search.tsx | 639 | ||||
-rw-r--r-- | src/view/screens/Search/index.tsx | 1 | ||||
-rw-r--r-- | src/view/screens/Search/index.web.tsx | 1 | ||||
-rw-r--r-- | src/view/screens/SearchMobile.tsx | 205 |
6 files changed, 641 insertions, 282 deletions
diff --git a/src/view/screens/Search.tsx b/src/view/screens/Search.tsx deleted file mode 100644 index bf9857df4..000000000 --- a/src/view/screens/Search.tsx +++ /dev/null @@ -1 +0,0 @@ -export * from './SearchMobile' diff --git a/src/view/screens/Search.web.tsx b/src/view/screens/Search.web.tsx deleted file mode 100644 index 2d0c0288a..000000000 --- a/src/view/screens/Search.web.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import React from 'react' -import {View, StyleSheet} from 'react-native' -import {SearchUIModel} from 'state/models/ui/search' -import {FoafsModel} from 'state/models/discovery/foafs' -import {SuggestedActorsModel} from 'state/models/discovery/suggested-actors' -import {withAuthRequired} from 'view/com/auth/withAuthRequired' -import {Suggestions} from 'view/com/search/Suggestions' -import {SearchResults} from 'view/com/search/SearchResults' -import {observer} from 'mobx-react-lite' -import { - NativeStackScreenProps, - SearchTabNavigatorParams, -} from 'lib/routes/types' -import {useStores} from 'state/index' -import {CenteredView} from 'view/com/util/Views' -import * as Mobile from './SearchMobile' -import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' - -type Props = NativeStackScreenProps<SearchTabNavigatorParams, 'Search'> -export const SearchScreen = withAuthRequired( - observer(function SearchScreenImpl({navigation, route}: Props) { - const store = useStores() - const params = route.params || {} - const foafs = React.useMemo<FoafsModel>( - () => new FoafsModel(store), - [store], - ) - const suggestedActors = React.useMemo<SuggestedActorsModel>( - () => new SuggestedActorsModel(store), - [store], - ) - const searchUIModel = React.useMemo<SearchUIModel | undefined>( - () => (params.q ? new SearchUIModel(store) : undefined), - [params.q, store], - ) - - React.useEffect(() => { - if (params.q && searchUIModel) { - searchUIModel.fetch(params.q) - } - if (!foafs.hasData) { - foafs.fetch() - } - if (!suggestedActors.hasLoaded) { - suggestedActors.loadMore(true) - } - }, [foafs, suggestedActors, searchUIModel, params.q]) - - const {isDesktop} = useWebMediaQueries() - - if (searchUIModel) { - return ( - <View style={styles.scrollContainer}> - <SearchResults model={searchUIModel} /> - </View> - ) - } - - if (!isDesktop) { - return ( - <CenteredView style={styles.scrollContainer}> - <Mobile.SearchScreen navigation={navigation} route={route} /> - </CenteredView> - ) - } - - return <Suggestions foafs={foafs} suggestedActors={suggestedActors} /> - }), -) - -const styles = StyleSheet.create({ - scrollContainer: { - height: '100%', - overflowY: 'auto', - }, -}) diff --git a/src/view/screens/Search/Search.tsx b/src/view/screens/Search/Search.tsx new file mode 100644 index 000000000..08f4fbd70 --- /dev/null +++ b/src/view/screens/Search/Search.tsx @@ -0,0 +1,639 @@ +import React from 'react' +import { + View, + StyleSheet, + ActivityIndicator, + RefreshControl, + TextInput, + Pressable, +} from 'react-native' +import {FlatList, ScrollView, CenteredView} from '#/view/com/util/Views' +import {AppBskyActorDefs, AppBskyFeedDefs, moderateProfile} from '@atproto/api' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import { + FontAwesomeIcon, + FontAwesomeIconStyle, +} from '@fortawesome/react-native-fontawesome' +import {useFocusEffect} from '@react-navigation/native' + +import {logger} from '#/logger' +import { + NativeStackScreenProps, + SearchTabNavigatorParams, +} from 'lib/routes/types' +import {Text} from '#/view/com/util/text/Text' +import {NotificationFeedLoadingPlaceholder} from 'view/com/util/LoadingPlaceholder' +import {ProfileCardWithFollowBtn} from '#/view/com/profile/ProfileCard' +import {Post} from '#/view/com/post/Post' +import {Pager} from '#/view/com/pager/Pager' +import {TabBar} from '#/view/com/pager/TabBar' +import {HITSLOP_10} from '#/lib/constants' +import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' +import {usePalette} from '#/lib/hooks/usePalette' +import {useTheme} from 'lib/ThemeContext' +import {useSession} from '#/state/session' +import {useMyFollowsQuery} from '#/state/queries/my-follows' +import {useGetSuggestedFollowersByActor} from '#/state/queries/suggested-follows' +import {useSearchPostsQuery} from '#/state/queries/search-posts' +import {useActorAutocompleteFn} from '#/state/queries/actor-autocomplete' +import {useSetDrawerOpen} from '#/state/shell' +import {useAnalytics} from '#/lib/analytics/analytics' +import {MagnifyingGlassIcon} from '#/lib/icons' +import {useModerationOpts} from '#/state/queries/preferences' +import {SearchResultCard} from '#/view/shell/desktop/Search' +import {useSetMinimalShellMode, useSetDrawerSwipeDisabled} from '#/state/shell' +import {useStores} from '#/state' +import {isWeb} from '#/platform/detection' + +function Loader() { + const pal = usePalette('default') + const {isMobile} = useWebMediaQueries() + return ( + <CenteredView + style={[ + // @ts-ignore web only -prf + { + padding: 18, + height: isWeb ? '100vh' : undefined, + }, + pal.border, + ]} + sideBorders={!isMobile}> + <ActivityIndicator /> + </CenteredView> + ) +} + +// TODO refactor how to translate? +function EmptyState({message, error}: {message: string; error?: string}) { + const pal = usePalette('default') + const {isMobile} = useWebMediaQueries() + + return ( + <CenteredView + sideBorders={!isMobile} + style={[ + pal.border, + // @ts-ignore web only -prf + { + padding: 18, + height: isWeb ? '100vh' : undefined, + }, + ]}> + <View style={[pal.viewLight, {padding: 18, borderRadius: 8}]}> + <Text style={[pal.text]}> + <Trans>{message}</Trans> + </Text> + + {error && ( + <> + <View + style={[ + { + marginVertical: 12, + height: 1, + width: '100%', + backgroundColor: pal.text.color, + opacity: 0.2, + }, + ]} + /> + + <Text style={[pal.textLight]}> + <Trans>Error:</Trans> {error} + </Text> + </> + )} + </View> + </CenteredView> + ) +} + +function SearchScreenSuggestedFollows() { + const pal = usePalette('default') + const {currentAccount} = useSession() + const [dataUpdatedAt, setDataUpdatedAt] = React.useState(0) + const [suggestions, setSuggestions] = React.useState< + AppBskyActorDefs.ProfileViewBasic[] + >([]) + const getSuggestedFollowsByActor = useGetSuggestedFollowersByActor() + + React.useEffect(() => { + async function getSuggestions() { + // TODO not quite right, doesn't fetch your follows + const friends = await getSuggestedFollowsByActor( + currentAccount!.did, + ).then(friendsRes => friendsRes.suggestions) + + if (!friends) return // :( + + const friendsOfFriends = ( + await Promise.all( + friends + .slice(0, 4) + .map(friend => + getSuggestedFollowsByActor(friend.did).then( + foafsRes => foafsRes.suggestions, + ), + ), + ) + ).flat() + + setSuggestions( + // dedupe + friendsOfFriends.filter(f => !friends.find(f2 => f.did === f2.did)), + ) + setDataUpdatedAt(Date.now()) + } + + try { + getSuggestions() + } catch (e) { + logger.error(`SearchScreenSuggestedFollows: failed to get suggestions`, { + error: e, + }) + } + }, [ + currentAccount, + setSuggestions, + setDataUpdatedAt, + getSuggestedFollowsByActor, + ]) + + return suggestions.length ? ( + <FlatList + data={suggestions} + renderItem={({item}) => ( + <ProfileCardWithFollowBtn + profile={item} + noBg + dataUpdatedAt={dataUpdatedAt} + /> + )} + keyExtractor={item => item.did} + // @ts-ignore web only -prf + desktopFixedHeight + contentContainerStyle={{paddingBottom: 1200}} + /> + ) : ( + <CenteredView + style={[pal.border, {borderLeftWidth: 1, borderRightWidth: 1}]}> + <NotificationFeedLoadingPlaceholder /> + </CenteredView> + ) +} + +type SearchResultSlice = + | { + type: 'post' + key: string + post: AppBskyFeedDefs.PostView + } + | { + type: 'loadingMore' + key: string + } + +function SearchScreenPostResults({query}: {query: string}) { + const pal = usePalette('default') + const [isPTR, setIsPTR] = React.useState(false) + const { + isFetched, + data: results, + isFetching, + error, + refetch, + fetchNextPage, + isFetchingNextPage, + hasNextPage, + dataUpdatedAt, + } = useSearchPostsQuery({query}) + + const onPullToRefresh = React.useCallback(async () => { + setIsPTR(true) + await refetch() + setIsPTR(false) + }, [setIsPTR, refetch]) + const onEndReached = React.useCallback(() => { + if (isFetching || !hasNextPage || error) return + fetchNextPage() + }, [isFetching, error, hasNextPage, fetchNextPage]) + + const posts = React.useMemo(() => { + return results?.pages.flatMap(page => page.posts) || [] + }, [results]) + const items = React.useMemo(() => { + let items: SearchResultSlice[] = [] + + for (const post of posts) { + items.push({ + type: 'post', + key: post.uri, + post, + }) + } + + if (isFetchingNextPage) { + items.push({ + type: 'loadingMore', + key: 'loadingMore', + }) + } + + return items + }, [posts, isFetchingNextPage]) + + return error ? ( + <EmptyState + message="We're sorry, but your search could not be completed. Please try again in a few minutes." + error={error.toString()} + /> + ) : ( + <> + {isFetched ? ( + <> + {posts.length ? ( + <FlatList + data={items} + renderItem={({item}) => { + if (item.type === 'post') { + return <Post post={item.post} dataUpdatedAt={dataUpdatedAt} /> + } else { + return <Loader /> + } + }} + keyExtractor={item => item.key} + refreshControl={ + <RefreshControl + refreshing={isPTR} + onRefresh={onPullToRefresh} + tintColor={pal.colors.text} + titleColor={pal.colors.text} + /> + } + onEndReached={onEndReached} + // @ts-ignore web only -prf + desktopFixedHeight + contentContainerStyle={{paddingBottom: 100}} + /> + ) : ( + <EmptyState message={`No results found for ${query}`} /> + )} + </> + ) : ( + <Loader /> + )} + </> + ) +} + +function SearchScreenUserResults({query}: {query: string}) { + const [isFetched, setIsFetched] = React.useState(false) + const [dataUpdatedAt, setDataUpdatedAt] = React.useState(0) + const [results, setResults] = React.useState< + AppBskyActorDefs.ProfileViewBasic[] + >([]) + const search = useActorAutocompleteFn() + // fuzzy search relies on followers + const {isFetched: isFollowsFetched} = useMyFollowsQuery() + + React.useEffect(() => { + async function getResults() { + const results = await search({query, limit: 30}) + + if (results) { + setDataUpdatedAt(Date.now()) + setResults(results) + setIsFetched(true) + } + } + + if (query && isFollowsFetched) { + getResults() + } else { + setResults([]) + setIsFetched(false) + } + }, [query, isFollowsFetched, setDataUpdatedAt, search]) + + return isFetched ? ( + <> + {results.length ? ( + <FlatList + data={results} + renderItem={({item}) => ( + <ProfileCardWithFollowBtn + profile={item} + noBg + dataUpdatedAt={dataUpdatedAt} + /> + )} + keyExtractor={item => item.did} + // @ts-ignore web only -prf + desktopFixedHeight + contentContainerStyle={{paddingBottom: 100}} + /> + ) : ( + <EmptyState message={`No results found for ${query}`} /> + )} + </> + ) : ( + <Loader /> + ) +} + +const SECTIONS = ['Posts', 'Users'] +export function SearchScreenInner({query}: {query?: string}) { + const pal = usePalette('default') + const setMinimalShellMode = useSetMinimalShellMode() + const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled() + + const onPageSelected = React.useCallback( + (index: number) => { + setMinimalShellMode(false) + setDrawerSwipeDisabled(index > 0) + }, + [setDrawerSwipeDisabled, setMinimalShellMode], + ) + + return query ? ( + <Pager + tabBarPosition="top" + onPageSelected={onPageSelected} + renderTabBar={props => ( + <CenteredView sideBorders style={pal.border}> + <TabBar items={SECTIONS} {...props} /> + </CenteredView> + )} + initialPage={0}> + <View> + <SearchScreenPostResults query={query} /> + </View> + <View> + <SearchScreenUserResults query={query} /> + </View> + </Pager> + ) : ( + <View> + <CenteredView sideBorders style={pal.border}> + <Text + type="title" + style={[ + pal.text, + pal.border, + { + display: 'flex', + paddingVertical: 12, + paddingHorizontal: 18, + fontWeight: 'bold', + }, + ]}> + <Trans>Suggested Follows</Trans> + </Text> + </CenteredView> + <SearchScreenSuggestedFollows /> + </View> + ) +} + +export function SearchScreenDesktop( + props: NativeStackScreenProps<SearchTabNavigatorParams, 'Search'>, +) { + const {isDesktop} = useWebMediaQueries() + + return isDesktop ? ( + <SearchScreenInner query={props.route.params?.q} /> + ) : ( + <SearchScreenMobile {...props} /> + ) +} + +export function SearchScreenMobile( + _props: NativeStackScreenProps<SearchTabNavigatorParams, 'Search'>, +) { + const theme = useTheme() + const textInput = React.useRef<TextInput>(null) + const {_} = useLingui() + const pal = usePalette('default') + const {track} = useAnalytics() + const setDrawerOpen = useSetDrawerOpen() + const moderationOpts = useModerationOpts() + const search = useActorAutocompleteFn() + const setMinimalShellMode = useSetMinimalShellMode() + const store = useStores() + const {isTablet} = useWebMediaQueries() + + const searchDebounceTimeout = React.useRef<NodeJS.Timeout | undefined>( + undefined, + ) + const [isFetching, setIsFetching] = React.useState<boolean>(false) + const [query, setQuery] = React.useState<string>('') + const [searchResults, setSearchResults] = React.useState< + AppBskyActorDefs.ProfileViewBasic[] + >([]) + const [inputIsFocused, setInputIsFocused] = React.useState(false) + const [showAutocompleteResults, setShowAutocompleteResults] = + React.useState(false) + + const onPressMenu = React.useCallback(() => { + track('ViewHeader:MenuButtonClicked') + setDrawerOpen(true) + }, [track, setDrawerOpen]) + const onPressCancelSearch = React.useCallback(() => { + textInput.current?.blur() + setQuery('') + setShowAutocompleteResults(false) + if (searchDebounceTimeout.current) + clearTimeout(searchDebounceTimeout.current) + }, [textInput]) + const onPressClearQuery = React.useCallback(() => { + setQuery('') + setShowAutocompleteResults(false) + }, [setQuery]) + const onChangeText = React.useCallback( + async (text: string) => { + setQuery(text) + + if (text.length > 0) { + setIsFetching(true) + setShowAutocompleteResults(true) + + if (searchDebounceTimeout.current) + clearTimeout(searchDebounceTimeout.current) + + searchDebounceTimeout.current = setTimeout(async () => { + const results = await search({query: text, limit: 30}) + + if (results) { + setSearchResults(results) + setIsFetching(false) + } + }, 300) + } else { + if (searchDebounceTimeout.current) + clearTimeout(searchDebounceTimeout.current) + setSearchResults([]) + setIsFetching(false) + setShowAutocompleteResults(false) + } + }, + [setQuery, search, setSearchResults], + ) + const onSubmit = React.useCallback(() => { + setShowAutocompleteResults(false) + }, [setShowAutocompleteResults]) + + const onSoftReset = React.useCallback(() => { + onPressCancelSearch() + }, [onPressCancelSearch]) + + useFocusEffect( + React.useCallback(() => { + const softResetSub = store.onScreenSoftReset(onSoftReset) + + setMinimalShellMode(false) + + return () => { + softResetSub.remove() + } + }, [store, onSoftReset, setMinimalShellMode]), + ) + + return ( + <View style={{flex: 1}}> + <CenteredView style={[styles.header, pal.border]} sideBorders={isTablet}> + <Pressable + testID="viewHeaderBackOrMenuBtn" + onPress={onPressMenu} + hitSlop={HITSLOP_10} + style={styles.headerMenuBtn} + accessibilityRole="button" + accessibilityLabel={_(msg`Menu`)} + accessibilityHint="Access navigation links and settings"> + <FontAwesomeIcon icon="bars" size={18} color={pal.colors.textLight} /> + </Pressable> + + <View + style={[ + {backgroundColor: pal.colors.backgroundLight}, + styles.headerSearchContainer, + ]}> + <MagnifyingGlassIcon + style={[pal.icon, styles.headerSearchIcon]} + size={21} + /> + <TextInput + testID="searchTextInput" + ref={textInput} + placeholder="Search" + placeholderTextColor={pal.colors.textLight} + selectTextOnFocus + returnKeyType="search" + value={query} + style={[pal.text, styles.headerSearchInput]} + keyboardAppearance={theme.colorScheme} + onFocus={() => setInputIsFocused(true)} + onBlur={() => setInputIsFocused(false)} + onChangeText={onChangeText} + onSubmitEditing={onSubmit} + autoFocus={false} + accessibilityRole="search" + accessibilityLabel={_(msg`Search`)} + accessibilityHint="" + autoCorrect={false} + autoCapitalize="none" + /> + {query ? ( + <Pressable + testID="searchTextInputClearBtn" + onPress={onPressClearQuery} + accessibilityRole="button" + accessibilityLabel={_(msg`Clear search query`)} + accessibilityHint=""> + <FontAwesomeIcon + icon="xmark" + size={16} + style={pal.textLight as FontAwesomeIconStyle} + /> + </Pressable> + ) : undefined} + </View> + + {query || inputIsFocused ? ( + <View style={styles.headerCancelBtn}> + <Pressable onPress={onPressCancelSearch} accessibilityRole="button"> + <Text style={[pal.text]}> + <Trans>Cancel</Trans> + </Text> + </Pressable> + </View> + ) : undefined} + </CenteredView> + + {showAutocompleteResults && moderationOpts ? ( + <> + {isFetching ? ( + <Loader /> + ) : ( + <ScrollView style={{height: '100%'}}> + {searchResults.length ? ( + searchResults.map((item, i) => ( + <SearchResultCard + key={item.did} + profile={item} + moderation={moderateProfile(item, moderationOpts)} + style={i === 0 ? {borderTopWidth: 0} : {}} + /> + )) + ) : ( + <EmptyState message={`No results found for ${query}`} /> + )} + + <View style={{height: 200}} /> + </ScrollView> + )} + </> + ) : ( + <SearchScreenInner query={query} /> + )} + </View> + ) +} + +const styles = StyleSheet.create({ + header: { + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: 12, + paddingVertical: 4, + }, + headerMenuBtn: { + width: 30, + height: 30, + borderRadius: 30, + marginRight: 6, + paddingBottom: 2, + alignItems: 'center', + justifyContent: 'center', + }, + headerSearchContainer: { + flex: 1, + flexDirection: 'row', + alignItems: 'center', + borderRadius: 30, + paddingHorizontal: 12, + paddingVertical: 8, + }, + headerSearchIcon: { + marginRight: 6, + alignSelf: 'center', + }, + headerSearchInput: { + flex: 1, + fontSize: 17, + }, + headerCancelBtn: { + paddingLeft: 10, + }, +}) diff --git a/src/view/screens/Search/index.tsx b/src/view/screens/Search/index.tsx new file mode 100644 index 000000000..d1b9684ef --- /dev/null +++ b/src/view/screens/Search/index.tsx @@ -0,0 +1 @@ +export {SearchScreenMobile as SearchScreen} from '#/view/screens/Search/Search' diff --git a/src/view/screens/Search/index.web.tsx b/src/view/screens/Search/index.web.tsx new file mode 100644 index 000000000..8d09f342a --- /dev/null +++ b/src/view/screens/Search/index.web.tsx @@ -0,0 +1 @@ +export {SearchScreenDesktop as SearchScreen} from '#/view/screens/Search/Search' diff --git a/src/view/screens/SearchMobile.tsx b/src/view/screens/SearchMobile.tsx deleted file mode 100644 index 92c255d5b..000000000 --- a/src/view/screens/SearchMobile.tsx +++ /dev/null @@ -1,205 +0,0 @@ -import React, {useCallback} from 'react' -import { - StyleSheet, - TouchableWithoutFeedback, - Keyboard, - View, -} from 'react-native' -import {useFocusEffect} from '@react-navigation/native' -import {withAuthRequired} from 'view/com/auth/withAuthRequired' -import {FlatList, ScrollView} from 'view/com/util/Views' -import { - NativeStackScreenProps, - SearchTabNavigatorParams, -} from 'lib/routes/types' -import {observer} from 'mobx-react-lite' -import {Text} from 'view/com/util/text/Text' -import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED' -import {useStores} from 'state/index' -import {UserAutocompleteModel} from 'state/models/discovery/user-autocomplete' -import {SearchUIModel} from 'state/models/ui/search' -import {FoafsModel} from 'state/models/discovery/foafs' -import {SuggestedActorsModel} from 'state/models/discovery/suggested-actors' -import {HeaderWithInput} from 'view/com/search/HeaderWithInput' -import {Suggestions} from 'view/com/search/Suggestions' -import {SearchResults} from 'view/com/search/SearchResults' -import {s} from 'lib/styles' -import {ProfileCard} from 'view/com/profile/ProfileCard' -import {usePalette} from 'lib/hooks/usePalette' -import {useOnMainScroll} from 'lib/hooks/useOnMainScroll' -import {isAndroid, isIOS} from 'platform/detection' -import {useSetMinimalShellMode, useSetDrawerSwipeDisabled} from '#/state/shell' - -type Props = NativeStackScreenProps<SearchTabNavigatorParams, 'Search'> -export const SearchScreen = withAuthRequired( - observer<Props>(function SearchScreenImpl({}: Props) { - const pal = usePalette('default') - const store = useStores() - const setMinimalShellMode = useSetMinimalShellMode() - const setIsDrawerSwipeDisabled = useSetDrawerSwipeDisabled() - const scrollViewRef = React.useRef<ScrollView>(null) - const flatListRef = React.useRef<FlatList>(null) - const [onMainScroll] = useOnMainScroll() - const [isInputFocused, setIsInputFocused] = React.useState<boolean>(false) - const [query, setQuery] = React.useState<string>('') - const autocompleteView = React.useMemo<UserAutocompleteModel>( - () => new UserAutocompleteModel(store), - [store], - ) - const foafs = React.useMemo<FoafsModel>( - () => new FoafsModel(store), - [store], - ) - const suggestedActors = React.useMemo<SuggestedActorsModel>( - () => new SuggestedActorsModel(store), - [store], - ) - const [searchUIModel, setSearchUIModel] = React.useState< - SearchUIModel | undefined - >() - - const onChangeQuery = React.useCallback( - (text: string) => { - setQuery(text) - if (text.length > 0) { - autocompleteView.setActive(true) - autocompleteView.setPrefix(text) - } else { - autocompleteView.setActive(false) - } - }, - [setQuery, autocompleteView], - ) - - const onPressClearQuery = React.useCallback(() => { - setQuery('') - }, [setQuery]) - - const onPressCancelSearch = React.useCallback(() => { - setQuery('') - autocompleteView.setActive(false) - setSearchUIModel(undefined) - setIsDrawerSwipeDisabled(false) - }, [setQuery, autocompleteView, setIsDrawerSwipeDisabled]) - - const onSubmitQuery = React.useCallback(() => { - if (query.length === 0) { - return - } - - const model = new SearchUIModel(store) - model.fetch(query) - setSearchUIModel(model) - setIsDrawerSwipeDisabled(true) - }, [query, setSearchUIModel, store, setIsDrawerSwipeDisabled]) - - const onSoftReset = React.useCallback(() => { - scrollViewRef.current?.scrollTo({x: 0, y: 0}) - flatListRef.current?.scrollToOffset({offset: 0}) - onPressCancelSearch() - }, [scrollViewRef, flatListRef, onPressCancelSearch]) - - useFocusEffect( - React.useCallback(() => { - const softResetSub = store.onScreenSoftReset(onSoftReset) - const cleanup = () => { - softResetSub.remove() - } - - setMinimalShellMode(false) - autocompleteView.setup() - if (!foafs.hasData) { - foafs.fetch() - } - if (!suggestedActors.hasLoaded) { - suggestedActors.loadMore(true) - } - - return cleanup - }, [ - store, - autocompleteView, - foafs, - suggestedActors, - onSoftReset, - setMinimalShellMode, - ]), - ) - - const onPress = useCallback(() => { - if (isIOS || isAndroid) { - Keyboard.dismiss() - } - }, []) - - const scrollHandler = useAnimatedScrollHandler(onMainScroll) - return ( - <TouchableWithoutFeedback onPress={onPress} accessible={false}> - <View style={[pal.view, styles.container]}> - <HeaderWithInput - isInputFocused={isInputFocused} - query={query} - setIsInputFocused={setIsInputFocused} - onChangeQuery={onChangeQuery} - onPressClearQuery={onPressClearQuery} - onPressCancelSearch={onPressCancelSearch} - onSubmitQuery={onSubmitQuery} - /> - {searchUIModel ? ( - <SearchResults model={searchUIModel} /> - ) : !isInputFocused && !query ? ( - <Suggestions - ref={flatListRef} - foafs={foafs} - suggestedActors={suggestedActors} - /> - ) : ( - <ScrollView - ref={scrollViewRef} - testID="searchScrollView" - style={pal.view} - onScroll={scrollHandler} - scrollEventThrottle={1}> - {query && autocompleteView.suggestions.length ? ( - <> - {autocompleteView.suggestions.map((suggestion, index) => ( - <ProfileCard - key={suggestion.did} - testID={`searchAutoCompleteResult-${suggestion.handle}`} - profile={suggestion} - noBorder={index === 0} - /> - ))} - </> - ) : query && !autocompleteView.suggestions.length ? ( - <View> - <Text style={[pal.textLight, styles.searchPrompt]}> - No results found for {autocompleteView.prefix} - </Text> - </View> - ) : isInputFocused ? ( - <View> - <Text style={[pal.textLight, styles.searchPrompt]}> - Search for users and posts on the network - </Text> - </View> - ) : null} - <View style={s.footerSpacer} /> - </ScrollView> - )} - </View> - </TouchableWithoutFeedback> - ) - }), -) - -const styles = StyleSheet.create({ - container: { - flex: 1, - }, - - searchPrompt: { - textAlign: 'center', - paddingTop: 10, - }, -}) |