import {useCallback, useContext, useMemo, useState} from 'react' import {View} from 'react-native' import {type ModerationOpts} from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useMutation, useQueryClient} from '@tanstack/react-query' import * as bcp47Match from 'bcp-47-match' import {wait} from '#/lib/async/wait' import {isBlockedOrBlocking, isMuted} from '#/lib/moderation/blocked-and-muted' import {logger} from '#/logger' import {isWeb} from '#/platform/detection' import {updateProfileShadow} from '#/state/cache/profile-shadow' import {useLanguagePrefs} from '#/state/preferences' import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useAgent, useSession} from '#/state/session' import {useOnboardingDispatch} from '#/state/shell' import {OnboardingControls} from '#/screens/Onboarding/Layout' import { Context, popularInterests, useInterestsDisplayNames, } from '#/screens/Onboarding/state' import {useSuggestedUsers} from '#/screens/Search/util/useSuggestedUsers' import {atoms as a, tokens, useBreakpoints, useTheme} from '#/alf' import {Admonition} from '#/components/Admonition' import {Button, ButtonIcon, ButtonText} from '#/components/Button' import {ArrowRotateCounterClockwise_Stroke2_Corner0_Rounded as ArrowRotateCounterClockwiseIcon} from '#/components/icons/ArrowRotateCounterClockwise' import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/Plus' import {boostInterests, InterestTabs} from '#/components/InterestTabs' import {Loader} from '#/components/Loader' import * as ProfileCard from '#/components/ProfileCard' import * as toast from '#/components/Toast' import {Text} from '#/components/Typography' import type * as bsky from '#/types/bsky' import {bulkWriteFollows} from '../util' export function StepSuggestedAccounts() { const {_} = useLingui() const t = useTheme() const {gtMobile} = useBreakpoints() const moderationOpts = useModerationOpts() const agent = useAgent() const {currentAccount} = useSession() const queryClient = useQueryClient() const {state, dispatch} = useContext(Context) const onboardDispatch = useOnboardingDispatch() const [selectedInterest, setSelectedInterest] = useState(null) // keeping track of who was followed via the follow all button // so we can enable/disable the button without having to dig through the shadow cache const [followedUsers, setFollowedUsers] = useState([]) /* * Special language handling copied wholesale from the Explore screen */ const {contentLanguages} = useLanguagePrefs() const useFullExperience = useMemo(() => { if (contentLanguages.length === 0) return true return bcp47Match.basicFilter('en', contentLanguages).length > 0 }, [contentLanguages]) const interestsDisplayNames = useInterestsDisplayNames() const interests = Object.keys(interestsDisplayNames) .sort(boostInterests(popularInterests)) .sort(boostInterests(state.interestsStepResults.selectedInterests)) const { data: suggestedUsers, isLoading, error, isRefetching, refetch, } = useSuggestedUsers({ category: selectedInterest || (useFullExperience ? null : interests[0]), search: !useFullExperience, overrideInterests: state.interestsStepResults.selectedInterests, }) const isError = !!error const skipOnboarding = useCallback(() => { onboardDispatch({type: 'finish'}) dispatch({type: 'finish'}) }, [onboardDispatch, dispatch]) const followableDids = suggestedUsers?.actors .filter( user => user.did !== currentAccount?.did && !isBlockedOrBlocking(user) && !isMuted(user) && !user.viewer?.following && !followedUsers.includes(user.did), ) .map(user => user.did) ?? [] const {mutate: followAll, isPending: isFollowingAll} = useMutation({ onMutate: () => { logger.metric('onboarding:suggestedAccounts:followAllPressed', { tab: selectedInterest ?? 'all', numAccounts: followableDids.length, }) }, mutationFn: async () => { for (const did of followableDids) { updateProfileShadow(queryClient, did, { followingUri: 'pending', }) } const uris = await wait(1e3, bulkWriteFollows(agent, followableDids)) for (const did of followableDids) { const uri = uris.get(did) updateProfileShadow(queryClient, did, { followingUri: uri, }) } return followableDids }, onSuccess: newlyFollowed => { toast.show(_(msg`Followed all accounts!`), {type: 'success'}) setFollowedUsers(followed => [...followed, ...newlyFollowed]) }, onError: () => { toast.show( _(msg`Failed to follow all suggested accounts, please try again`), {type: 'error'}, ) }, }) const canFollowAll = followableDids.length > 0 && !isFollowingAll return ( Suggested for you {isLoading || !moderationOpts ? ( ) : isError ? ( An error occurred while fetching suggested accounts. ) : ( {suggestedUsers?.actors.map((user, index) => ( ))} )} {isError ? ( ) : ( )} ) } function TabBar({ selectedInterest, onSelectInterest, selectedInterests, hideDefaultTab, defaultTabLabel, }: { selectedInterest: string | null onSelectInterest: (interest: string | null) => void selectedInterests: string[] hideDefaultTab?: boolean defaultTabLabel?: string }) { const {_} = useLingui() const interestsDisplayNames = useInterestsDisplayNames() const interests = Object.keys(interestsDisplayNames) .sort(boostInterests(popularInterests)) .sort(boostInterests(selectedInterests)) return ( { logger.metric( 'onboarding:suggestedAccounts:tabPressed', {tab: tab}, {statsig: true}, ) onSelectInterest(tab === 'all' ? null : tab) }} interestsDisplayNames={ hideDefaultTab ? interestsDisplayNames : { all: defaultTabLabel || _(msg`For You`), ...interestsDisplayNames, } } gutterWidth={isWeb ? 0 : tokens.space.xl} /> ) } function SuggestedProfileCard({ profile, moderationOpts, position, }: { profile: bsky.profile.AnyProfileView moderationOpts: ModerationOpts position: number }) { const t = useTheme() return ( { logger.metric( 'suggestedUser:follow', { logContext: 'Onboarding', location: 'Card', recId: undefined, position, }, {statsig: true}, ) }} /> ) }