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 ? ( - + ) : (