about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--package.json2
-rw-r--r--src/state/models/discovery/foafs.ts16
-rw-r--r--src/view/com/discover/SuggestedFollows.tsx68
-rw-r--r--src/view/com/search/Suggestions.tsx290
-rw-r--r--src/view/screens/Search.tsx197
-rw-r--r--src/view/screens/Search.web.tsx59
-rw-r--r--src/view/screens/SearchMobile.tsx87
-rw-r--r--yarn.lock41
8 files changed, 312 insertions, 448 deletions
diff --git a/package.json b/package.json
index 1621b4ab8..120948f04 100644
--- a/package.json
+++ b/package.json
@@ -60,7 +60,7 @@
     "await-lock": "^2.2.2",
     "base64-js": "^1.5.1",
     "email-validator": "^2.0.4",
-    "expo": "~48.0.10",
+    "expo": "~48.0.11",
     "expo-build-properties": "~0.5.1",
     "expo-camera": "~13.2.1",
     "expo-dev-client": "~2.1.1",
diff --git a/src/state/models/discovery/foafs.ts b/src/state/models/discovery/foafs.ts
index 8dac2ec2d..f6e3157b7 100644
--- a/src/state/models/discovery/foafs.ts
+++ b/src/state/models/discovery/foafs.ts
@@ -57,15 +57,19 @@ export class FoafsModel {
       }
 
       // grab 10 of the users followed by the user
-      this.sources = sampleSize(
-        Object.keys(this.rootStore.me.follows.followDidToRecordMap),
-        10,
-      )
+      runInAction(() => {
+        this.sources = sampleSize(
+          Object.keys(this.rootStore.me.follows.followDidToRecordMap),
+          10,
+        )
+      })
       if (this.sources.length === 0) {
         return
       }
-      this.foafs.clear()
-      this.popular.length = 0
+      runInAction(() => {
+        this.foafs.clear()
+        this.popular.length = 0
+      })
 
       // fetch their profiles
       const profiles = await this.rootStore.agent.getProfiles({
diff --git a/src/view/com/discover/SuggestedFollows.tsx b/src/view/com/discover/SuggestedFollows.tsx
deleted file mode 100644
index ae5605c5c..000000000
--- a/src/view/com/discover/SuggestedFollows.tsx
+++ /dev/null
@@ -1,68 +0,0 @@
-import React from 'react'
-import {StyleSheet, View} from 'react-native'
-import {AppBskyActorDefs} from '@atproto/api'
-import {RefWithInfoAndFollowers} from 'state/models/discovery/foafs'
-import {ProfileCardWithFollowBtn} from '../profile/ProfileCard'
-import {Text} from '../util/text/Text'
-import {usePalette} from 'lib/hooks/usePalette'
-
-export const SuggestedFollows = ({
-  title,
-  suggestions,
-}: {
-  title: string
-  suggestions: (
-    | AppBskyActorDefs.ProfileViewBasic
-    | AppBskyActorDefs.ProfileView
-    | RefWithInfoAndFollowers
-  )[]
-}) => {
-  const pal = usePalette('default')
-  return (
-    <View style={[styles.container, pal.view, pal.border]}>
-      <Text type="title" style={[styles.heading, pal.text]}>
-        {title}
-      </Text>
-      {suggestions.map(item => (
-        <View key={item.did} style={[styles.card, pal.view, pal.border]}>
-          <ProfileCardWithFollowBtn
-            key={item.did}
-            did={item.did}
-            handle={item.handle}
-            displayName={item.displayName}
-            avatar={item.avatar}
-            labels={item.labels}
-            noBg
-            noBorder
-            description={
-              item.description
-                ? (item as AppBskyActorDefs.ProfileView).description
-                : ''
-            }
-            followers={
-              item.followers
-                ? (item.followers as AppBskyActorDefs.ProfileView[])
-                : undefined
-            }
-          />
-        </View>
-      ))}
-    </View>
-  )
-}
-
-const styles = StyleSheet.create({
-  container: {
-    borderBottomWidth: 1,
-  },
-
-  heading: {
-    fontWeight: 'bold',
-    paddingHorizontal: 12,
-    paddingBottom: 8,
-  },
-
-  card: {
-    borderTopWidth: 1,
-  },
-})
diff --git a/src/view/com/search/Suggestions.tsx b/src/view/com/search/Suggestions.tsx
index e9999e1d2..c1355bfb5 100644
--- a/src/view/com/search/Suggestions.tsx
+++ b/src/view/com/search/Suggestions.tsx
@@ -1,65 +1,255 @@
-import React from 'react'
-import {StyleSheet, View} from 'react-native'
+import React, {forwardRef, ForwardedRef} from 'react'
+import {RefreshControl, StyleSheet, View} from 'react-native'
 import {observer} from 'mobx-react-lite'
+import {AppBskyActorDefs} from '@atproto/api'
+import {CenteredView, FlatList} from '../util/Views'
 import {FoafsModel} from 'state/models/discovery/foafs'
-import {SuggestedActorsModel} from 'state/models/discovery/suggested-actors'
-import {SuggestedFollows} from 'view/com/discover/SuggestedFollows'
+import {
+  SuggestedActorsModel,
+  SuggestedActor,
+} from 'state/models/discovery/suggested-actors'
+import {Text} from '../util/text/Text'
+import {ProfileCardWithFollowBtn} from '../profile/ProfileCard'
 import {ProfileCardFeedLoadingPlaceholder} from 'view/com/util/LoadingPlaceholder'
 import {sanitizeDisplayName} from 'lib/strings/display-names'
+import {RefWithInfoAndFollowers} from 'state/models/discovery/foafs'
+import {usePalette} from 'lib/hooks/usePalette'
+
+interface Heading {
+  _reactKey: string
+  type: 'heading'
+  title: string
+}
+interface RefWrapper {
+  _reactKey: string
+  type: 'ref'
+  ref: RefWithInfoAndFollowers
+}
+interface SuggestWrapper {
+  _reactKey: string
+  type: 'suggested'
+  suggested: SuggestedActor
+}
+interface ProfileView {
+  _reactKey: string
+  type: 'profile-view'
+  view: AppBskyActorDefs.ProfileViewBasic
+}
+type Item = Heading | RefWrapper | SuggestWrapper | ProfileView
 
 export const Suggestions = observer(
-  ({
-    foafs,
-    suggestedActors,
-  }: {
-    foafs: FoafsModel
-    suggestedActors: SuggestedActorsModel
-  }) => {
-    if (foafs.isLoading || suggestedActors.isLoading) {
-      return <ProfileCardFeedLoadingPlaceholder />
-    }
-    return (
-      <>
-        {foafs.popular.length > 0 && (
-          <View style={styles.suggestions}>
-            <SuggestedFollows
-              title="In your network"
-              suggestions={foafs.popular}
-            />
-          </View>
-        )}
-        {suggestedActors.hasContent && (
-          <View style={styles.suggestions}>
-            <SuggestedFollows
-              title="Suggested follows"
-              suggestions={suggestedActors.suggestions}
-            />
-          </View>
-        )}
-        {foafs.sources.map((source, i) => {
+  forwardRef(
+    (
+      {
+        foafs,
+        suggestedActors,
+      }: {
+        foafs: FoafsModel
+        suggestedActors: SuggestedActorsModel
+      },
+      flatListRef: ForwardedRef<FlatList>,
+    ) => {
+      const pal = usePalette('default')
+      const [refreshing, setRefreshing] = React.useState(false)
+      const data = React.useMemo(() => {
+        let items: Item[] = []
+
+        if (foafs.popular.length > 0) {
+          items = items
+            .concat([
+              {
+                _reactKey: '__popular_heading__',
+                type: 'heading',
+                title: 'In your network',
+              },
+            ])
+            .concat(
+              foafs.popular.map(ref => ({
+                _reactKey: `popular-${ref.did}`,
+                type: 'ref',
+                ref,
+              })),
+            )
+        }
+        if (suggestedActors.hasContent) {
+          items = items
+            .concat([
+              {
+                _reactKey: '__suggested_heading__',
+                type: 'heading',
+                title: 'Suggested follows',
+              },
+            ])
+            .concat(
+              suggestedActors.suggestions.map(suggested => ({
+                _reactKey: `suggested-${suggested.did}`,
+                type: 'suggested',
+                suggested,
+              })),
+            )
+        }
+        for (const source of foafs.sources) {
           const item = foafs.foafs.get(source)
           if (!item || item.follows.length === 0) {
-            return <View key={`sf-${item?.did || i}`} />
+            return
           }
-          return (
-            <View key={`sf-${item.did}`} style={styles.suggestions}>
-              <SuggestedFollows
-                title={`Followed by ${sanitizeDisplayName(
+          items = items
+            .concat([
+              {
+                _reactKey: `__${item.did}_heading__`,
+                type: 'heading',
+                title: `Followed by ${sanitizeDisplayName(
                   item.displayName || item.handle,
-                )}`}
-                suggestions={item.follows.slice(0, 10)}
-              />
-            </View>
-          )
-        })}
-      </>
-    )
-  },
+                )}`,
+              },
+            ])
+            .concat(
+              item.follows.slice(0, 10).map(view => ({
+                _reactKey: `${item.did}-${view.did}`,
+                type: 'profile-view',
+                view,
+              })),
+            )
+        }
+
+        return items
+      }, [
+        foafs.popular,
+        suggestedActors.hasContent,
+        suggestedActors.suggestions,
+        foafs.sources,
+        foafs.foafs,
+      ])
+
+      const onRefresh = React.useCallback(async () => {
+        setRefreshing(true)
+        try {
+          await foafs.fetch()
+        } finally {
+          setRefreshing(false)
+        }
+      }, [foafs, setRefreshing])
+
+      const renderItem = React.useCallback(
+        ({item}: {item: Item}) => {
+          if (item.type === 'heading') {
+            return (
+              <Text type="title" style={[styles.heading, pal.text]}>
+                {item.title}
+              </Text>
+            )
+          }
+          if (item.type === 'ref') {
+            return (
+              <View style={[styles.card, pal.view, pal.border]}>
+                <ProfileCardWithFollowBtn
+                  key={item.ref.did}
+                  did={item.ref.did}
+                  handle={item.ref.handle}
+                  displayName={item.ref.displayName}
+                  avatar={item.ref.avatar}
+                  labels={item.ref.labels}
+                  noBg
+                  noBorder
+                  description={
+                    item.ref.description
+                      ? (item.ref as AppBskyActorDefs.ProfileView).description
+                      : ''
+                  }
+                  followers={
+                    item.ref.followers
+                      ? (item.ref.followers as AppBskyActorDefs.ProfileView[])
+                      : undefined
+                  }
+                />
+              </View>
+            )
+          }
+          if (item.type === 'profile-view') {
+            return (
+              <View style={[styles.card, pal.view, pal.border]}>
+                <ProfileCardWithFollowBtn
+                  key={item.view.did}
+                  did={item.view.did}
+                  handle={item.view.handle}
+                  displayName={item.view.displayName}
+                  avatar={item.view.avatar}
+                  labels={item.view.labels}
+                  noBg
+                  noBorder
+                  description={
+                    item.view.description
+                      ? (item.view as AppBskyActorDefs.ProfileView).description
+                      : ''
+                  }
+                />
+              </View>
+            )
+          }
+          if (item.type === 'suggested') {
+            return (
+              <View style={[styles.card, pal.view, pal.border]}>
+                <ProfileCardWithFollowBtn
+                  key={item.suggested.did}
+                  did={item.suggested.did}
+                  handle={item.suggested.handle}
+                  displayName={item.suggested.displayName}
+                  avatar={item.suggested.avatar}
+                  labels={item.suggested.labels}
+                  noBg
+                  noBorder
+                  description={
+                    item.suggested.description
+                      ? (item.suggested as AppBskyActorDefs.ProfileView)
+                          .description
+                      : ''
+                  }
+                />
+              </View>
+            )
+          }
+          return null
+        },
+        [pal],
+      )
+
+      if (foafs.isLoading || suggestedActors.isLoading) {
+        return (
+          <CenteredView>
+            <ProfileCardFeedLoadingPlaceholder />
+          </CenteredView>
+        )
+      }
+      return (
+        <FlatList
+          ref={flatListRef}
+          data={data}
+          keyExtractor={item => item._reactKey}
+          refreshControl={
+            <RefreshControl
+              refreshing={refreshing}
+              onRefresh={onRefresh}
+              tintColor={pal.colors.text}
+              titleColor={pal.colors.text}
+            />
+          }
+          renderItem={renderItem}
+          initialNumToRender={15}
+        />
+      )
+    },
+  ),
 )
 
 const styles = StyleSheet.create({
-  suggestions: {
-    marginTop: 10,
-    marginBottom: 20,
+  heading: {
+    fontWeight: 'bold',
+    paddingHorizontal: 12,
+    paddingBottom: 8,
+    paddingTop: 16,
+  },
+
+  card: {
+    borderTopWidth: 1,
   },
 })
diff --git a/src/view/screens/Search.tsx b/src/view/screens/Search.tsx
index ed9effd0b..bf9857df4 100644
--- a/src/view/screens/Search.tsx
+++ b/src/view/screens/Search.tsx
@@ -1,196 +1 @@
-import React from 'react'
-import {
-  Keyboard,
-  RefreshControl,
-  StyleSheet,
-  TouchableWithoutFeedback,
-  View,
-} from 'react-native'
-import {useFocusEffect} from '@react-navigation/native'
-import {withAuthRequired} from 'view/com/auth/withAuthRequired'
-import {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 {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'
-
-type Props = NativeStackScreenProps<SearchTabNavigatorParams, 'Search'>
-export const SearchScreen = withAuthRequired(
-  observer<Props>(({}: Props) => {
-    const pal = usePalette('default')
-    const store = useStores()
-    const scrollElRef = React.useRef<ScrollView>(null)
-    const onMainScroll = useOnMainScroll(store)
-    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 [refreshing, setRefreshing] = React.useState(false)
-
-    const onSoftReset = () => {
-      scrollElRef.current?.scrollTo({x: 0, y: 0})
-    }
-
-    useFocusEffect(
-      React.useCallback(() => {
-        const softResetSub = store.onScreenSoftReset(onSoftReset)
-        const cleanup = () => {
-          softResetSub.remove()
-        }
-
-        store.shell.setMinimalShellMode(false)
-        autocompleteView.setup()
-        if (!foafs.hasData) {
-          foafs.fetch()
-        }
-        if (!suggestedActors.hasLoaded) {
-          suggestedActors.loadMore(true)
-        }
-
-        return cleanup
-      }, [store, autocompleteView, foafs, suggestedActors]),
-    )
-
-    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)
-      store.shell.setIsDrawerSwipeDisabled(false)
-    }, [setQuery, autocompleteView, store])
-
-    const onSubmitQuery = React.useCallback(() => {
-      const model = new SearchUIModel(store)
-      model.fetch(query)
-      setSearchUIModel(model)
-      store.shell.setIsDrawerSwipeDisabled(true)
-    }, [query, setSearchUIModel, store])
-
-    const onRefresh = React.useCallback(async () => {
-      setRefreshing(true)
-      try {
-        await foafs.fetch()
-      } finally {
-        setRefreshing(false)
-      }
-    }, [foafs, setRefreshing])
-
-    return (
-      <TouchableWithoutFeedback onPress={Keyboard.dismiss}>
-        <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} />
-          ) : (
-            <ScrollView
-              ref={scrollElRef}
-              testID="searchScrollView"
-              style={pal.view}
-              onScroll={onMainScroll}
-              scrollEventThrottle={100}
-              refreshControl={
-                <RefreshControl
-                  refreshing={refreshing}
-                  onRefresh={onRefresh}
-                  tintColor={pal.colors.text}
-                  titleColor={pal.colors.text}
-                />
-              }>
-              {query && autocompleteView.searchRes.length ? (
-                <>
-                  {autocompleteView.searchRes.map(item => (
-                    <ProfileCard
-                      key={item.did}
-                      testID={`searchAutoCompleteResult-${item.handle}`}
-                      handle={item.handle}
-                      displayName={item.displayName}
-                      labels={item.labels}
-                      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>
-              ) : (
-                <Suggestions foafs={foafs} suggestedActors={suggestedActors} />
-              )}
-              <View style={s.footerSpacer} />
-            </ScrollView>
-          )}
-        </View>
-      </TouchableWithoutFeedback>
-    )
-  }),
-)
-
-const styles = StyleSheet.create({
-  container: {
-    flex: 1,
-  },
-
-  searchPrompt: {
-    textAlign: 'center',
-    paddingTop: 10,
-  },
-})
+export * from './SearchMobile'
diff --git a/src/view/screens/Search.web.tsx b/src/view/screens/Search.web.tsx
index 92df1d920..62f3fb900 100644
--- a/src/view/screens/Search.web.tsx
+++ b/src/view/screens/Search.web.tsx
@@ -1,10 +1,8 @@
 import React from 'react'
-import {StyleSheet, View} 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 {ScrollView} from 'view/com/util/Views'
 import {Suggestions} from 'view/com/search/Suggestions'
 import {SearchResults} from 'view/com/search/SearchResults'
 import {observer} from 'mobx-react-lite'
@@ -13,15 +11,12 @@ import {
   SearchTabNavigatorParams,
 } from 'lib/routes/types'
 import {useStores} from 'state/index'
-import {s} from 'lib/styles'
-import {usePalette} from 'lib/hooks/usePalette'
 import * as Mobile from './SearchMobile'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
 
 type Props = NativeStackScreenProps<SearchTabNavigatorParams, 'Search'>
 export const SearchScreen = withAuthRequired(
   observer(({navigation, route}: Props) => {
-    const pal = usePalette('default')
     const store = useStores()
     const params = route.params || {}
     const foafs = React.useMemo<FoafsModel>(
@@ -59,58 +54,6 @@ export const SearchScreen = withAuthRequired(
       return <Mobile.SearchScreen navigation={navigation} route={route} />
     }
 
-    return (
-      <ScrollView
-        testID="searchScrollView"
-        style={[pal.view, styles.container]}
-        scrollEventThrottle={100}>
-        <Suggestions foafs={foafs} suggestedActors={suggestedActors} />
-        <View style={s.footerSpacer} />
-      </ScrollView>
-    )
+    return <Suggestions foafs={foafs} suggestedActors={suggestedActors} />
   }),
 )
-
-const styles = StyleSheet.create({
-  container: {
-    flex: 1,
-  },
-
-  header: {
-    flexDirection: 'row',
-    alignItems: 'center',
-    paddingHorizontal: 12,
-    paddingTop: 4,
-    marginBottom: 14,
-  },
-  headerMenuBtn: {
-    width: 40,
-    height: 30,
-    marginLeft: 6,
-  },
-  headerSearchContainer: {
-    flex: 1,
-    flexDirection: 'row',
-    alignItems: 'center',
-    borderRadius: 30,
-    paddingHorizontal: 12,
-    paddingVertical: 8,
-  },
-  headerSearchIcon: {
-    marginRight: 6,
-    alignSelf: 'center',
-  },
-  headerSearchInput: {
-    flex: 1,
-    fontSize: 17,
-  },
-  headerCancelBtn: {
-    width: 60,
-    paddingLeft: 10,
-  },
-
-  searchPrompt: {
-    textAlign: 'center',
-    paddingTop: 10,
-  },
-})
diff --git a/src/view/screens/SearchMobile.tsx b/src/view/screens/SearchMobile.tsx
index e1fb3ec0a..679fb07c2 100644
--- a/src/view/screens/SearchMobile.tsx
+++ b/src/view/screens/SearchMobile.tsx
@@ -1,14 +1,13 @@
 import React from 'react'
 import {
   Keyboard,
-  RefreshControl,
   StyleSheet,
   TouchableWithoutFeedback,
   View,
 } from 'react-native'
 import {useFocusEffect} from '@react-navigation/native'
 import {withAuthRequired} from 'view/com/auth/withAuthRequired'
-import {ScrollView} from 'view/com/util/Views'
+import {FlatList, ScrollView} from 'view/com/util/Views'
 import {
   NativeStackScreenProps,
   SearchTabNavigatorParams,
@@ -33,7 +32,8 @@ export const SearchScreen = withAuthRequired(
   observer<Props>(({}: Props) => {
     const pal = usePalette('default')
     const store = useStores()
-    const scrollElRef = React.useRef<ScrollView>(null)
+    const scrollViewRef = React.useRef<ScrollView>(null)
+    const flatListRef = React.useRef<FlatList>(null)
     const onMainScroll = useOnMainScroll(store)
     const [isInputFocused, setIsInputFocused] = React.useState<boolean>(false)
     const [query, setQuery] = React.useState<string>('')
@@ -52,31 +52,6 @@ export const SearchScreen = withAuthRequired(
     const [searchUIModel, setSearchUIModel] = React.useState<
       SearchUIModel | undefined
     >()
-    const [refreshing, setRefreshing] = React.useState(false)
-
-    const onSoftReset = () => {
-      scrollElRef.current?.scrollTo({x: 0, y: 0})
-    }
-
-    useFocusEffect(
-      React.useCallback(() => {
-        const softResetSub = store.onScreenSoftReset(onSoftReset)
-        const cleanup = () => {
-          softResetSub.remove()
-        }
-
-        store.shell.setMinimalShellMode(false)
-        autocompleteView.setup()
-        if (!foafs.hasData) {
-          foafs.fetch()
-        }
-        if (!suggestedActors.hasLoaded) {
-          suggestedActors.loadMore(true)
-        }
-
-        return cleanup
-      }, [store, autocompleteView, foafs, suggestedActors]),
-    )
 
     const onChangeQuery = React.useCallback(
       (text: string) => {
@@ -109,14 +84,31 @@ export const SearchScreen = withAuthRequired(
       store.shell.setIsDrawerSwipeDisabled(true)
     }, [query, setSearchUIModel, store])
 
-    const onRefresh = React.useCallback(async () => {
-      setRefreshing(true)
-      try {
-        await foafs.fetch()
-      } finally {
-        setRefreshing(false)
-      }
-    }, [foafs, setRefreshing])
+    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()
+        }
+
+        store.shell.setMinimalShellMode(false)
+        autocompleteView.setup()
+        if (!foafs.hasData) {
+          foafs.fetch()
+        }
+        if (!suggestedActors.hasLoaded) {
+          suggestedActors.loadMore(true)
+        }
+
+        return cleanup
+      }, [store, autocompleteView, foafs, suggestedActors, onSoftReset]),
+    )
 
     return (
       <TouchableWithoutFeedback onPress={Keyboard.dismiss}>
@@ -132,21 +124,19 @@ export const SearchScreen = withAuthRequired(
           />
           {searchUIModel ? (
             <SearchResults model={searchUIModel} />
+          ) : !isInputFocused && !query ? (
+            <Suggestions
+              ref={flatListRef}
+              foafs={foafs}
+              suggestedActors={suggestedActors}
+            />
           ) : (
             <ScrollView
-              ref={scrollElRef}
+              ref={scrollViewRef}
               testID="searchScrollView"
               style={pal.view}
               onScroll={onMainScroll}
-              scrollEventThrottle={100}
-              refreshControl={
-                <RefreshControl
-                  refreshing={refreshing}
-                  onRefresh={onRefresh}
-                  tintColor={pal.colors.text}
-                  titleColor={pal.colors.text}
-                />
-              }>
+              scrollEventThrottle={100}>
               {query && autocompleteView.searchRes.length ? (
                 <>
                   {autocompleteView.searchRes.map(item => (
@@ -155,6 +145,7 @@ export const SearchScreen = withAuthRequired(
                       testID={`searchAutoCompleteResult-${item.handle}`}
                       handle={item.handle}
                       displayName={item.displayName}
+                      labels={item.labels}
                       avatar={item.avatar}
                     />
                   ))}
@@ -171,9 +162,7 @@ export const SearchScreen = withAuthRequired(
                     Search for users on the network
                   </Text>
                 </View>
-              ) : (
-                <Suggestions foafs={foafs} suggestedActors={suggestedActors} />
-              )}
+              ) : null}
               <View style={s.footerSpacer} />
             </ScrollView>
           )}
diff --git a/yarn.lock b/yarn.lock
index 5a014ff1b..8685a0d4e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1535,16 +1535,16 @@
     mv "~2"
     safe-json-stringify "~1"
 
-"@expo/cli@0.6.2":
-  version "0.6.2"
-  resolved "https://registry.yarnpkg.com/@expo/cli/-/cli-0.6.2.tgz#1090c9d23f49d9603c4c85fa85b878b2848da322"
-  integrity sha512-uhmrXNemXTbCTKP/ycyJHOU/KLGdFwVCrWNBzz1VkwnmL8yJV5F3C18a83ybFFnUNfkGHeH5LtID7CSNbbTWKg==
+"@expo/cli@0.7.0":
+  version "0.7.0"
+  resolved "https://registry.yarnpkg.com/@expo/cli/-/cli-0.7.0.tgz#2a16873ced05c1f3b7f3990d7b410e9853600f45"
+  integrity sha512-9gjr3pRgwWzUDW/P7B4tA0QevKb+hCrvTmVc3Ce5w7CjdM3zNoBcro8vwviRHqkiB1IifG7zQh0PPStSbK+FRQ==
   dependencies:
     "@babel/runtime" "^7.20.0"
     "@expo/code-signing-certificates" "0.0.5"
     "@expo/config" "~8.0.0"
     "@expo/config-plugins" "~6.0.0"
-    "@expo/dev-server" "0.2.3"
+    "@expo/dev-server" "0.3.0"
     "@expo/devcert" "^1.0.0"
     "@expo/json-file" "^8.2.37"
     "@expo/metro-config" "~0.7.0"
@@ -1600,6 +1600,7 @@
     text-table "^0.2.0"
     url-join "4.0.0"
     wrap-ansi "^7.0.0"
+    ws "^8.12.1"
 
 "@expo/code-signing-certificates@0.0.5":
   version "0.0.5"
@@ -1709,10 +1710,10 @@
     xcode "^3.0.0"
     xml-js "^1.6.11"
 
-"@expo/dev-server@0.2.3":
-  version "0.2.3"
-  resolved "https://registry.yarnpkg.com/@expo/dev-server/-/dev-server-0.2.3.tgz#736317cc1340b28dc49da8a45b85040306048e24"
-  integrity sha512-9+6QGRdymj3dmTp1vUpROvWJ+Ezz6Qp9xHafAcaRHzw322pUCOiRKxTYqDqYYZ/72shrHPGQ2CiIXTnV1vM2tA==
+"@expo/dev-server@0.3.0":
+  version "0.3.0"
+  resolved "https://registry.yarnpkg.com/@expo/dev-server/-/dev-server-0.3.0.tgz#c575c88b0ec28f127f328a80ea6a3a4c6f785800"
+  integrity sha512-2A6/8uZADSKAtzyR6YqhCBUFxb5DFmjxmFn0EHMqnPnsh13ZSiKEjrZPrRkM6Li2EHLYqHK2rmweJ7O/7q9pPQ==
   dependencies:
     "@expo/bunyan" "4.0.0"
     "@expo/metro-config" "~0.7.0"
@@ -8390,10 +8391,10 @@ expo-media-library@~15.2.3:
   resolved "https://registry.yarnpkg.com/expo-media-library/-/expo-media-library-15.2.3.tgz#188f3c77f58b354f0ea6250f6756ac1e1a226291"
   integrity sha512-Oz8b8Xsvfj7YcutUBtI84NUIqSnt7iCM5HZ5DyKoWKKiDK/+aUuj3RXNQELG8jUw6pQPgEwgbZ1+J8SdH/y9jw==
 
-expo-modules-autolinking@1.1.2:
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/expo-modules-autolinking/-/expo-modules-autolinking-1.1.2.tgz#a81c65c63bd281922410c6d8c3ad6255b6305246"
-  integrity sha512-oOlkAccVnHwwR5ccvF/F/x4Omj9HWzSimMUlIVz0SVGdNBEqTPyn0L/d4uIufhyQbEWvrarqL8o5Yz11wEI0SQ==
+expo-modules-autolinking@1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/expo-modules-autolinking/-/expo-modules-autolinking-1.2.0.tgz#3ead115510a43fe196fc0498586b6133bd573209"
+  integrity sha512-QOPh/iXykNDCAzUual1imSrn2aDakzCGUp2QmxVREr0llajXygroUWlT9sQXh1zKzbNp+a+i/xK375ZeBFiNJA==
   dependencies:
     chalk "^4.1.0"
     commander "^7.2.0"
@@ -8467,13 +8468,13 @@ expo-updates@~0.16.4:
     fbemitter "^3.0.0"
     resolve-from "^5.0.0"
 
-expo@~48.0.10:
-  version "48.0.10"
-  resolved "https://registry.yarnpkg.com/expo/-/expo-48.0.10.tgz#c1218f6a0ca9ca8209d6f833dc6911743870dfaf"
-  integrity sha512-8YXG6um3ld36nu/ONEC0NNkMatdj4k/HwR7OUd3dKUt3PJSkZHsCeLXIu8za7WSWpgPAU/xAj35noPFEFnjO1w==
+expo@~48.0.11:
+  version "48.0.11"
+  resolved "https://registry.yarnpkg.com/expo/-/expo-48.0.11.tgz#afd43c7a5ddce3d02a3f27263c95f8d01e1fb84d"
+  integrity sha512-KX1RCHhdhdT4DjCeRqYJpZXhdCTuqxHHdNIRoFkmCgkUARYlZbB+Y1U8/KMz8fBAlFoEq99cF/KyRr87VAxRCw==
   dependencies:
     "@babel/runtime" "^7.20.0"
-    "@expo/cli" "0.6.2"
+    "@expo/cli" "0.7.0"
     "@expo/config" "8.0.2"
     "@expo/config-plugins" "6.0.1"
     "@expo/vector-icons" "^13.0.0"
@@ -8485,7 +8486,7 @@ expo@~48.0.10:
     expo-file-system "~15.2.2"
     expo-font "~11.1.1"
     expo-keep-awake "~12.0.1"
-    expo-modules-autolinking "1.1.2"
+    expo-modules-autolinking "1.2.0"
     expo-modules-core "1.2.6"
     fbemitter "^3.0.0"
     getenv "^1.0.0"
@@ -17884,7 +17885,7 @@ ws@^7, ws@^7.0.0, ws@^7.4.6, ws@^7.5.1:
   resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591"
   integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==
 
-ws@^8.11.0, ws@^8.13.0:
+ws@^8.11.0, ws@^8.12.1, ws@^8.13.0:
   version "8.13.0"
   resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0"
   integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==