about summary refs log tree commit diff
path: root/src/view/screens
diff options
context:
space:
mode:
authorSamuel Newman <mozzius@protonmail.com>2025-04-03 03:21:15 +0300
committerGitHub <noreply@github.com>2025-04-02 17:21:15 -0700
commit87da619aaa92e0ec762e68c13b24e58a25da10a8 (patch)
tree4da902d3ca43a226f6da8e5c090ab33c2df3297a /src/view/screens
parent8d1f97b5ffac5d86762f1d4e9384ff3097acbc52 (diff)
downloadvoidsky-87da619aaa92e0ec762e68c13b24e58a25da10a8.tar.zst
[Explore] Base (#8053)
* migrate to #/screens

* rm unneeded import

* block drawer gesture on recent profiles

* rm recommendations (#8056)

* [Explore] Disable Trending videos (#8054)

* remove giant header

* disable

* [Explore] Dynamic module ordering (#8066)

* Dynamic module ordering

* [Explore] New headers, metrics (#8067)

* new sticky headers

* improve spacing between modules

* view metric on modules

* update metrics names

* [Explore] Suggested accounts module (#8072)

* use modern profile card, update load more

* add tab bar

* tabbed suggested accounts

* [Explore] Discover feeds module (#8073)

* cap number of feeds to 3

* change feed pin button

* Apply suggestions from code review

Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>

* restore statsig to log events

* filter out followed profiles, make suer enough are loaded (#8090)

* [Explore] Trending topics (#8055)

* redesigned trending topics

* rm borders on web

* get post count / age / ranking from api

* spacing tweaks

* fetch more topics then slice

* use api data for avis/category

* rm top border

* Integrate new SDK, part out components

* Clean up

* Use status field

* Bump SDK

* Send up interests and langs

---------

Co-authored-by: Eric Bailey <git@esb.lol>

* Clean up module spacing and borders

(cherry picked from commit 63d19b6c2d67e226e0e14709b1047a1f88b3ce1c)
(cherry picked from commit 62d7d394ab1dc31b40b9c2cf59075adbf94737a1)

* Switch back border ordering

(cherry picked from commit 34e3789f8b410132c1390df3c2bb8257630ebdd9)

* [Explore] Starter Packs (#8095)

* Temp WIP

(cherry picked from commit 43b5d7b1e64b3adb1ed162262d0310e0bf026c18)

* New SP card

* Load state

* Revert change

* Cleanup

* Interests and caching

* Count total

* Format

* Caching

* [Explore] Feed previews module (#8075)

* wip new hook

* get fetching working, maybe

* get feed previews rendering!

* fix header height

* working pin button

* extract out FeedLink

* add loader

* only make preview:header sticky

* Fix headers

* Header tweaks

* Fix moderation filter

* Fix threading

---------

Co-authored-by: Eric Bailey <git@esb.lol>

* Space it out

* Fix query key

* Mock new endpoint, filter saved feeds

* Make sure we're pinning, lower cache time

* add news category

* Remove log

* Improve suggested accounts load state

* Integrate new app view endpoint

* fragment

* Update src/screens/Search/modules/ExploreTrendingTopics.tsx

Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>

* Update src/screens/Search/modules/ExploreTrendingTopics.tsx

Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>

* lint

* maybe fix this

---------

Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>
Co-authored-by: Eric Bailey <git@esb.lol>
Co-authored-by: Hailey <me@haileyok.com>
Diffstat (limited to 'src/view/screens')
-rw-r--r--src/view/screens/Search/Explore.tsx641
-rw-r--r--src/view/screens/Search/Search.tsx1165
-rw-r--r--src/view/screens/Search/index.tsx1
3 files changed, 0 insertions, 1807 deletions
diff --git a/src/view/screens/Search/Explore.tsx b/src/view/screens/Search/Explore.tsx
deleted file mode 100644
index 520e103a4..000000000
--- a/src/view/screens/Search/Explore.tsx
+++ /dev/null
@@ -1,641 +0,0 @@
-import React from 'react'
-import {View} from 'react-native'
-import {
-  AppBskyActorDefs,
-  AppBskyFeedDefs,
-  moderateProfile,
-  ModerationDecision,
-  ModerationOpts,
-} from '@atproto/api'
-import {msg, Trans} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-
-import {logEvent} from '#/lib/statsig/statsig'
-import {cleanError} from '#/lib/strings/errors'
-import {logger} from '#/logger'
-import {isNative, isWeb} from '#/platform/detection'
-import {useModerationOpts} from '#/state/preferences/moderation-opts'
-import {useGetPopularFeedsQuery} from '#/state/queries/feed'
-import {usePreferencesQuery} from '#/state/queries/preferences'
-import {useSuggestedFollowsQuery} from '#/state/queries/suggested-follows'
-import {ProfileCardWithFollowBtn} from '#/view/com/profile/ProfileCard'
-import {List} from '#/view/com/util/List'
-import {
-  FeedFeedLoadingPlaceholder,
-  ProfileCardFeedLoadingPlaceholder,
-} from '#/view/com/util/LoadingPlaceholder'
-import {UserAvatar} from '#/view/com/util/UserAvatar'
-import {ExploreRecommendations} from '#/screens/Search/components/ExploreRecommendations'
-import {ExploreTrendingTopics} from '#/screens/Search/components/ExploreTrendingTopics'
-import {ExploreTrendingVideos} from '#/screens/Search/components/ExploreTrendingVideos'
-import {atoms as a, useTheme, ViewStyleProp} from '#/alf'
-import {Button} from '#/components/Button'
-import * as FeedCard from '#/components/FeedCard'
-import {ArrowBottom_Stroke2_Corner0_Rounded as ArrowBottom} from '#/components/icons/Arrow'
-import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
-import {Props as SVGIconProps} from '#/components/icons/common'
-import {ListSparkle_Stroke2_Corner0_Rounded as ListSparkle} from '#/components/icons/ListSparkle'
-import {UserCircle_Stroke2_Corner0_Rounded as Person} from '#/components/icons/UserCircle'
-import {Loader} from '#/components/Loader'
-import {Text} from '#/components/Typography'
-
-function SuggestedItemsHeader({
-  title,
-  description,
-  style,
-  icon: Icon,
-}: {
-  title: string
-  description: string
-  icon: React.ComponentType<SVGIconProps>
-} & ViewStyleProp) {
-  const t = useTheme()
-
-  return (
-    <View
-      style={[
-        isWeb
-          ? [a.flex_row, a.px_lg, a.py_lg, a.pt_2xl, a.gap_md]
-          : [{flexDirection: 'row-reverse'}, a.p_lg, a.pt_2xl, a.gap_md],
-        a.border_b,
-        t.atoms.border_contrast_low,
-        style,
-      ]}>
-      <View style={[a.flex_1, a.gap_sm]}>
-        <View style={[a.flex_row, a.align_center, a.gap_sm]}>
-          <Icon
-            size="lg"
-            fill={t.palette.primary_500}
-            style={{marginLeft: -2}}
-          />
-          <Text style={[a.text_2xl, a.font_heavy, t.atoms.text]}>{title}</Text>
-        </View>
-        <Text style={[t.atoms.text_contrast_high, a.leading_snug]}>
-          {description}
-        </Text>
-      </View>
-    </View>
-  )
-}
-
-type LoadMoreItem =
-  | {
-      type: 'profile'
-      key: string
-      avatar: string | undefined
-      moderation: ModerationDecision
-    }
-  | {
-      type: 'feed'
-      key: string
-      avatar: string | undefined
-      moderation: undefined
-    }
-
-function LoadMore({
-  item,
-  moderationOpts,
-}: {
-  item: ExploreScreenItems & {type: 'loadMore'}
-  moderationOpts?: ModerationOpts
-}) {
-  const t = useTheme()
-  const {_} = useLingui()
-  const items: LoadMoreItem[] = React.useMemo(() => {
-    return item.items
-      .map(_item => {
-        let loadMoreItem: LoadMoreItem | undefined
-        if (_item.type === 'profile') {
-          loadMoreItem = {
-            type: 'profile',
-            key: _item.profile.did,
-            avatar: _item.profile.avatar,
-            moderation: moderateProfile(_item.profile, moderationOpts!),
-          }
-        } else if (_item.type === 'feed') {
-          loadMoreItem = {
-            type: 'feed',
-            key: _item.feed.uri,
-            avatar: _item.feed.avatar,
-            moderation: undefined,
-          }
-        }
-        return loadMoreItem
-      })
-      .filter(n => !!n)
-  }, [item.items, moderationOpts])
-
-  if (items.length === 0) return null
-
-  const type = items[0].type
-
-  return (
-    <View style={[]}>
-      <Button
-        label={_(msg`Load more`)}
-        onPress={item.onLoadMore}
-        style={[a.relative, a.w_full]}>
-        {({hovered, pressed}) => (
-          <View
-            style={[
-              a.flex_1,
-              a.flex_row,
-              a.align_center,
-              a.px_lg,
-              a.py_md,
-              (hovered || pressed) && t.atoms.bg_contrast_25,
-            ]}>
-            <View
-              style={[
-                a.relative,
-                {
-                  height: 32,
-                  width: 32 + 15 * items.length,
-                },
-              ]}>
-              <View
-                style={[
-                  a.align_center,
-                  a.justify_center,
-                  t.atoms.bg_contrast_25,
-                  a.absolute,
-                  {
-                    width: 30,
-                    height: 30,
-                    left: 0,
-                    borderWidth: 1,
-                    backgroundColor: t.palette.primary_500,
-                    borderColor: t.atoms.bg.backgroundColor,
-                    borderRadius: type === 'profile' ? 999 : 4,
-                    zIndex: 4,
-                  },
-                ]}>
-                <ArrowBottom fill={t.palette.white} />
-              </View>
-              {items.map((_item, i) => {
-                return (
-                  <View
-                    key={_item.key}
-                    style={[
-                      t.atoms.bg_contrast_25,
-                      a.absolute,
-                      {
-                        width: 30,
-                        height: 30,
-                        left: (i + 1) * 15,
-                        borderWidth: 1,
-                        borderColor: t.atoms.bg.backgroundColor,
-                        borderRadius: _item.type === 'profile' ? 999 : 4,
-                        zIndex: 3 - i,
-                      },
-                    ]}>
-                    {moderationOpts && (
-                      <>
-                        {_item.type === 'profile' ? (
-                          <UserAvatar
-                            size={28}
-                            avatar={_item.avatar}
-                            moderation={_item.moderation.ui('avatar')}
-                            type="user"
-                          />
-                        ) : _item.type === 'feed' ? (
-                          <UserAvatar
-                            size={28}
-                            avatar={_item.avatar}
-                            type="algo"
-                          />
-                        ) : null}
-                      </>
-                    )}
-                  </View>
-                )
-              })}
-            </View>
-
-            <Text
-              style={[
-                a.pl_sm,
-                a.leading_snug,
-                hovered ? t.atoms.text : t.atoms.text_contrast_medium,
-              ]}>
-              {type === 'profile' ? (
-                <Trans>Load more suggested follows</Trans>
-              ) : (
-                <Trans>Load more suggested feeds</Trans>
-              )}
-            </Text>
-
-            <View style={[a.flex_1, a.align_end]}>
-              {item.isLoadingMore && <Loader size="lg" />}
-            </View>
-          </View>
-        )}
-      </Button>
-    </View>
-  )
-}
-
-type ExploreScreenItems =
-  | {
-      type: 'header'
-      key: string
-      title: string
-      description: string
-      style?: ViewStyleProp['style']
-      icon: React.ComponentType<SVGIconProps>
-    }
-  | {
-      type: 'trendingTopics'
-      key: string
-    }
-  | {
-      type: 'trendingVideos'
-      key: string
-    }
-  | {
-      type: 'recommendations'
-      key: string
-    }
-  | {
-      type: 'profile'
-      key: string
-      profile: AppBskyActorDefs.ProfileView
-      recId?: number
-    }
-  | {
-      type: 'feed'
-      key: string
-      feed: AppBskyFeedDefs.GeneratorView
-    }
-  | {
-      type: 'loadMore'
-      key: string
-      isLoadingMore: boolean
-      onLoadMore: () => void
-      items: ExploreScreenItems[]
-    }
-  | {
-      type: 'profilePlaceholder'
-      key: string
-    }
-  | {
-      type: 'feedPlaceholder'
-      key: string
-    }
-  | {
-      type: 'error'
-      key: string
-      message: string
-      error: string
-    }
-
-export function Explore() {
-  const {_} = useLingui()
-  const t = useTheme()
-  const {data: preferences, error: preferencesError} = usePreferencesQuery()
-  const moderationOpts = useModerationOpts()
-  const {
-    data: profiles,
-    hasNextPage: hasNextProfilesPage,
-    isLoading: isLoadingProfiles,
-    isFetchingNextPage: isFetchingNextProfilesPage,
-    error: profilesError,
-    fetchNextPage: fetchNextProfilesPage,
-  } = useSuggestedFollowsQuery({limit: 6, subsequentPageLimit: 10})
-  const {
-    data: feeds,
-    hasNextPage: hasNextFeedsPage,
-    isLoading: isLoadingFeeds,
-    isFetchingNextPage: isFetchingNextFeedsPage,
-    error: feedsError,
-    fetchNextPage: fetchNextFeedsPage,
-  } = useGetPopularFeedsQuery({limit: 10})
-
-  const isLoadingMoreProfiles = isFetchingNextProfilesPage && !isLoadingProfiles
-  const onLoadMoreProfiles = React.useCallback(async () => {
-    if (isFetchingNextProfilesPage || !hasNextProfilesPage || profilesError)
-      return
-    try {
-      await fetchNextProfilesPage()
-    } catch (err) {
-      logger.error('Failed to load more suggested follows', {message: err})
-    }
-  }, [
-    isFetchingNextProfilesPage,
-    hasNextProfilesPage,
-    profilesError,
-    fetchNextProfilesPage,
-  ])
-
-  const isLoadingMoreFeeds = isFetchingNextFeedsPage && !isLoadingFeeds
-  const onLoadMoreFeeds = React.useCallback(async () => {
-    if (isFetchingNextFeedsPage || !hasNextFeedsPage || feedsError) return
-    try {
-      await fetchNextFeedsPage()
-    } catch (err) {
-      logger.error('Failed to load more suggested follows', {message: err})
-    }
-  }, [
-    isFetchingNextFeedsPage,
-    hasNextFeedsPage,
-    feedsError,
-    fetchNextFeedsPage,
-  ])
-
-  const items = React.useMemo<ExploreScreenItems[]>(() => {
-    const i: ExploreScreenItems[] = []
-
-    i.push({
-      type: 'trendingTopics',
-      key: `trending-topics`,
-    })
-
-    if (isNative) {
-      i.push({
-        type: 'trendingVideos',
-        key: `trending-videos`,
-      })
-    }
-
-    i.push({
-      type: 'recommendations',
-      key: `recommendations`,
-    })
-
-    i.push({
-      type: 'header',
-      key: 'suggested-follows-header',
-      title: _(msg`Suggested accounts`),
-      description: _(
-        msg`Follow more accounts to get connected to your interests and build your network.`,
-      ),
-      icon: Person,
-    })
-
-    if (profiles) {
-      // Currently the responses contain duplicate items.
-      // Needs to be fixed on backend, but let's dedupe to be safe.
-      let seen = new Set()
-      const profileItems: ExploreScreenItems[] = []
-      for (const page of profiles.pages) {
-        for (const actor of page.actors) {
-          if (!seen.has(actor.did)) {
-            seen.add(actor.did)
-            profileItems.push({
-              type: 'profile',
-              key: actor.did,
-              profile: actor,
-              recId: page.recId,
-            })
-          }
-        }
-      }
-
-      if (hasNextProfilesPage) {
-        // splice off 3 as previews if we have a next page
-        const previews = profileItems.splice(-3)
-        // push remainder
-        i.push(...profileItems)
-        i.push({
-          type: 'loadMore',
-          key: 'loadMoreProfiles',
-          isLoadingMore: isLoadingMoreProfiles,
-          onLoadMore: onLoadMoreProfiles,
-          items: previews,
-        })
-      } else {
-        i.push(...profileItems)
-      }
-    } else {
-      if (profilesError) {
-        i.push({
-          type: 'error',
-          key: 'profilesError',
-          message: _(msg`Failed to load suggested follows`),
-          error: cleanError(profilesError),
-        })
-      } else {
-        i.push({type: 'profilePlaceholder', key: 'profilePlaceholder'})
-      }
-    }
-
-    i.push({
-      type: 'header',
-      key: 'suggested-feeds-header',
-      title: _(msg`Discover new feeds`),
-      description: _(
-        msg`Choose your own timeline! Feeds built by the community help you find content you love.`,
-      ),
-      style: [a.pt_5xl],
-      icon: ListSparkle,
-    })
-
-    if (feeds && preferences) {
-      // Currently the responses contain duplicate items.
-      // Needs to be fixed on backend, but let's dedupe to be safe.
-      let seen = new Set()
-      const feedItems: ExploreScreenItems[] = []
-      for (const page of feeds.pages) {
-        for (const feed of page.feeds) {
-          if (!seen.has(feed.uri)) {
-            seen.add(feed.uri)
-            feedItems.push({
-              type: 'feed',
-              key: feed.uri,
-              feed,
-            })
-          }
-        }
-      }
-
-      // feeds errors can occur during pagination, so feeds is truthy
-      if (feedsError) {
-        i.push({
-          type: 'error',
-          key: 'feedsError',
-          message: _(msg`Failed to load suggested feeds`),
-          error: cleanError(feedsError),
-        })
-      } else if (preferencesError) {
-        i.push({
-          type: 'error',
-          key: 'preferencesError',
-          message: _(msg`Failed to load feeds preferences`),
-          error: cleanError(preferencesError),
-        })
-      } else if (hasNextFeedsPage) {
-        const preview = feedItems.splice(-3)
-        i.push(...feedItems)
-        i.push({
-          type: 'loadMore',
-          key: 'loadMoreFeeds',
-          isLoadingMore: isLoadingMoreFeeds,
-          onLoadMore: onLoadMoreFeeds,
-          items: preview,
-        })
-      } else {
-        i.push(...feedItems)
-      }
-    } else {
-      if (feedsError) {
-        i.push({
-          type: 'error',
-          key: 'feedsError',
-          message: _(msg`Failed to load suggested feeds`),
-          error: cleanError(feedsError),
-        })
-      } else if (preferencesError) {
-        i.push({
-          type: 'error',
-          key: 'preferencesError',
-          message: _(msg`Failed to load feeds preferences`),
-          error: cleanError(preferencesError),
-        })
-      } else {
-        i.push({type: 'feedPlaceholder', key: 'feedPlaceholder'})
-      }
-    }
-
-    return i
-  }, [
-    _,
-    profiles,
-    feeds,
-    preferences,
-    onLoadMoreFeeds,
-    onLoadMoreProfiles,
-    isLoadingMoreProfiles,
-    isLoadingMoreFeeds,
-    profilesError,
-    feedsError,
-    preferencesError,
-    hasNextProfilesPage,
-    hasNextFeedsPage,
-  ])
-
-  const renderItem = React.useCallback(
-    ({item, index}: {item: ExploreScreenItems; index: number}) => {
-      switch (item.type) {
-        case 'header': {
-          return (
-            <SuggestedItemsHeader
-              title={item.title}
-              description={item.description}
-              style={item.style}
-              icon={item.icon}
-            />
-          )
-        }
-        case 'trendingTopics': {
-          return <ExploreTrendingTopics />
-        }
-        case 'trendingVideos': {
-          return <ExploreTrendingVideos />
-        }
-        case 'recommendations': {
-          return <ExploreRecommendations />
-        }
-        case 'profile': {
-          return (
-            <View style={[a.border_b, t.atoms.border_contrast_low]}>
-              <ProfileCardWithFollowBtn
-                profile={item.profile}
-                noBg
-                noBorder
-                showKnownFollowers
-                onPress={() => {
-                  logEvent('suggestedUser:press', {
-                    logContext: 'Explore',
-                    recId: item.recId,
-                    position: index,
-                  })
-                }}
-                onFollow={() => {
-                  logEvent('suggestedUser:follow', {
-                    logContext: 'Explore',
-                    location: 'Card',
-                    recId: item.recId,
-                    position: index,
-                  })
-                }}
-              />
-            </View>
-          )
-        }
-        case 'feed': {
-          return (
-            <View
-              style={[
-                a.border_b,
-                t.atoms.border_contrast_low,
-                a.px_lg,
-                a.py_lg,
-              ]}>
-              <FeedCard.Default view={item.feed} />
-            </View>
-          )
-        }
-        case 'loadMore': {
-          return <LoadMore item={item} moderationOpts={moderationOpts} />
-        }
-        case 'profilePlaceholder': {
-          return <ProfileCardFeedLoadingPlaceholder />
-        }
-        case 'feedPlaceholder': {
-          return <FeedFeedLoadingPlaceholder />
-        }
-        case 'error': {
-          return (
-            <View
-              style={[
-                a.border_t,
-                a.pt_md,
-                a.px_md,
-                t.atoms.border_contrast_low,
-              ]}>
-              <View
-                style={[
-                  a.flex_row,
-                  a.gap_md,
-                  a.p_lg,
-                  a.rounded_sm,
-                  t.atoms.bg_contrast_25,
-                ]}>
-                <CircleInfo size="md" fill={t.palette.negative_400} />
-                <View style={[a.flex_1, a.gap_sm]}>
-                  <Text style={[a.font_bold, a.leading_snug]}>
-                    {item.message}
-                  </Text>
-                  <Text
-                    style={[
-                      a.italic,
-                      a.leading_snug,
-                      t.atoms.text_contrast_medium,
-                    ]}>
-                    {item.error}
-                  </Text>
-                </View>
-              </View>
-            </View>
-          )
-        }
-      }
-    },
-    [t, moderationOpts],
-  )
-
-  // note: actually not a screen, instead it's nested within
-  // the search screen. so we don't need Layout.Screen
-  return (
-    <List
-      data={items}
-      renderItem={renderItem}
-      keyExtractor={item => item.key}
-      // @ts-ignore web only -prf
-      desktopFixedHeight
-      contentContainerStyle={{paddingBottom: 100}}
-      keyboardShouldPersistTaps="handled"
-      keyboardDismissMode="on-drag"
-    />
-  )
-}
diff --git a/src/view/screens/Search/Search.tsx b/src/view/screens/Search/Search.tsx
deleted file mode 100644
index 785e1872e..000000000
--- a/src/view/screens/Search/Search.tsx
+++ /dev/null
@@ -1,1165 +0,0 @@
-import React, {useCallback, useLayoutEffect, useMemo} from 'react'
-import {
-  ActivityIndicator,
-  Pressable,
-  StyleProp,
-  StyleSheet,
-  TextInput,
-  View,
-  ViewStyle,
-} from 'react-native'
-import {ScrollView as RNGHScrollView} from 'react-native-gesture-handler'
-import {AppBskyActorDefs, AppBskyFeedDefs, moderateProfile} from '@atproto/api'
-import {msg, Trans} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-import {useFocusEffect, useNavigation, useRoute} from '@react-navigation/native'
-import {useQueryClient} from '@tanstack/react-query'
-
-import {APP_LANGUAGES, LANGUAGES} from '#/lib/../locale/languages'
-import {createHitslop, HITSLOP_20} from '#/lib/constants'
-import {HITSLOP_10} from '#/lib/constants'
-import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
-import {MagnifyingGlassIcon} from '#/lib/icons'
-import {makeProfileLink} from '#/lib/routes/links'
-import {NavigationProp} from '#/lib/routes/types'
-import {
-  NativeStackScreenProps,
-  SearchTabNavigatorParams,
-} from '#/lib/routes/types'
-import {sanitizeDisplayName} from '#/lib/strings/display-names'
-import {augmentSearchQuery} from '#/lib/strings/helpers'
-import {languageName} from '#/locale/helpers'
-import {isNative, isWeb} from '#/platform/detection'
-import {listenSoftReset} from '#/state/events'
-import {useLanguagePrefs} from '#/state/preferences/languages'
-import {useModerationOpts} from '#/state/preferences/moderation-opts'
-import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete'
-import {useActorSearch} from '#/state/queries/actor-search'
-import {usePopularFeedsSearch} from '#/state/queries/feed'
-import {
-  unstableCacheProfileView,
-  useProfilesQuery,
-} from '#/state/queries/profile'
-import {useSearchPostsQuery} from '#/state/queries/search-posts'
-import {useSession} from '#/state/session'
-import {useSetMinimalShellMode} from '#/state/shell'
-import {Pager} from '#/view/com/pager/Pager'
-import {TabBar} from '#/view/com/pager/TabBar'
-import {Post} from '#/view/com/post/Post'
-import {ProfileCardWithFollowBtn} from '#/view/com/profile/ProfileCard'
-import {Link} from '#/view/com/util/Link'
-import {List} from '#/view/com/util/List'
-import {UserAvatar} from '#/view/com/util/UserAvatar'
-import {Explore} from '#/view/screens/Search/Explore'
-import {SearchLinkCard, SearchProfileCard} from '#/view/shell/desktop/Search'
-import {makeSearchQuery, Params, parseSearchQuery} from '#/screens/Search/utils'
-import {
-  atoms as a,
-  native,
-  platform,
-  tokens,
-  useBreakpoints,
-  useTheme,
-  web,
-} from '#/alf'
-import {Button, ButtonIcon, ButtonText} from '#/components/Button'
-import * as FeedCard from '#/components/FeedCard'
-import {SearchInput} from '#/components/forms/SearchInput'
-import {
-  ChevronBottom_Stroke2_Corner0_Rounded as ChevronDownIcon,
-  ChevronTopBottom_Stroke2_Corner0_Rounded as ChevronUpDownIcon,
-} from '#/components/icons/Chevron'
-import {Earth_Stroke2_Corner0_Rounded as EarthIcon} from '#/components/icons/Globe'
-import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times'
-import * as Layout from '#/components/Layout'
-import * as Menu from '#/components/Menu'
-import {Text} from '#/components/Typography'
-import {account, useStorage} from '#/storage'
-import * as bsky from '#/types/bsky'
-
-function Loader() {
-  return (
-    <Layout.Content>
-      <View style={[a.py_xl]}>
-        <ActivityIndicator />
-      </View>
-    </Layout.Content>
-  )
-}
-
-function EmptyState({message, error}: {message: string; error?: string}) {
-  const t = useTheme()
-
-  return (
-    <Layout.Content>
-      <View style={[a.p_xl]}>
-        <View style={[t.atoms.bg_contrast_25, a.rounded_sm, a.p_lg]}>
-          <Text style={[a.text_md]}>{message}</Text>
-
-          {error && (
-            <>
-              <View
-                style={[
-                  {
-                    marginVertical: 12,
-                    height: 1,
-                    width: '100%',
-                    backgroundColor: t.atoms.text.color,
-                    opacity: 0.2,
-                  },
-                ]}
-              />
-
-              <Text style={[t.atoms.text_contrast_medium]}>
-                <Trans>Error:</Trans> {error}
-              </Text>
-            </>
-          )}
-        </View>
-      </View>
-    </Layout.Content>
-  )
-}
-
-type SearchResultSlice =
-  | {
-      type: 'post'
-      key: string
-      post: AppBskyFeedDefs.PostView
-    }
-  | {
-      type: 'loadingMore'
-      key: string
-    }
-
-let SearchScreenPostResults = ({
-  query,
-  sort,
-  active,
-}: {
-  query: string
-  sort?: 'top' | 'latest'
-  active: boolean
-}): React.ReactNode => {
-  const {_} = useLingui()
-  const {currentAccount} = useSession()
-  const [isPTR, setIsPTR] = React.useState(false)
-
-  const augmentedQuery = React.useMemo(() => {
-    return augmentSearchQuery(query || '', {did: currentAccount?.did})
-  }, [query, currentAccount])
-
-  const {
-    isFetched,
-    data: results,
-    isFetching,
-    error,
-    refetch,
-    fetchNextPage,
-    isFetchingNextPage,
-    hasNextPage,
-  } = useSearchPostsQuery({query: augmentedQuery, sort, enabled: active})
-
-  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 ? (
-    <EmptyState
-      message={_(
-        msg`We're sorry, but your search could not be completed. Please try again in a few minutes.`,
-      )}
-      error={error.toString()}
-    />
-  ) : (
-    <>
-      {isFetched ? (
-        <>
-          {posts.length ? (
-            <List
-              data={items}
-              renderItem={({item}) => {
-                if (item.type === 'post') {
-                  return <Post post={item.post} />
-                } else {
-                  return null
-                }
-              }}
-              keyExtractor={item => item.key}
-              refreshing={isPTR}
-              onRefresh={onPullToRefresh}
-              onEndReached={onEndReached}
-              desktopFixedHeight
-              contentContainerStyle={{paddingBottom: 100}}
-            />
-          ) : (
-            <EmptyState message={_(msg`No results found for ${query}`)} />
-          )}
-        </>
-      ) : (
-        <Loader />
-      )}
-    </>
-  )
-}
-SearchScreenPostResults = React.memo(SearchScreenPostResults)
-
-let SearchScreenUserResults = ({
-  query,
-  active,
-}: {
-  query: string
-  active: boolean
-}): React.ReactNode => {
-  const {_} = useLingui()
-
-  const {data: results, isFetched} = useActorSearch({
-    query,
-    enabled: active,
-  })
-
-  return isFetched && results ? (
-    <>
-      {results.length ? (
-        <List
-          data={results}
-          renderItem={({item}) => (
-            <ProfileCardWithFollowBtn profile={item} noBg />
-          )}
-          keyExtractor={item => item.did}
-          desktopFixedHeight
-          contentContainerStyle={{paddingBottom: 100}}
-        />
-      ) : (
-        <EmptyState message={_(msg`No results found for ${query}`)} />
-      )}
-    </>
-  ) : (
-    <Loader />
-  )
-}
-SearchScreenUserResults = React.memo(SearchScreenUserResults)
-
-let SearchScreenFeedsResults = ({
-  query,
-  active,
-}: {
-  query: string
-  active: boolean
-}): React.ReactNode => {
-  const t = useTheme()
-  const {_} = useLingui()
-
-  const {data: results, isFetched} = usePopularFeedsSearch({
-    query,
-    enabled: active,
-  })
-
-  return isFetched && results ? (
-    <>
-      {results.length ? (
-        <List
-          data={results}
-          renderItem={({item}) => (
-            <View
-              style={[
-                a.border_b,
-                t.atoms.border_contrast_low,
-                a.px_lg,
-                a.py_lg,
-              ]}>
-              <FeedCard.Default view={item} />
-            </View>
-          )}
-          keyExtractor={item => item.uri}
-          desktopFixedHeight
-          contentContainerStyle={{paddingBottom: 100}}
-        />
-      ) : (
-        <EmptyState message={_(msg`No results found for ${query}`)} />
-      )}
-    </>
-  ) : (
-    <Loader />
-  )
-}
-SearchScreenFeedsResults = React.memo(SearchScreenFeedsResults)
-
-function SearchLanguageDropdown({
-  value,
-  onChange,
-}: {
-  value: string
-  onChange(value: string): void
-}) {
-  const {_} = useLingui()
-  const {appLanguage, contentLanguages} = useLanguagePrefs()
-
-  const languages = useMemo(() => {
-    return LANGUAGES.filter(
-      (lang, index, self) =>
-        Boolean(lang.code2) && // reduce to the code2 varieties
-        index === self.findIndex(t => t.code2 === lang.code2), // remove dupes (which will happen)
-    )
-      .map(l => ({
-        label: languageName(l, appLanguage),
-        value: l.code2,
-        key: l.code2 + l.code3,
-      }))
-      .sort((a, b) => {
-        // prioritize user's languages
-        const aIsUser = contentLanguages.includes(a.value)
-        const bIsUser = contentLanguages.includes(b.value)
-        if (aIsUser && !bIsUser) return -1
-        if (bIsUser && !aIsUser) return 1
-        // prioritize "common" langs in the network
-        const aIsCommon = !!APP_LANGUAGES.find(
-          al =>
-            // skip `ast`, because it uses a 3-letter code which conflicts with `as`
-            // it begins with `a` anyway so still is top of the list
-            al.code2 !== 'ast' && al.code2.startsWith(a.value),
-        )
-        const bIsCommon = !!APP_LANGUAGES.find(
-          al =>
-            // ditto
-            al.code2 !== 'ast' && al.code2.startsWith(b.value),
-        )
-        if (aIsCommon && !bIsCommon) return -1
-        if (bIsCommon && !aIsCommon) return 1
-        // fall back to alphabetical
-        return a.label.localeCompare(b.label)
-      })
-  }, [appLanguage, contentLanguages])
-
-  const currentLanguageLabel =
-    languages.find(lang => lang.value === value)?.label ?? _(msg`All languages`)
-
-  return (
-    <Menu.Root>
-      <Menu.Trigger
-        label={_(
-          msg`Filter search by language (currently: ${currentLanguageLabel})`,
-        )}>
-        {({props}) => (
-          <Button
-            {...props}
-            label={props.accessibilityLabel}
-            size="small"
-            color={platform({native: 'primary', default: 'secondary'})}
-            variant={platform({native: 'ghost', default: 'solid'})}
-            style={native([
-              a.py_sm,
-              a.px_sm,
-              {marginRight: tokens.space.sm * -1},
-            ])}>
-            <ButtonIcon icon={EarthIcon} />
-            <ButtonText>{currentLanguageLabel}</ButtonText>
-            <ButtonIcon
-              icon={platform({
-                native: ChevronUpDownIcon,
-                default: ChevronDownIcon,
-              })}
-            />
-          </Button>
-        )}
-      </Menu.Trigger>
-      <Menu.Outer>
-        <Menu.LabelText>
-          <Trans>Filter search by language</Trans>
-        </Menu.LabelText>
-        <Menu.Item label={_(msg`All languages`)} onPress={() => onChange('')}>
-          <Menu.ItemText>
-            <Trans>All languages</Trans>
-          </Menu.ItemText>
-          <Menu.ItemRadio selected={value === ''} />
-        </Menu.Item>
-        <Menu.Divider />
-        <Menu.Group>
-          {languages.map(lang => (
-            <Menu.Item
-              key={lang.key}
-              label={lang.label}
-              onPress={() => onChange(lang.value)}>
-              <Menu.ItemText>{lang.label}</Menu.ItemText>
-              <Menu.ItemRadio selected={value === lang.value} />
-            </Menu.Item>
-          ))}
-        </Menu.Group>
-      </Menu.Outer>
-    </Menu.Root>
-  )
-}
-
-function useQueryManager({
-  initialQuery,
-  fixedParams,
-}: {
-  initialQuery: string
-  fixedParams?: Params
-}) {
-  const {query, params: initialParams} = React.useMemo(() => {
-    return parseSearchQuery(initialQuery || '')
-  }, [initialQuery])
-  const [prevInitialQuery, setPrevInitialQuery] = React.useState(initialQuery)
-  const [lang, setLang] = React.useState(initialParams.lang || '')
-
-  if (initialQuery !== prevInitialQuery) {
-    // handle new queryParam change (from manual search entry)
-    setPrevInitialQuery(initialQuery)
-    setLang(initialParams.lang || '')
-  }
-
-  const params = React.useMemo(
-    () => ({
-      // default stuff
-      ...initialParams,
-      // managed stuff
-      lang,
-      ...fixedParams,
-    }),
-    [lang, initialParams, fixedParams],
-  )
-  const handlers = React.useMemo(
-    () => ({
-      setLang,
-    }),
-    [setLang],
-  )
-
-  return React.useMemo(() => {
-    return {
-      query,
-      queryWithParams: makeSearchQuery(query, params),
-      params: {
-        ...params,
-        ...handlers,
-      },
-    }
-  }, [query, params, handlers])
-}
-
-let SearchScreenInner = ({
-  query,
-  queryWithParams,
-  headerHeight,
-}: {
-  query: string
-  queryWithParams: string
-  headerHeight: number
-}): React.ReactNode => {
-  const t = useTheme()
-  const setMinimalShellMode = useSetMinimalShellMode()
-  const {hasSession} = useSession()
-  const {gtTablet} = useBreakpoints()
-  const [activeTab, setActiveTab] = React.useState(0)
-  const {_} = useLingui()
-
-  const onPageSelected = React.useCallback(
-    (index: number) => {
-      setMinimalShellMode(false)
-      setActiveTab(index)
-    },
-    [setMinimalShellMode],
-  )
-
-  const sections = React.useMemo(() => {
-    if (!queryWithParams) return []
-    const noParams = queryWithParams === query
-    return [
-      {
-        title: _(msg`Top`),
-        component: (
-          <SearchScreenPostResults
-            query={queryWithParams}
-            sort="top"
-            active={activeTab === 0}
-          />
-        ),
-      },
-      {
-        title: _(msg`Latest`),
-        component: (
-          <SearchScreenPostResults
-            query={queryWithParams}
-            sort="latest"
-            active={activeTab === 1}
-          />
-        ),
-      },
-      noParams && {
-        title: _(msg`People`),
-        component: (
-          <SearchScreenUserResults query={query} active={activeTab === 2} />
-        ),
-      },
-      noParams && {
-        title: _(msg`Feeds`),
-        component: (
-          <SearchScreenFeedsResults query={query} active={activeTab === 3} />
-        ),
-      },
-    ].filter(Boolean) as {
-      title: string
-      component: React.ReactNode
-    }[]
-  }, [_, query, queryWithParams, activeTab])
-
-  return queryWithParams ? (
-    <Pager
-      onPageSelected={onPageSelected}
-      renderTabBar={props => (
-        <Layout.Center style={[a.z_10, web([a.sticky, {top: headerHeight}])]}>
-          <TabBar items={sections.map(section => section.title)} {...props} />
-        </Layout.Center>
-      )}
-      initialPage={0}>
-      {sections.map((section, i) => (
-        <View key={i}>{section.component}</View>
-      ))}
-    </Pager>
-  ) : hasSession ? (
-    <Explore />
-  ) : (
-    <Layout.Center>
-      <View style={a.flex_1}>
-        {gtTablet && (
-          <View
-            style={[
-              a.border_b,
-              t.atoms.border_contrast_low,
-              a.px_lg,
-              a.pt_sm,
-              a.pb_lg,
-            ]}>
-            <Text style={[a.text_2xl, a.font_heavy]}>
-              <Trans>Search</Trans>
-            </Text>
-          </View>
-        )}
-
-        <View style={[a.align_center, a.justify_center, a.py_4xl, a.gap_lg]}>
-          <MagnifyingGlassIcon
-            strokeWidth={3}
-            size={60}
-            style={t.atoms.text_contrast_medium as StyleProp<ViewStyle>}
-          />
-          <Text style={[t.atoms.text_contrast_medium, a.text_md]}>
-            <Trans>Find posts, users, and feeds on Bluesky</Trans>
-          </Text>
-        </View>
-      </View>
-    </Layout.Center>
-  )
-}
-SearchScreenInner = React.memo(SearchScreenInner)
-
-export function SearchScreen(
-  props: NativeStackScreenProps<SearchTabNavigatorParams, 'Search'>,
-) {
-  const queryParam = props.route?.params?.q ?? ''
-
-  return <SearchScreenShell queryParam={queryParam} testID="searchScreen" />
-}
-
-export function SearchScreenShell({
-  queryParam,
-  testID,
-  fixedParams,
-  navButton = 'menu',
-  inputPlaceholder,
-}: {
-  queryParam: string
-  testID: string
-  fixedParams?: Params
-  navButton?: 'back' | 'menu'
-  inputPlaceholder?: string
-}) {
-  const t = useTheme()
-  const {gtMobile} = useBreakpoints()
-  const navigation = useNavigation<NavigationProp>()
-  const route = useRoute()
-  const textInput = React.useRef<TextInput>(null)
-  const {_} = useLingui()
-  const setMinimalShellMode = useSetMinimalShellMode()
-  const {currentAccount} = useSession()
-  const queryClient = useQueryClient()
-
-  // Query terms
-  const [searchText, setSearchText] = React.useState<string>(queryParam)
-  const {data: autocompleteData, isFetching: isAutocompleteFetching} =
-    useActorAutocompleteQuery(searchText, true)
-
-  const [showAutocomplete, setShowAutocomplete] = React.useState(false)
-
-  const [termHistory = [], setTermHistory] = useStorage(account, [
-    currentAccount?.did ?? 'pwi',
-    'searchTermHistory',
-  ] as const)
-  const [accountHistory = [], setAccountHistory] = useStorage(account, [
-    currentAccount?.did ?? 'pwi',
-    'searchAccountHistory',
-  ])
-
-  const {data: accountHistoryProfiles} = useProfilesQuery({
-    handles: accountHistory,
-    maintainData: true,
-  })
-
-  const updateSearchHistory = useCallback(
-    async (item: string) => {
-      if (!item) return
-      const newSearchHistory = [
-        item,
-        ...termHistory.filter(search => search !== item),
-      ].slice(0, 6)
-      setTermHistory(newSearchHistory)
-    },
-    [termHistory, setTermHistory],
-  )
-
-  const updateProfileHistory = useCallback(
-    async (item: bsky.profile.AnyProfileView) => {
-      const newAccountHistory = [
-        item.did,
-        ...accountHistory.filter(p => p !== item.did),
-      ].slice(0, 5)
-      setAccountHistory(newAccountHistory)
-    },
-    [accountHistory, setAccountHistory],
-  )
-
-  const deleteSearchHistoryItem = useCallback(
-    async (item: string) => {
-      setTermHistory(termHistory.filter(search => search !== item))
-    },
-    [termHistory, setTermHistory],
-  )
-  const deleteProfileHistoryItem = useCallback(
-    async (item: AppBskyActorDefs.ProfileViewDetailed) => {
-      setAccountHistory(accountHistory.filter(p => p !== item.did))
-    },
-    [accountHistory, setAccountHistory],
-  )
-
-  const {params, query, queryWithParams} = useQueryManager({
-    initialQuery: queryParam,
-    fixedParams,
-  })
-  const showFilters = Boolean(queryWithParams && !showAutocomplete)
-
-  // web only - measure header height for sticky positioning
-  const [headerHeight, setHeaderHeight] = React.useState(0)
-  const headerRef = React.useRef(null)
-  useLayoutEffect(() => {
-    if (isWeb) {
-      if (!headerRef.current) return
-      const measurement = (headerRef.current as Element).getBoundingClientRect()
-      setHeaderHeight(measurement.height)
-    }
-  }, [])
-
-  useFocusEffect(
-    useNonReactiveCallback(() => {
-      if (isWeb) {
-        setSearchText(queryParam)
-      }
-    }),
-  )
-
-  const onPressClearQuery = React.useCallback(() => {
-    scrollToTopWeb()
-    setSearchText('')
-    textInput.current?.focus()
-  }, [])
-
-  const onChangeText = React.useCallback(async (text: string) => {
-    scrollToTopWeb()
-    setSearchText(text)
-  }, [])
-
-  const navigateToItem = React.useCallback(
-    (item: string) => {
-      scrollToTopWeb()
-      setShowAutocomplete(false)
-      updateSearchHistory(item)
-
-      if (isWeb) {
-        // @ts-expect-error route is not typesafe
-        navigation.push(route.name, {...route.params, q: item})
-      } else {
-        textInput.current?.blur()
-        navigation.setParams({q: item})
-      }
-    },
-    [updateSearchHistory, navigation, route],
-  )
-
-  const onPressCancelSearch = React.useCallback(() => {
-    scrollToTopWeb()
-    textInput.current?.blur()
-    setShowAutocomplete(false)
-    if (isWeb) {
-      // Empty params resets the URL to be /search rather than /search?q=
-      // eslint-disable-next-line @typescript-eslint/no-unused-vars
-      const {q: _q, ...parameters} = (route.params ?? {}) as {
-        [key: string]: string
-      }
-      // @ts-expect-error route is not typesafe
-      navigation.replace(route.name, parameters)
-    } else {
-      setSearchText('')
-      navigation.setParams({q: ''})
-    }
-  }, [setShowAutocomplete, setSearchText, navigation, route.params, route.name])
-
-  const onSubmit = React.useCallback(() => {
-    navigateToItem(searchText)
-  }, [navigateToItem, searchText])
-
-  const onAutocompleteResultPress = React.useCallback(() => {
-    if (isWeb) {
-      setShowAutocomplete(false)
-    } else {
-      textInput.current?.blur()
-    }
-  }, [])
-
-  const handleHistoryItemClick = React.useCallback(
-    (item: string) => {
-      setSearchText(item)
-      navigateToItem(item)
-    },
-    [navigateToItem],
-  )
-
-  const handleProfileClick = React.useCallback(
-    (profile: bsky.profile.AnyProfileView) => {
-      unstableCacheProfileView(queryClient, profile)
-      // Slight delay to avoid updating during push nav animation.
-      setTimeout(() => {
-        updateProfileHistory(profile)
-      }, 400)
-    },
-    [updateProfileHistory, queryClient],
-  )
-
-  const onSoftReset = React.useCallback(() => {
-    if (isWeb) {
-      // Empty params resets the URL to be /search rather than /search?q=
-      // eslint-disable-next-line @typescript-eslint/no-unused-vars
-      const {q: _q, ...parameters} = (route.params ?? {}) as {
-        [key: string]: string
-      }
-      // @ts-expect-error route is not typesafe
-      navigation.replace(route.name, parameters)
-    } else {
-      setSearchText('')
-      navigation.setParams({q: ''})
-      textInput.current?.focus()
-    }
-  }, [navigation, route])
-
-  useFocusEffect(
-    React.useCallback(() => {
-      setMinimalShellMode(false)
-      return listenSoftReset(onSoftReset)
-    }, [onSoftReset, setMinimalShellMode]),
-  )
-
-  const onSearchInputFocus = React.useCallback(() => {
-    if (isWeb) {
-      // Prevent a jump on iPad by ensuring that
-      // the initial focused render has no result list.
-      requestAnimationFrame(() => {
-        setShowAutocomplete(true)
-      })
-    } else {
-      setShowAutocomplete(true)
-    }
-  }, [setShowAutocomplete])
-
-  const showHeader = !gtMobile || navButton !== 'menu'
-
-  return (
-    <Layout.Screen testID={testID}>
-      <View
-        ref={headerRef}
-        onLayout={evt => {
-          if (isWeb) setHeaderHeight(evt.nativeEvent.layout.height)
-        }}
-        style={[
-          a.relative,
-          a.z_10,
-          web({
-            position: 'sticky',
-            top: 0,
-          }),
-        ]}>
-        <Layout.Center style={t.atoms.bg}>
-          {showHeader && (
-            <View
-              // HACK: shift up search input. we can't remove the top padding
-              // on the search input because it messes up the layout animation
-              // if we add it only when the header is hidden
-              style={{marginBottom: tokens.space.xs * -1}}>
-              <Layout.Header.Outer noBottomBorder>
-                {navButton === 'menu' ? (
-                  <Layout.Header.MenuButton />
-                ) : (
-                  <Layout.Header.BackButton />
-                )}
-                <Layout.Header.Content align="left">
-                  <Layout.Header.TitleText>
-                    <Trans>Search</Trans>
-                  </Layout.Header.TitleText>
-                </Layout.Header.Content>
-                {showFilters ? (
-                  <SearchLanguageDropdown
-                    value={params.lang}
-                    onChange={params.setLang}
-                  />
-                ) : (
-                  <Layout.Header.Slot />
-                )}
-              </Layout.Header.Outer>
-            </View>
-          )}
-          <View style={[a.px_md, a.pt_sm, a.pb_sm, a.overflow_hidden]}>
-            <View style={[a.gap_sm]}>
-              <View style={[a.w_full, a.flex_row, a.align_stretch, a.gap_xs]}>
-                <View style={[a.flex_1]}>
-                  <SearchInput
-                    ref={textInput}
-                    value={searchText}
-                    onFocus={onSearchInputFocus}
-                    onChangeText={onChangeText}
-                    onClearText={onPressClearQuery}
-                    onSubmitEditing={onSubmit}
-                    placeholder={
-                      inputPlaceholder ??
-                      _(msg`Search for posts, users, or feeds`)
-                    }
-                    hitSlop={{...HITSLOP_20, top: 0}}
-                  />
-                </View>
-                {showAutocomplete && (
-                  <Button
-                    label={_(msg`Cancel search`)}
-                    size="large"
-                    variant="ghost"
-                    color="secondary"
-                    style={[a.px_sm]}
-                    onPress={onPressCancelSearch}
-                    hitSlop={HITSLOP_10}>
-                    <ButtonText>
-                      <Trans>Cancel</Trans>
-                    </ButtonText>
-                  </Button>
-                )}
-              </View>
-
-              {showFilters && !showHeader && (
-                <View
-                  style={[
-                    a.flex_row,
-                    a.align_center,
-                    a.justify_between,
-                    a.gap_sm,
-                  ]}>
-                  <SearchLanguageDropdown
-                    value={params.lang}
-                    onChange={params.setLang}
-                  />
-                </View>
-              )}
-            </View>
-          </View>
-        </Layout.Center>
-      </View>
-
-      <View
-        style={{
-          display: showAutocomplete && !fixedParams ? 'flex' : 'none',
-          flex: 1,
-        }}>
-        {searchText.length > 0 ? (
-          <AutocompleteResults
-            isAutocompleteFetching={isAutocompleteFetching}
-            autocompleteData={autocompleteData}
-            searchText={searchText}
-            onSubmit={onSubmit}
-            onResultPress={onAutocompleteResultPress}
-            onProfileClick={handleProfileClick}
-          />
-        ) : (
-          <SearchHistory
-            searchHistory={termHistory}
-            selectedProfiles={accountHistoryProfiles?.profiles || []}
-            onItemClick={handleHistoryItemClick}
-            onProfileClick={handleProfileClick}
-            onRemoveItemClick={deleteSearchHistoryItem}
-            onRemoveProfileClick={deleteProfileHistoryItem}
-          />
-        )}
-      </View>
-      <View
-        style={{
-          display: showAutocomplete ? 'none' : 'flex',
-          flex: 1,
-        }}>
-        <SearchScreenInner
-          query={query}
-          queryWithParams={queryWithParams}
-          headerHeight={headerHeight}
-        />
-      </View>
-    </Layout.Screen>
-  )
-}
-
-let AutocompleteResults = ({
-  isAutocompleteFetching,
-  autocompleteData,
-  searchText,
-  onSubmit,
-  onResultPress,
-  onProfileClick,
-}: {
-  isAutocompleteFetching: boolean
-  autocompleteData: AppBskyActorDefs.ProfileViewBasic[] | undefined
-  searchText: string
-  onSubmit: () => void
-  onResultPress: () => void
-  onProfileClick: (profile: AppBskyActorDefs.ProfileViewBasic) => void
-}): React.ReactNode => {
-  const moderationOpts = useModerationOpts()
-  const {_} = useLingui()
-  return (
-    <>
-      {(isAutocompleteFetching && !autocompleteData?.length) ||
-      !moderationOpts ? (
-        <Loader />
-      ) : (
-        <Layout.Content
-          keyboardShouldPersistTaps="handled"
-          keyboardDismissMode="on-drag">
-          <SearchLinkCard
-            label={_(msg`Search for "${searchText}"`)}
-            onPress={isNative ? onSubmit : undefined}
-            to={
-              isNative
-                ? undefined
-                : `/search?q=${encodeURIComponent(searchText)}`
-            }
-            style={{borderBottomWidth: 1}}
-          />
-          {autocompleteData?.map(item => (
-            <SearchProfileCard
-              key={item.did}
-              profile={item}
-              moderation={moderateProfile(item, moderationOpts)}
-              onPress={() => {
-                onProfileClick(item)
-                onResultPress()
-              }}
-            />
-          ))}
-          <View style={{height: 200}} />
-        </Layout.Content>
-      )}
-    </>
-  )
-}
-AutocompleteResults = React.memo(AutocompleteResults)
-
-function SearchHistory({
-  searchHistory,
-  selectedProfiles,
-  onItemClick,
-  onProfileClick,
-  onRemoveItemClick,
-  onRemoveProfileClick,
-}: {
-  searchHistory: string[]
-  selectedProfiles: AppBskyActorDefs.ProfileViewDetailed[]
-  onItemClick: (item: string) => void
-  onProfileClick: (profile: AppBskyActorDefs.ProfileViewDetailed) => void
-  onRemoveItemClick: (item: string) => void
-  onRemoveProfileClick: (profile: AppBskyActorDefs.ProfileViewDetailed) => void
-}) {
-  const {gtMobile} = useBreakpoints()
-  const t = useTheme()
-  const {_} = useLingui()
-
-  return (
-    <Layout.Content
-      keyboardDismissMode="interactive"
-      keyboardShouldPersistTaps="handled">
-      <View style={[a.w_full, a.px_md]}>
-        {(searchHistory.length > 0 || selectedProfiles.length > 0) && (
-          <Text style={[a.text_md, a.font_bold, a.p_md]}>
-            <Trans>Recent Searches</Trans>
-          </Text>
-        )}
-        {selectedProfiles.length > 0 && (
-          <View
-            style={[
-              styles.selectedProfilesContainer,
-              !gtMobile && styles.selectedProfilesContainerMobile,
-            ]}>
-            <RNGHScrollView
-              keyboardShouldPersistTaps="handled"
-              horizontal={true}
-              style={[
-                a.flex_row,
-                a.flex_nowrap,
-                {marginHorizontal: tokens.space._2xl * -1},
-              ]}
-              contentContainerStyle={[a.px_2xl, a.border_0]}>
-              {selectedProfiles.slice(0, 5).map((profile, index) => (
-                <View
-                  key={index}
-                  style={[
-                    styles.profileItem,
-                    !gtMobile && styles.profileItemMobile,
-                  ]}>
-                  <Link
-                    href={makeProfileLink(profile)}
-                    title={profile.handle}
-                    asAnchor
-                    anchorNoUnderline
-                    onBeforePress={() => onProfileClick(profile)}
-                    style={[a.align_center, a.w_full]}>
-                    <UserAvatar
-                      avatar={profile.avatar}
-                      type={profile.associated?.labeler ? 'labeler' : 'user'}
-                      size={60}
-                    />
-                    <Text
-                      emoji
-                      style={[a.text_xs, a.text_center, styles.profileName]}
-                      numberOfLines={1}>
-                      {sanitizeDisplayName(
-                        profile.displayName || profile.handle,
-                      )}
-                    </Text>
-                  </Link>
-                  <Pressable
-                    accessibilityRole="button"
-                    accessibilityLabel={_(msg`Remove profile`)}
-                    accessibilityHint={_(
-                      msg`Removes profile from search history`,
-                    )}
-                    onPress={() => onRemoveProfileClick(profile)}
-                    hitSlop={createHitslop(6)}
-                    style={styles.profileRemoveBtn}>
-                    <XIcon size="xs" style={t.atoms.text_contrast_low} />
-                  </Pressable>
-                </View>
-              ))}
-            </RNGHScrollView>
-          </View>
-        )}
-        {searchHistory.length > 0 && (
-          <View style={[a.pl_md, a.pr_xs, a.mt_md]}>
-            {searchHistory.slice(0, 5).map((historyItem, index) => (
-              <View key={index} style={[a.flex_row, a.align_center, a.mt_xs]}>
-                <Pressable
-                  accessibilityRole="button"
-                  onPress={() => onItemClick(historyItem)}
-                  hitSlop={HITSLOP_10}
-                  style={[a.flex_1, a.py_md]}>
-                  <Text style={[a.text_md]}>{historyItem}</Text>
-                </Pressable>
-                <Button
-                  label={_(msg`Remove ${historyItem}`)}
-                  onPress={() => onRemoveItemClick(historyItem)}
-                  size="small"
-                  variant="ghost"
-                  color="secondary"
-                  shape="round">
-                  <ButtonIcon icon={XIcon} />
-                </Button>
-              </View>
-            ))}
-          </View>
-        )}
-      </View>
-    </Layout.Content>
-  )
-}
-
-function scrollToTopWeb() {
-  if (isWeb) {
-    window.scrollTo(0, 0)
-  }
-}
-
-const styles = StyleSheet.create({
-  selectedProfilesContainer: {
-    marginTop: 10,
-    paddingHorizontal: 12,
-    height: 80,
-  },
-  selectedProfilesContainerMobile: {
-    height: 100,
-  },
-  profileItem: {
-    alignItems: 'center',
-    marginRight: 15,
-    width: 78,
-  },
-  profileItemMobile: {
-    width: 70,
-  },
-  profileName: {
-    width: 78,
-    marginTop: 6,
-  },
-  profileRemoveBtn: {
-    position: 'absolute',
-    top: 0,
-    right: 5,
-    backgroundColor: 'white',
-    borderRadius: 10,
-    width: 18,
-    height: 18,
-    alignItems: 'center',
-    justifyContent: 'center',
-  },
-})
diff --git a/src/view/screens/Search/index.tsx b/src/view/screens/Search/index.tsx
deleted file mode 100644
index f6c0eca26..000000000
--- a/src/view/screens/Search/index.tsx
+++ /dev/null
@@ -1 +0,0 @@
-export {SearchScreen} from '#/view/screens/Search/Search'