import {memo, useMemo, useState} from 'react' import {View} from 'react-native' import {type AppBskyActorDefs} from '@atproto/api' import { type ModerationDecision, type ModerationOpts, } from '@bsky.app/sdk/moderation' import {type RichText as RichTextAPI} from '@bsky.app/sdk/richtext' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' import {useHaptics} from '#/lib/haptics' import {moderateProfile} from '#/lib/moderation/subjects' import {sanitizeDisplayName} from '#/lib/strings/display-names' import {logger} from '#/logger' import {type Shadow, useProfileShadow} from '#/state/cache/profile-shadow' import { useProfileBlockMutationQueue, useProfileFollowMutationQueue, } from '#/state/queries/profile' import {useRequireAuth, useSession} from '#/state/session' import {ProfileMenu} from '#/view/com/profile/ProfileMenu' import {atoms as a, platform} from '#/alf' import {SubscribeProfileButton} from '#/components/activity-notifications/SubscribeProfileButton' import {Button, ButtonIcon, ButtonText} from '#/components/Button' import {DebugFieldDisplay} from '#/components/DebugFieldDisplay' import {useDialogControl} from '#/components/Dialog' import {MessageProfileButton} from '#/components/dms/MessageProfileButton' import {ArrowShareRight_Stroke2_Corner2_Rounded as ArrowShareRight} from '#/components/icons/ArrowShareRight' import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' import { KnownFollowers, shouldShowKnownFollowers, } from '#/components/KnownFollowers' import * as Prompt from '#/components/Prompt' import {RichText} from '#/components/RichText' import * as Toast from '#/components/Toast' import {useAnalytics} from '#/analytics' import {IS_IOS, IS_NATIVE} from '#/env' import {InviteFriendsDialog} from '#/features/inviteFriends' import {useActorStatus} from '#/features/liveNow' import {GermButton} from '../components/GermButton' import {ProfileHeaderDisplayName} from './DisplayName' import {EditProfileDialog} from './EditProfileDialog' import {ProfileHeaderHandle} from './Handle' import {ProfileHeaderMetrics} from './Metrics' import {ProfileHeaderShell} from './Shell' import {ProfileHeaderSuggestedFollows} from './SuggestedFollows' interface Props { profile: AppBskyActorDefs.ProfileViewDetailed descriptionRT: RichTextAPI | null moderationOpts: ModerationOpts hideBackButton?: boolean isPlaceholderProfile?: boolean } let ProfileHeaderStandard = ({ profile: profileUnshadowed, descriptionRT, moderationOpts, hideBackButton = false, isPlaceholderProfile, }: Props): React.ReactNode => { const profile = useProfileShadow(profileUnshadowed) const {currentAccount} = useSession() const {_} = useLingui() const moderation = useMemo( () => moderateProfile(profile, moderationOpts), [profile, moderationOpts], ) const [, queueUnblock] = useProfileBlockMutationQueue(profile) const unblockPromptControl = Prompt.usePromptControl() const [showSuggestedFollows, setShowSuggestedFollows] = useState(false) const [hasSeenAllSuggestedFollows, setHasSeenAllSuggestedFollows] = useState(false) const isBlockedUser = profile.viewer?.blocking || profile.viewer?.blockedBy || profile.viewer?.blockingByList const unblockAccount = async () => { try { await queueUnblock() Toast.show(_(msg({message: 'Account unblocked', context: 'toast'}))) } catch (err) { const e = err as Error if (e?.name !== 'AbortError') { logger.error('Failed to unblock account', {message: e}) Toast.show(_(msg`There was an issue! ${e.toString()}`), {type: 'error'}) } } } const onRequestHide = () => { setHasSeenAllSuggestedFollows(true) setShowSuggestedFollows(false) } const isMe = currentAccount?.did === profile.did const {isActive: live} = useActorStatus(profile) return ( <> setShowSuggestedFollows(true)} onUnfollow={() => setShowSuggestedFollows(false)} /> {!isPlaceholderProfile && !isBlockedUser && ( {descriptionRT && !moderation.ui('profileView').blur ? ( ) : undefined} {profile.associated?.germ && ( )} {!isMe && !isBlockedUser && shouldShowKnownFollowers(profile.viewer?.knownFollowers) && ( )} )} { void unblockAccount() }} confirmButtonCta={ profile.viewer?.blocking ? _(msg`Unblock`) : _(msg`Block`) } confirmButtonColor="negative" /> ) } ProfileHeaderStandard = memo(ProfileHeaderStandard) export {ProfileHeaderStandard} export function HeaderStandardButtons({ profile, moderation, moderationOpts, onFollow, onUnfollow, minimal, }: { profile: Shadow moderation: ModerationDecision moderationOpts: ModerationOpts onFollow?: () => void onUnfollow?: () => void minimal?: boolean }) { const {_} = useLingui() const ax = useAnalytics() const {hasSession, currentAccount} = useSession() const playHaptic = useHaptics() const requireAuth = useRequireAuth() const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue( profile, 'ProfileHeader', ) const [, queueUnblock] = useProfileBlockMutationQueue(profile) const editProfileControl = useDialogControl() const inviteFriendsControl = useDialogControl() const unblockPromptControl = Prompt.usePromptControl() const isMe = currentAccount?.did === profile.did const onPressFollow = () => { playHaptic() requireAuth(async () => { try { await queueFollow() onFollow?.() Toast.show( _( msg`Following ${sanitizeDisplayName( profile.displayName || profile.handle, moderation.ui('displayName'), )}`, ), ) } catch (err) { const e = err as Error if (e?.name !== 'AbortError') { logger.error('Failed to follow', {message: String(e)}) Toast.show(_(msg`There was an issue! ${e.toString()}`), { type: 'error', }) } } }) } const onPressUnfollow = () => { playHaptic() requireAuth(async () => { try { await queueUnfollow() onUnfollow?.() Toast.show( _( msg`No longer following ${sanitizeDisplayName( profile.displayName || profile.handle, moderation.ui('displayName'), )}`, ), {type: 'default'}, ) } catch (err) { const e = err as Error if (e?.name !== 'AbortError') { logger.error('Failed to unfollow', {message: String(e)}) Toast.show(_(msg`There was an issue! ${e.toString()}`), { type: 'error', }) } } }) } const unblockAccount = async () => { try { await queueUnblock() Toast.show(_(msg({message: 'Account unblocked', context: 'toast'}))) } catch (err) { const e = err as Error if (e?.name !== 'AbortError') { logger.error('Failed to unblock account', {message: e}) Toast.show(_(msg`There was an issue! ${e.toString()}`), {type: 'error'}) } } } const subscriptionsAllowed = useMemo(() => { switch (profile.associated?.activitySubscription?.allowSubscriptions) { case 'followers': case undefined: return !!profile.viewer?.following case 'mutuals': return !!profile.viewer?.following && !!profile.viewer.followedBy case 'none': default: return false } }, [profile]) return ( <> {isMe ? ( <> {/* Invite friends is a native-only share sheet (the dialog is a no-op on web), so gate the entry point to avoid a dead button. */} {IS_NATIVE && ( )} {IS_NATIVE && } ) : profile.viewer?.blocking ? ( profile.viewer?.blockingByList ? null : ( ) ) : !profile.viewer?.blockedBy ? ( <> {hasSession && (!minimal || profile.viewer?.following) && ( <> {subscriptionsAllowed && ( )} )} {(!minimal || !profile.viewer?.following) && ( )} ) : null} { void unblockAccount() }} confirmButtonCta={_(msg`Unblock`)} confirmButtonColor="negative" /> ) }