import React from 'react' import {View} from 'react-native' import {ScrollView} from 'react-native-gesture-handler' import {AppBskyFeedDefs, AtUri} from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useNavigation} from '@react-navigation/native' import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import {NavigationProp} from '#/lib/routes/types' import {logEvent} from '#/lib/statsig/statsig' import {logger} from '#/logger' import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useGetPopularFeedsQuery} from '#/state/queries/feed' import {useProfilesQuery} from '#/state/queries/profile' import {useProgressGuide} from '#/state/shell/progress-guide' import * as userActionHistory from '#/state/userActionHistory' import {SeenPost} from '#/state/userActionHistory' import {atoms as a, useBreakpoints, useTheme, ViewStyleProp, web} from '#/alf' import {Button} from '#/components/Button' import * as FeedCard from '#/components/FeedCard' import {ArrowRight_Stroke2_Corner0_Rounded as Arrow} from '#/components/icons/Arrow' import {Hashtag_Stroke2_Corner0_Rounded as Hashtag} from '#/components/icons/Hashtag' import {PersonPlus_Stroke2_Corner0_Rounded as Person} from '#/components/icons/Person' import {InlineLinkText} from '#/components/Link' import * as ProfileCard from '#/components/ProfileCard' import {Text} from '#/components/Typography' import {ProgressGuideList} from './ProgressGuide/List' const MOBILE_CARD_WIDTH = 300 function CardOuter({ children, style, }: {children: React.ReactNode | React.ReactNode[]} & ViewStyleProp) { const t = useTheme() const {gtMobile} = useBreakpoints() return ( {children} ) } export function SuggestedFollowPlaceholder() { const t = useTheme() return ( ) } export function SuggestedFeedsCardPlaceholder() { const t = useTheme() return ( ) } function getRank(seenPost: SeenPost): string { let tier: string if (seenPost.feedContext === 'popfriends') { tier = 'a' } else if (seenPost.feedContext?.startsWith('cluster')) { tier = 'b' } else if (seenPost.feedContext?.startsWith('ntpc')) { tier = 'c' } else if (seenPost.feedContext?.startsWith('t-')) { tier = 'd' } else if (seenPost.feedContext === 'nettop') { tier = 'e' } else { tier = 'f' } let score = Math.round( Math.log( 1 + seenPost.likeCount + seenPost.repostCount + seenPost.replyCount, ), ) if (seenPost.isFollowedBy || Math.random() > 0.9) { score *= 2 } const rank = 100 - score return `${tier}-${rank}` } function sortSeenPosts(postA: SeenPost, postB: SeenPost): 0 | 1 | -1 { const rankA = getRank(postA) const rankB = getRank(postB) // Yes, we're comparing strings here. // The "larger" string means a worse rank. if (rankA > rankB) { return 1 } else if (rankA < rankB) { return -1 } else { return 0 } } function useExperimentalSuggestedUsersQuery() { const userActionSnapshot = userActionHistory.useActionHistorySnapshot() const dids = React.useMemo(() => { const {likes, follows, seen} = userActionSnapshot const likeDids = likes .map(l => new AtUri(l)) .map(uri => uri.host) .filter(did => !follows.includes(did)) const seenDids = seen .sort(sortSeenPosts) .map(l => new AtUri(l.uri)) .map(uri => uri.host) return [...new Set([...likeDids, ...seenDids])] }, [userActionSnapshot]) const {data, isLoading, error} = useProfilesQuery({ handles: dids.slice(0, 16), }) const profiles = data ? data.profiles.filter(profile => { return !profile.viewer?.following }) : [] return { isLoading, error, profiles: profiles.slice(0, 6), } } export function SuggestedFollows() { const t = useTheme() const {_} = useLingui() const { isLoading: isSuggestionsLoading, profiles, error, } = useExperimentalSuggestedUsersQuery() const moderationOpts = useModerationOpts() const navigation = useNavigation() const {gtMobile} = useBreakpoints() const isLoading = isSuggestionsLoading || !moderationOpts const maxLength = gtMobile ? 4 : 6 const content = isLoading ? ( Array(maxLength) .fill(0) .map((_, i) => ( )) ) : error || !profiles.length ? null : ( <> {profiles.slice(0, maxLength).map(profile => ( { logEvent('feed:interstitial:profileCard:press', {}) }} style={[ a.flex_1, gtMobile && web([a.flex_0, {width: 'calc(50% - 6px)'}]), ]}> {({hovered, pressed}) => ( )} ))} ) if (error || (!isLoading && profiles.length < 4)) { logger.debug(`Not enough profiles to show suggested follows`) return null } return ( Suggested for you {gtMobile ? ( {content} Browse more suggestions ) : ( {content} )} ) } export function SuggestedFeeds() { const numFeedsToDisplay = 3 const t = useTheme() const {_} = useLingui() const {data, isLoading, error} = useGetPopularFeedsQuery({ limit: numFeedsToDisplay, }) const navigation = useNavigation() const {gtMobile} = useBreakpoints() const feeds = React.useMemo(() => { const items: AppBskyFeedDefs.GeneratorView[] = [] if (!data) return items for (const page of data.pages) { for (const feed of page.feeds) { items.push(feed) } } return items }, [data]) const content = isLoading ? ( Array(numFeedsToDisplay) .fill(0) .map((_, i) => ) ) : error || !feeds ? null : ( <> {feeds.slice(0, numFeedsToDisplay).map(feed => ( { logEvent('feed:interstitial:feedCard:press', {}) }}> {({hovered, pressed}) => ( )} ))} ) return error ? null : ( Some other feeds you might like {gtMobile ? ( {content} Browse more suggestions ) : ( {content} )} ) } export function ProgressGuide() { const t = useTheme() const {isDesktop} = useWebMediaQueries() const guide = useProgressGuide('like-10-and-follow-7') if (isDesktop) { return null } return guide ? ( ) : null }