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 {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 {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 { FeedFeedLoadingPlaceholder, ProfileCardFeedLoadingPlaceholder, } from 'view/com/util/LoadingPlaceholder' 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 } & ViewStyleProp) { const t = useTheme() return ( {title} {description} ) } type LoadMoreItems = | { type: 'profile' key: string avatar: string moderation: ModerationDecision } | { type: 'feed' key: string avatar: string moderation: undefined } function LoadMore({ item, moderationOpts, }: { item: ExploreScreenItems & {type: 'loadMore'} moderationOpts?: ModerationOpts }) { const t = useTheme() const {_} = useLingui() const items = React.useMemo(() => { return item.items .map(_item => { if (_item.type === 'profile') { return { type: 'profile', key: _item.profile.did, avatar: _item.profile.avatar, moderation: moderateProfile(_item.profile, moderationOpts!), } } else if (_item.type === 'feed') { return { type: 'feed', key: _item.feed.uri, avatar: _item.feed.avatar, moderation: undefined, } } return undefined }) .filter(Boolean) as LoadMoreItems[] }, [item.items, moderationOpts]) const type = items[0].type return ( ) } type ExploreScreenItems = | { type: 'header' key: string title: string description: string style?: ViewStyleProp['style'] icon: React.ComponentType } | { 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 = 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: 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 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() 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`Discover new feeds`), description: _( msg`Custom feeds built by the community bring you new experiences and help you find the 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() 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, moderationOpts], ) return ( item.key} // @ts-ignore web only -prf desktopFixedHeight contentContainerStyle={{paddingBottom: 200}} keyboardShouldPersistTaps="handled" keyboardDismissMode="on-drag" /> ) }