import React, {memo} from 'react' import {type AppBskyActorDefs} from '@atproto/api' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' import {useNavigation} from '@react-navigation/native' import {useQueryClient} from '@tanstack/react-query' import {HITSLOP_20} from '#/lib/constants' import {makeProfileLink} from '#/lib/routes/links' import {type NavigationProp} from '#/lib/routes/types' import {shareText, shareUrl} from '#/lib/sharing' import {toShareUrl, toShareUrlBsky} from '#/lib/strings/url-helpers' import {type Shadow} from '#/state/cache/types' import {useModalControls} from '#/state/modals' import { useDeerVerificationEnabled, useDeerVerificationTrusted, useSetDeerVerificationTrust, } from '#/state/preferences/deer-verification' import {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons' import {Nux, useNux, useSaveNux} from '#/state/queries/nuxs' import { RQKEY as profileQueryKey, useProfileBlockMutationQueue, useProfileFollowMutationQueue, useProfileMuteMutationQueue, } from '#/state/queries/profile' import {useSession} from '#/state/session' import {EventStopper} from '#/view/com/util/EventStopper' import * as Toast from '#/view/com/util/Toast' import {atoms as a, useTheme} from '#/alf' import {Button, ButtonIcon} from '#/components/Button' import {useDialogControl} from '#/components/Dialog' import {StarterPackDialog} from '#/components/dialogs/StarterPackDialog' import {ArrowOutOfBoxModified_Stroke2_Corner2_Rounded as ArrowOutOfBoxIcon} from '#/components/icons/ArrowOutOfBox' import {ChainLink_Stroke2_Corner0_Rounded as ChainLinkIcon} from '#/components/icons/ChainLink' import {CircleCheck_Stroke2_Corner0_Rounded as CircleCheckIcon} from '#/components/icons/CircleCheck' import {CircleX_Stroke2_Corner0_Rounded as CircleXIcon} from '#/components/icons/CircleX' import {Clipboard_Stroke2_Corner2_Rounded as ClipboardIcon} from '#/components/icons/Clipboard' import {DotGrid3x1_Stroke2_Corner0_Rounded as Ellipsis} from '#/components/icons/DotGrid' import {Flag_Stroke2_Corner0_Rounded as Flag} from '#/components/icons/Flag' import {ListSparkle_Stroke2_Corner0_Rounded as List} from '#/components/icons/ListSparkle' import {Live_Stroke2_Corner0_Rounded as LiveIcon} from '#/components/icons/Live' import {MagnifyingGlass_Stroke2_Corner0_Rounded as SearchIcon} from '#/components/icons/MagnifyingGlass' import {Mute_Stroke2_Corner0_Rounded as Mute} from '#/components/icons/Mute' import {PeopleRemove2_Stroke2_Corner0_Rounded as UserMinus} from '#/components/icons/PeopleRemove2' import { PersonCheck_Stroke2_Corner0_Rounded as PersonCheck, PersonX_Stroke2_Corner0_Rounded as PersonX, } from '#/components/icons/Person' import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as Unmute} from '#/components/icons/Speaker' import {StarterPack} from '#/components/icons/StarterPack' import * as Menu from '#/components/Menu' import { ReportDialog, useReportDialogControl, } from '#/components/moderation/ReportDialog' import * as Prompt from '#/components/Prompt' import {useFullVerificationState} from '#/components/verification' import {VerificationCreatePrompt} from '#/components/verification/VerificationCreatePrompt' import {VerificationRemovePrompt} from '#/components/verification/VerificationRemovePrompt' import {useAnalytics} from '#/analytics' import {IS_WEB} from '#/env' import {useActorStatus, useLiveNowConfig} from '#/features/liveNow' import {EditLiveDialog} from '#/features/liveNow/components/EditLiveDialog' import {GoLiveDialog} from '#/features/liveNow/components/GoLiveDialog' import {GoLiveDisabledDialog} from '#/features/liveNow/components/GoLiveDisabledDialog' import {Dot} from '#/features/nuxs/components/Dot' import {Gradient} from '#/features/nuxs/components/Gradient' import {useDevMode} from '#/storage/hooks/dev-mode' let ProfileMenu = ({ profile, }: { profile: Shadow }): React.ReactNode => { const t = useTheme() const ax = useAnalytics() const {_} = useLingui() const {currentAccount, hasSession} = useSession() const {openModal} = useModalControls() const reportDialogControl = useReportDialogControl() const queryClient = useQueryClient() const navigation = useNavigation() const isSelf = currentAccount?.did === profile.did const isFollowedBy = profile.viewer?.followedBy const isFollowing = profile.viewer?.following const isBlocked = profile.viewer?.blocking || profile.viewer?.blockedBy const isFollowingBlockedAccount = isFollowing && isBlocked const isLabelerAndNotBlocked = !!profile.associated?.labeler && !isBlocked const [devModeEnabled] = useDevMode() const verification = useFullVerificationState({profile}) const {canGoLive} = useLiveNowConfig() const status = useActorStatus(profile) const statusNudge = useNux(Nux.LiveNowBetaNudge) const statusNudgeActive = isSelf && canGoLive && statusNudge.status === 'ready' && !statusNudge.nux?.completed const {mutate: saveNux} = useSaveNux() const deerVerificationEnabled = useDeerVerificationEnabled() const deerVerificationTrusted = useDeerVerificationTrusted().has(profile.did) const setDeerVerificationTrust = useSetDeerVerificationTrust() const [queueMute, queueUnmute] = useProfileMuteMutationQueue(profile) const [queueBlock, queueUnblock] = useProfileBlockMutationQueue(profile) const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue( profile, 'ProfileMenu', ) const blockPromptControl = Prompt.usePromptControl() const loggedOutWarningPromptControl = Prompt.usePromptControl() const goLiveDialogControl = useDialogControl() const goLiveDisabledDialogControl = useDialogControl() const addToStarterPacksDialogControl = useDialogControl() const showLoggedOutWarning = React.useMemo(() => { return ( profile.did !== currentAccount?.did && !!profile.labels?.find(label => label.val === '!no-unauthenticated') ) }, [currentAccount, profile]) const invalidateProfileQuery = React.useCallback(() => { queryClient.invalidateQueries({ queryKey: profileQueryKey(profile.did), }) }, [queryClient, profile.did]) const onPressAddToStarterPacks = React.useCallback(() => { ax.metric('profile:addToStarterPack', {}) addToStarterPacksDialogControl.open() }, [addToStarterPacksDialogControl]) const onPressShare = React.useCallback(() => { shareUrl(toShareUrl(makeProfileLink(profile))) }, [profile]) const onPressShareBsky = React.useCallback(() => { shareUrl(toShareUrlBsky(makeProfileLink(profile))) }, [profile]) const onPressAddRemoveLists = React.useCallback(() => { openModal({ name: 'user-add-remove-lists', subject: profile.did, handle: profile.handle, displayName: profile.displayName || profile.handle, onAdd: invalidateProfileQuery, onRemove: invalidateProfileQuery, }) }, [profile, openModal, invalidateProfileQuery]) const onPressMuteAccount = React.useCallback(async () => { if (profile.viewer?.muted) { try { await queueUnmute() Toast.show(_(msg({message: 'Account unmuted', context: 'toast'}))) } catch (e: any) { if (e?.name !== 'AbortError') { ax.logger.error('Failed to unmute account', {message: e}) Toast.show(_(msg`There was an issue! ${e.toString()}`), 'xmark') } } } else { try { await queueMute() Toast.show(_(msg({message: 'Account muted', context: 'toast'}))) } catch (e: any) { if (e?.name !== 'AbortError') { ax.logger.error('Failed to mute account', {message: e}) Toast.show(_(msg`There was an issue! ${e.toString()}`), 'xmark') } } } }, [ax, profile.viewer?.muted, queueUnmute, _, queueMute]) const blockAccount = React.useCallback(async () => { if (profile.viewer?.blocking) { try { await queueUnblock() Toast.show(_(msg({message: 'Account unblocked', context: 'toast'}))) } catch (e: any) { if (e?.name !== 'AbortError') { ax.logger.error('Failed to unblock account', {message: e}) Toast.show(_(msg`There was an issue! ${e.toString()}`), 'xmark') } } } else { try { await queueBlock() Toast.show(_(msg({message: 'Account blocked', context: 'toast'}))) } catch (e: any) { if (e?.name !== 'AbortError') { ax.logger.error('Failed to block account', {message: e}) Toast.show(_(msg`There was an issue! ${e.toString()}`), 'xmark') } } } }, [ax, profile.viewer?.blocking, _, queueUnblock, queueBlock]) const onPressFollowAccount = React.useCallback(async () => { try { await queueFollow() Toast.show(_(msg({message: 'Account followed', context: 'toast'}))) } catch (e: any) { if (e?.name !== 'AbortError') { ax.logger.error('Failed to follow account', {message: e}) Toast.show(_(msg`There was an issue! ${e.toString()}`), 'xmark') } } }, [_, ax, queueFollow]) const onPressUnfollowAccount = React.useCallback(async () => { try { await queueUnfollow() Toast.show(_(msg({message: 'Account unfollowed', context: 'toast'}))) } catch (e: any) { if (e?.name !== 'AbortError') { ax.logger.error('Failed to unfollow account', {message: e}) Toast.show(_(msg`There was an issue! ${e.toString()}`), 'xmark') } } }, [_, ax, queueUnfollow]) const onPressReportAccount = React.useCallback(() => { reportDialogControl.open() }, [reportDialogControl]) const onPressShareATUri = React.useCallback(() => { shareText(`at://${profile.did}`) }, [profile.did]) const onPressShareDID = React.useCallback(() => { shareText(profile.did) }, [profile.did]) const onPressSearch = React.useCallback(() => { navigation.navigate('ProfileSearch', {name: profile.handle}) }, [navigation, profile.handle]) const verificationCreatePromptControl = Prompt.usePromptControl() const verificationRemovePromptControl = Prompt.usePromptControl() const currentAccountVerifications = profile.verification?.verifications?.filter(v => { return v.issuer === currentAccount?.did }) ?? [] const enableSquareButtons = useEnableSquareButtons() return ( {({props}) => { return ( <> {statusNudgeActive && } ) }} { if (showLoggedOutWarning) { loggedOutWarningPromptControl.open() } else { onPressShare() } }}> {IS_WEB ? ( Copy link to profile ) : ( Share via... )} { if (showLoggedOutWarning) { loggedOutWarningPromptControl.open() } else { onPressShareBsky() } }}> {IS_WEB ? ( Copy via bsky.app ) : ( Share via bsky.app... )} Search posts {hasSession && ( <> {!isSelf && ( <> {(isLabelerAndNotBlocked || isFollowingBlockedAccount) && ( {isFollowing ? ( isFollowedBy ? ( Divorce mutual ) : ( Unfollow account ) ) : ( Follow account )} )} )} {!isSelf && ( Add to starter packs )} Add to lists {!isSelf && deerVerificationEnabled && (deerVerificationTrusted ? ( setDeerVerificationTrust.remove(profile.did) }> Remove trust ) : ( setDeerVerificationTrust.add(profile.did)}> Trust verifier ))} {isSelf && canGoLive && ( { if (status.isDisabled) { goLiveDisabledDialogControl.open() } else { goLiveDialogControl.open() } saveNux({ id: Nux.LiveNowBetaNudge, data: undefined, completed: true, }) }}> {statusNudgeActive && } {status.isDisabled ? ( Go live (disabled) ) : status.isActive ? ( Edit live status ) : ( Go live )} {statusNudgeActive && ( New )} t.palette.primary_500 : undefined } /> )} {verification.viewer.role === 'verifier' && !verification.profile.isViewer && (verification.viewer.hasIssuedVerification ? ( verificationRemovePromptControl.open()}> Remove verification ) : ( verificationCreatePromptControl.open()}> Verify account ))} {!isSelf && ( <> {!profile.viewer?.blocking && !profile.viewer?.mutedByList && ( {profile.viewer?.muted ? ( Unmute account ) : ( Mute account )} )} {!profile.viewer?.blockingByList && ( blockPromptControl.open()}> {profile.viewer?.blocking ? ( Unblock account ) : ( Block account )} )} Report account )} )} {devModeEnabled ? ( <> Copy at:// URI Copy DID ) : null} {status.isDisabled ? ( ) : status.isActive ? ( ) : ( )} ) } ProfileMenu = memo(ProfileMenu) export {ProfileMenu}