diff options
Diffstat (limited to 'src/view/screens/Search.tsx')
-rw-r--r-- | src/view/screens/Search.tsx | 232 |
1 files changed, 158 insertions, 74 deletions
diff --git a/src/view/screens/Search.tsx b/src/view/screens/Search.tsx index 2a1caab89..2e176d98f 100644 --- a/src/view/screens/Search.tsx +++ b/src/view/screens/Search.tsx @@ -1,41 +1,73 @@ -import React, {useEffect, useState, useMemo, useRef} from 'react' +import React from 'react' import { Keyboard, StyleSheet, TextInput, TouchableOpacity, + TouchableWithoutFeedback, View, } from 'react-native' -import {ViewHeader} from '../com/util/ViewHeader' -import {CenteredView, ScrollView} from '../com/util/Views' -import {SuggestedFollows} from '../com/discover/SuggestedFollows' +import {ScrollView} from '../com/util/Views' +import {observer} from 'mobx-react-lite' import {UserAvatar} from '../com/util/UserAvatar' import {Text} from '../com/util/text/Text' import {ScreenParams} from '../routes' -import {useStores} from '../../state' -import {UserAutocompleteViewModel} from '../../state/models/user-autocomplete-view' -import {s} from '../lib/styles' -import {MagnifyingGlassIcon} from '../lib/icons' -import {usePalette} from '../lib/hooks/usePalette' +import {useStores} from 'state/index' +import {UserAutocompleteViewModel} from 'state/models/user-autocomplete-view' +import {s} from 'lib/styles' +import {MagnifyingGlassIcon} from 'lib/icons' +import {WhoToFollow} from '../com/discover/WhoToFollow' +import {SuggestedPosts} from '../com/discover/SuggestedPosts' +import {ProfileCard} from '../com/profile/ProfileCard' +import {usePalette} from 'lib/hooks/usePalette' +import {useOnMainScroll} from 'lib/hooks/useOnMainScroll' +import {useAnalytics} from 'lib/analytics' -export const Search = ({navIdx, visible, params}: ScreenParams) => { +const MENU_HITSLOP = {left: 10, top: 10, right: 30, bottom: 10} +const FIVE_MIN = 5 * 60 * 1e3 + +export const Search = observer(({navIdx, visible, params}: ScreenParams) => { const pal = usePalette('default') const store = useStores() - const textInput = useRef<TextInput>(null) - const [query, setQuery] = useState<string>('') - const autocompleteView = useMemo<UserAutocompleteViewModel>( + const {track} = useAnalytics() + const scrollElRef = React.useRef<ScrollView>(null) + const onMainScroll = useOnMainScroll(store) + const textInput = React.useRef<TextInput>(null) + const [lastRenderTime, setRenderTime] = React.useState<number>(Date.now()) // used to trigger reloads + const [isInputFocused, setIsInputFocused] = React.useState<boolean>(false) + const [query, setQuery] = React.useState<string>('') + const autocompleteView = React.useMemo<UserAutocompleteViewModel>( () => new UserAutocompleteViewModel(store), [store], ) const {name} = params - useEffect(() => { + const onSoftReset = () => { + scrollElRef.current?.scrollTo({x: 0, y: 0}) + } + + React.useEffect(() => { + const softResetSub = store.onScreenSoftReset(onSoftReset) + const cleanup = () => { + softResetSub.remove() + } + if (visible) { + const now = Date.now() + if (now - lastRenderTime > FIVE_MIN) { + setRenderTime(Date.now()) // trigger reload of suggestions + } store.shell.setMinimalShellMode(false) autocompleteView.setup() store.nav.setTitle(navIdx, 'Search') } - }, [store, visible, name, navIdx, autocompleteView]) + return cleanup + }, [store, visible, name, navIdx, autocompleteView, lastRenderTime]) + + const onPressMenu = () => { + track('ViewHeader:MenuButtonClicked') + store.shell.setMainMenuOpen(true) + } const onChangeQuery = (text: string) => { setQuery(text) @@ -46,87 +78,139 @@ export const Search = ({navIdx, visible, params}: ScreenParams) => { autocompleteView.setActive(false) } } - const onSelect = (handle: string) => { - textInput.current?.blur() - store.nav.navigate(`/profile/${handle}`) + const onPressCancelSearch = () => { + setQuery('') + autocompleteView.setActive(false) } return ( - <View style={[pal.view, styles.container]}> - <ViewHeader title="Search" /> - <CenteredView style={[pal.view, pal.border, styles.inputContainer]}> - <MagnifyingGlassIcon style={[pal.text, styles.inputIcon]} /> - <TextInput - testID="searchTextInput" - ref={textInput} - placeholder="Type your query here..." - placeholderTextColor={pal.colors.textLight} - selectTextOnFocus - returnKeyType="search" - style={[pal.text, styles.input]} - onChangeText={onChangeQuery} - /> - </CenteredView> - <View style={styles.outputContainer}> - {query ? ( - <ScrollView testID="searchScrollView" onScroll={Keyboard.dismiss}> - {autocompleteView.searchRes.map((item, i) => ( - <TouchableOpacity - key={i} - style={[pal.view, pal.border, styles.searchResult]} - onPress={() => onSelect(item.handle)}> - <UserAvatar - handle={item.handle} - displayName={item.displayName} - avatar={item.avatar} - size={36} - /> - <View style={[s.ml10]}> - <Text type="title-sm" style={pal.text}> - {item.displayName || item.handle} - </Text> - <Text style={pal.textLight}>@{item.handle}</Text> - </View> + <TouchableWithoutFeedback onPress={Keyboard.dismiss}> + <ScrollView + ref={scrollElRef} + testID="searchScrollView" + style={[pal.view, styles.container]} + onScroll={onMainScroll} + scrollEventThrottle={100}> + <View style={[pal.view, pal.border, styles.header]}> + <TouchableOpacity + testID="viewHeaderBackOrMenuBtn" + onPress={onPressMenu} + hitSlop={MENU_HITSLOP} + style={styles.headerMenuBtn}> + <UserAvatar + size={30} + handle={store.me.handle} + displayName={store.me.displayName} + avatar={store.me.avatar} + /> + </TouchableOpacity> + <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]} + onFocus={() => setIsInputFocused(true)} + onBlur={() => setIsInputFocused(false)} + onChangeText={onChangeQuery} + /> + </View> + {query ? ( + <View style={styles.headerCancelBtn}> + <TouchableOpacity onPress={onPressCancelSearch}> + <Text>Cancel</Text> </TouchableOpacity> + </View> + ) : undefined} + </View> + {query && autocompleteView.searchRes.length ? ( + <> + {autocompleteView.searchRes.map(item => ( + <ProfileCard + key={item.did} + handle={item.handle} + displayName={item.displayName} + avatar={item.avatar} + /> ))} + </> + ) : query && !autocompleteView.searchRes.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 on the network + </Text> + </View> + ) : ( + <ScrollView onScroll={Keyboard.dismiss}> + <WhoToFollow key={`wtf-${lastRenderTime}`} /> + <SuggestedPosts key={`sp-${lastRenderTime}`} /> <View style={s.footerSpacer} /> </ScrollView> - ) : ( - <SuggestedFollows asLinks /> )} - </View> - </View> + <View style={s.footerSpacer} /> + </ScrollView> + </TouchableWithoutFeedback> ) -} +}) const styles = StyleSheet.create({ container: { flex: 1, }, - inputContainer: { + header: { flexDirection: 'row', - paddingVertical: 16, - paddingHorizontal: 16, - borderTopWidth: 1, + alignItems: 'center', + paddingHorizontal: 12, + paddingTop: 4, + marginBottom: 14, }, - inputIcon: { - marginRight: 10, - alignSelf: 'center', + headerMenuBtn: { + width: 40, + height: 30, + marginLeft: 6, }, - input: { + headerSearchContainer: { flex: 1, - fontSize: 16, + flexDirection: 'row', + alignItems: 'center', + borderRadius: 30, + paddingHorizontal: 12, + paddingVertical: 8, }, - - outputContainer: { + headerSearchIcon: { + marginRight: 6, + alignSelf: 'center', + }, + headerSearchInput: { flex: 1, + fontSize: 17, + }, + headerCancelBtn: { + width: 60, + paddingLeft: 10, }, - searchResult: { - flexDirection: 'row', - borderTopWidth: 1, - paddingVertical: 12, - paddingHorizontal: 16, + searchPrompt: { + textAlign: 'center', + paddingTop: 10, }, }) |