diff --git a/src/view/screens/Search/Explore.tsx b/src/view/screens/Search/Explore.tsx
new file mode 100644
index 0000000000..e1ee3b15d8
--- /dev/null
+++ b/src/view/screens/Search/Explore.tsx
@@ -0,0 +1,449 @@
+import React from 'react'
+import {View} from 'react-native'
+import {AppBskyActorDefs, AppBskyFeedDefs, moderateProfile} from '@atproto/api'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
+import {logger} from '#/logger'
+import {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 {useSession} from '#/state/session'
+import {cleanError} from 'lib/strings/errors'
+import {ProfileCardWithFollowBtn} from '#/view/com/profile/ProfileCard'
+import {List} from '#/view/com/util/List'
+import {UserAvatar} from '#/view/com/util/UserAvatar'
+import {FeedSourceCard} from 'view/com/feeds/FeedSourceCard'
+import {
+ FeedFeedLoadingPlaceholder,
+ ProfileCardFeedLoadingPlaceholder,
+} from 'view/com/util/LoadingPlaceholder'
+import {atoms as a, useTheme as useThemeNew} from '#/alf'
+import {Button} from '#/components/Button'
+import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
+import {Loader} from '#/components/Loader'
+import {Text} from '#/components/Typography'
+
+function SuggestedItemsHeader({
+ title,
+ description,
+}: {
+ title: string
+ description: string
+}) {
+ const t = useThemeNew()
+
+ return (
+
+
+
+ {title}
+
+ {description}
+
+
+ )
+}
+
+type ExploreScreenItems =
+ | {
+ type: 'header'
+ key: string
+ title: string
+ description: string
+ }
+ | {
+ type: 'profile'
+ key: string
+ profile: AppBskyActorDefs.ProfileViewBasic
+ }
+ | {
+ 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 = useThemeNew()
+ const {hasSession} = useSession()
+ const {data: preferences, error: preferencesError} = usePreferencesQuery()
+ const moderationOpts = useModerationOpts()
+ const {
+ data: profiles,
+ hasNextPage: hasNextProfilesPage,
+ isLoading: isLoadingProfiles,
+ isFetchingNextPage: isFetchingNextProfilesPage,
+ error: profilesError,
+ fetchNextPage: fetchNextProfilesPage,
+ } = useSuggestedFollowsQuery({limit: 3})
+ const {
+ data: feeds,
+ hasNextPage: hasNextFeedsPage,
+ isLoading: isLoadingFeeds,
+ isFetchingNextPage: isFetchingNextFeedsPage,
+ error: feedsError,
+ fetchNextPage: fetchNextFeedsPage,
+ } = useGetPopularFeedsQuery({limit: 3})
+
+ 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(() => {
+ const i: ExploreScreenItems[] = [
+ {
+ type: 'header',
+ key: 'suggested-follows-header',
+ title: _(msg`Suggested follows`),
+ description: _(msg`Find new friends or interesting accounts`),
+ },
+ ]
+
+ 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()
+ for (const page of profiles.pages) {
+ for (const actor of page.actors) {
+ if (!seen.has(actor.did)) {
+ seen.add(actor.did)
+ i.push({
+ type: 'profile',
+ key: actor.did,
+ profile: actor,
+ })
+ }
+ }
+ }
+
+ i.push({
+ type: 'loadMore',
+ key: 'loadMoreProfiles',
+ isLoadingMore: isLoadingMoreProfiles,
+ onLoadMore: onLoadMoreProfiles,
+ items: i.filter(item => item.type === 'profile').slice(-3),
+ })
+ } 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`Suggested feeds`),
+ description: _(msg`Discover new content`),
+ })
+
+ 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()
+ for (const page of feeds.pages) {
+ for (const feed of page.feeds) {
+ if (!seen.has(feed.uri)) {
+ seen.add(feed.uri)
+ i.push({
+ type: 'feed',
+ key: feed.uri,
+ feed,
+ })
+ }
+ }
+ }
+
+ 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: 'loadMore',
+ key: 'loadMoreFeeds',
+ isLoadingMore: isLoadingMoreFeeds,
+ onLoadMore: onLoadMoreFeeds,
+ items: i.filter(item => item.type === 'feed').slice(-3),
+ })
+ }
+ } 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,
+ ])
+
+ const renderItem = React.useCallback(
+ ({item}: {item: ExploreScreenItems}) => {
+ switch (item.type) {
+ case 'header': {
+ return (
+
+ )
+ }
+ case 'profile': {
+ return
+ }
+ case 'feed': {
+ return (
+
+ )
+ }
+ case 'loadMore': {
+ return (
+
+
+
+ )
+ }
+ case 'profilePlaceholder': {
+ return
+ }
+ case 'feedPlaceholder': {
+ return
+ }
+ case 'error': {
+ return (
+
+
+
+
+
+ {item.message}
+
+
+ {item.error}
+
+
+
+
+ )
+ }
+ }
+ },
+ [_, t, hasSession, moderationOpts],
+ )
+
+ return (
+ item.key}
+ // @ts-ignore web only -prf
+ desktopFixedHeight
+ contentContainerStyle={{paddingBottom: 200}}
+ keyboardShouldPersistTaps="handled"
+ keyboardDismissMode="on-drag"
+ />
+ )
+}
diff --git a/src/view/screens/Search/Search.tsx b/src/view/screens/Search/Search.tsx
index cb8a41aefb..b1337c7397 100644
--- a/src/view/screens/Search/Search.tsx
+++ b/src/view/screens/Search/Search.tsx
@@ -35,10 +35,7 @@ import {listenSoftReset} from '#/state/events'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete'
import {useActorSearch} from '#/state/queries/actor-search'
-import {useGetPopularFeedsQuery} from '#/state/queries/feed'
-import {usePreferencesQuery} from '#/state/queries/preferences'
import {useSearchPostsQuery} from '#/state/queries/search-posts'
-import {useSuggestedFollowsQuery} from '#/state/queries/suggested-follows'
import {useSession} from '#/state/session'
import {useSetDrawerOpen} from '#/state/shell'
import {useSetDrawerSwipeDisabled, useSetMinimalShellMode} from '#/state/shell'
@@ -48,7 +45,6 @@ import {
NativeStackScreenProps,
SearchTabNavigatorParams,
} from 'lib/routes/types'
-import {cleanError} from 'lib/strings/errors'
import {useTheme} from 'lib/ThemeContext'
import {Pager} from '#/view/com/pager/Pager'
import {TabBar} from '#/view/com/pager/TabBar'
@@ -57,20 +53,11 @@ import {ProfileCardWithFollowBtn} from '#/view/com/profile/ProfileCard'
import {Link} from '#/view/com/util/Link'
import {List} from '#/view/com/util/List'
import {Text} from '#/view/com/util/text/Text'
-import {UserAvatar} from '#/view/com/util/UserAvatar'
import {CenteredView, ScrollView} from '#/view/com/util/Views'
+import {Explore} from '#/view/screens/Search/Explore'
import {SearchLinkCard, SearchProfileCard} from '#/view/shell/desktop/Search'
-import {FeedSourceCard} from 'view/com/feeds/FeedSourceCard'
-import {
- FeedFeedLoadingPlaceholder,
- ProfileCardFeedLoadingPlaceholder,
-} from 'view/com/util/LoadingPlaceholder'
-import {atoms as a, useTheme as useThemeNew} from '#/alf'
-import {Button} from '#/components/Button'
-import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
+import {atoms as a} from '#/alf'
import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu'
-import {Loader as LoaderIcon} from '#/components/Loader'
-import {Text as TextNewText} from '#/components/Typography'
function Loader() {
const pal = usePalette('default')
@@ -133,428 +120,6 @@ function EmptyState({message, error}: {message: string; error?: string}) {
)
}
-function SuggestedItemsHeader({
- title,
- description,
-}: {
- title: string
- description: string
-}) {
- const t = useThemeNew()
-
- return (
-
-
-
- {title}
-
- {description}
-
-
- )
-}
-
-type ExploreScreenItems =
- | {
- type: 'header'
- key: string
- title: string
- description: string
- }
- | {
- type: 'profile'
- key: string
- profile: AppBskyActorDefs.ProfileViewBasic
- }
- | {
- 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
- }
-
-function ExploreScreen() {
- const {_} = useLingui()
- const t = useThemeNew()
- const {hasSession} = useSession()
- const {data: preferences, error: preferencesError} = usePreferencesQuery()
- const moderationOpts = useModerationOpts()
- const {
- data: profiles,
- hasNextPage: hasNextProfilesPage,
- isLoading: isLoadingProfiles,
- isFetchingNextPage: isFetchingNextProfilesPage,
- error: profilesError,
- fetchNextPage: fetchNextProfilesPage,
- } = useSuggestedFollowsQuery({limit: 3})
- const {
- data: feeds,
- hasNextPage: hasNextFeedsPage,
- isLoading: isLoadingFeeds,
- isFetchingNextPage: isFetchingNextFeedsPage,
- error: feedsError,
- fetchNextPage: fetchNextFeedsPage,
- } = useGetPopularFeedsQuery({limit: 3})
-
- 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(() => {
- const i: ExploreScreenItems[] = [
- {
- type: 'header',
- key: 'suggested-follows-header',
- title: _(msg`Suggested follows`),
- description: _(msg`Find new friends or interesting accounts`),
- },
- ]
-
- 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()
- for (const page of profiles.pages) {
- for (const actor of page.actors) {
- if (!seen.has(actor.did)) {
- seen.add(actor.did)
- i.push({
- type: 'profile',
- key: actor.did,
- profile: actor,
- })
- }
- }
- }
-
- i.push({
- type: 'loadMore',
- key: 'loadMoreProfiles',
- isLoadingMore: isLoadingMoreProfiles,
- onLoadMore: onLoadMoreProfiles,
- items: i.filter(item => item.type === 'profile').slice(-3),
- })
- } 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`Suggested feeds`),
- description: _(msg`Discover new content`),
- })
-
- 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()
- for (const page of feeds.pages) {
- for (const feed of page.feeds) {
- if (!seen.has(feed.uri)) {
- seen.add(feed.uri)
- i.push({
- type: 'feed',
- key: feed.uri,
- feed,
- })
- }
- }
- }
-
- 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: 'loadMore',
- key: 'loadMoreFeeds',
- isLoadingMore: isLoadingMoreFeeds,
- onLoadMore: onLoadMoreFeeds,
- items: i.filter(item => item.type === 'feed').slice(-3),
- })
- }
- } 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,
- ])
-
- const renderItem = React.useCallback(
- ({item}: {item: ExploreScreenItems}) => {
- switch (item.type) {
- case 'header': {
- return (
-
- )
- }
- case 'profile': {
- return
- }
- case 'feed': {
- return (
-
- )
- }
- case 'loadMore': {
- return (
-
-
-
- )
- }
- case 'profilePlaceholder': {
- return
- }
- case 'feedPlaceholder': {
- return
- }
- case 'error': {
- return (
-
-
-
-
-
- {item.message}
-
-
- {item.error}
-
-
-
-
- )
- }
- }
- },
- [_, t, hasSession, moderationOpts],
- )
-
- return (
- item.key}
- // @ts-ignore web only -prf
- desktopFixedHeight
- contentContainerStyle={{paddingBottom: 200}}
- keyboardShouldPersistTaps="handled"
- keyboardDismissMode="on-drag"
- />
- )
-}
-
type SearchResultSlice =
| {
type: 'post'
@@ -777,7 +342,7 @@ let SearchScreenInner = ({query}: {query?: string}): React.ReactNode => {
))}
) : hasSession ? (
-
+
) : (