import React from 'react' import { View, StyleSheet, ActivityIndicator, TextInput, Pressable, Platform, } from 'react-native' import {ScrollView, CenteredView} from '#/view/com/util/Views' import {List} from '#/view/com/util/List' 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 {ProfileCardFeedLoadingPlaceholder} 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 {useGetSuggestedFollowersByActor} from '#/state/queries/suggested-follows' import {useSearchPostsQuery} from '#/state/queries/search-posts' import {useActorSearch} from '#/state/queries/actor-search' 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 {isWeb} from '#/platform/detection' import {listenSoftReset} from '#/state/events' import {s} from '#/lib/styles' function Loader() { const pal = usePalette('default') const {isMobile} = useWebMediaQueries() return ( ) } function EmptyState({message, error}: {message: string; error?: string}) { const pal = usePalette('default') const {isMobile} = useWebMediaQueries() return ( {message} {error && ( <> Error: {error} )} ) } function SearchScreenSuggestedFollows() { const pal = usePalette('default') const {currentAccount} = useSession() const [suggestions, setSuggestions] = React.useState< AppBskyActorDefs.ProfileViewBasic[] >([]) const getSuggestedFollowsByActor = useGetSuggestedFollowersByActor() React.useEffect(() => { async function getSuggestions() { const friends = await getSuggestedFollowsByActor( currentAccount!.did, ).then(friendsRes => friendsRes.suggestions) if (!friends) return // :( const friendsOfFriends = new Map< string, AppBskyActorDefs.ProfileViewBasic >() await Promise.all( friends.slice(0, 4).map(friend => getSuggestedFollowsByActor(friend.did).then(foafsRes => { for (const user of foafsRes.suggestions) { friendsOfFriends.set(user.did, user) } }), ), ) setSuggestions(Array.from(friendsOfFriends.values())) } try { getSuggestions() } catch (e) { logger.error(`SearchScreenSuggestedFollows: failed to get suggestions`, { error: e, }) } }, [currentAccount, setSuggestions, getSuggestedFollowsByActor]) return suggestions.length ? ( } keyExtractor={item => item.did} // @ts-ignore web only -prf desktopFixedHeight contentContainerStyle={{paddingBottom: 1200}} keyboardShouldPersistTaps="handled" keyboardDismissMode="on-drag" /> ) : ( ) } type SearchResultSlice = | { type: 'post' key: string post: AppBskyFeedDefs.PostView } | { type: 'loadingMore' key: string } function SearchScreenPostResults({query}: {query: string}) { const {_} = useLingui() const [isPTR, setIsPTR] = React.useState(false) const { isFetched, data: results, isFetching, error, refetch, fetchNextPage, isFetchingNextPage, hasNextPage, } = 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 temp: SearchResultSlice[] = [] const seenUris = new Set() for (const post of posts) { if (seenUris.has(post.uri)) { continue } temp.push({ type: 'post', key: post.uri, post, }) seenUris.add(post.uri) } if (isFetchingNextPage) { temp.push({ type: 'loadingMore', key: 'loadingMore', }) } return temp }, [posts, isFetchingNextPage]) return error ? ( ) : ( <> {isFetched ? ( <> {posts.length ? ( { if (item.type === 'post') { return } else { return } }} keyExtractor={item => item.key} refreshing={isPTR} onRefresh={onPullToRefresh} onEndReached={onEndReached} // @ts-ignore web only -prf desktopFixedHeight contentContainerStyle={{paddingBottom: 100}} /> ) : ( )} ) : ( )} ) } function SearchScreenUserResults({query}: {query: string}) { const {_} = useLingui() const {data: results, isFetched} = useActorSearch(query) return isFetched && results ? ( <> {results.length ? ( ( )} keyExtractor={item => item.did} // @ts-ignore web only -prf desktopFixedHeight contentContainerStyle={{paddingBottom: 100}} /> ) : ( )} ) : ( ) } const SECTIONS_LOGGEDOUT = ['Users'] const SECTIONS_LOGGEDIN = ['Posts', 'Users'] export function SearchScreenInner({ query, primarySearch, }: { query?: string primarySearch?: boolean }) { const pal = usePalette('default') const setMinimalShellMode = useSetMinimalShellMode() const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled() const {hasSession} = useSession() const {isDesktop} = useWebMediaQueries() const onPageSelected = React.useCallback( (index: number) => { setMinimalShellMode(false) setDrawerSwipeDisabled(index > 0) }, [setDrawerSwipeDisabled, setMinimalShellMode], ) if (hasSession) { return query ? ( ( )} initialPage={0}> ) : ( Suggested Follows ) } return query ? ( ( )} initialPage={0}> ) : ( {isDesktop && ( Search )} {isDesktop && !primarySearch ? ( Find users with the search tool on the right ) : ( Find users on Bluesky )} ) } export function SearchScreen( props: NativeStackScreenProps, ) { const theme = useTheme() const textInput = React.useRef(null) const {_} = useLingui() const pal = usePalette('default') const {track} = useAnalytics() const setDrawerOpen = useSetDrawerOpen() const moderationOpts = useModerationOpts() const search = useActorAutocompleteFn() const setMinimalShellMode = useSetMinimalShellMode() const {isTabletOrDesktop, isTabletOrMobile} = useWebMediaQueries() const searchDebounceTimeout = React.useRef( undefined, ) const [isFetching, setIsFetching] = React.useState(false) const [query, setQuery] = React.useState(props.route?.params?.q || '') 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(() => { setMinimalShellMode(false) return listenSoftReset(onSoftReset) }, [onSoftReset, setMinimalShellMode]), ) return ( {isTabletOrMobile && ( )} setInputIsFocused(true)} onBlur={() => setInputIsFocused(false)} onChangeText={onChangeText} onSubmitEditing={onSubmit} autoFocus={false} accessibilityRole="search" accessibilityLabel={_(msg`Search`)} accessibilityHint="" autoCorrect={false} autoCapitalize="none" /> {query ? ( ) : undefined} {query || inputIsFocused ? ( Cancel ) : undefined} {showAutocompleteResults && moderationOpts ? ( <> {isFetching ? ( ) : ( {searchResults.length ? ( searchResults.map((item, i) => ( )) ) : ( )} )} ) : ( )} ) } 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, }, })