import {useMemo, useState} from 'react' import { type GestureResponderEvent, type StyleProp, type TextStyle, View, type ViewStyle, } from 'react-native' import { moderateProfile, type ModerationOpts, RichText as RichTextApi, } from '@atproto/api' import {useLingui} from '@lingui/react/macro' import {getModerationCauseKey} from '#/lib/moderation' import {makeProfileLink} from '#/lib/routes/links' import {forceLTR} from '#/lib/strings/bidi' import {NON_BREAKING_SPACE} from '#/lib/strings/constants' import {getAuthorPrimaryName} from '#/lib/strings/display-names' import {sanitizeHandle} from '#/lib/strings/handles' import {useProfileShadow} from '#/state/cache/profile-shadow' import {useConfirmFollowUnfollow} from '#/state/preferences/confirm-follow-unfollow' import {useHideDisplayNames} from '#/state/preferences/hide-display-names' import {useShowFollowsYouBadge} from '#/state/preferences/show-follows-you-badge' import {useProfileFollowMutationQueue} from '#/state/queries/profile' import {type SessionAccount, useSession} from '#/state/session' import {PreviewableUserAvatar, UserAvatar} from '#/view/com/util/UserAvatar' import { atoms as a, platform, type TextStyleProp, useTheme, type ViewStyleProp, web, } from '#/alf' import { Button, ButtonIcon, type ButtonProps, ButtonText, } from '#/components/Button' import {FollowConfirmationDialog} from '#/components/dialogs/FollowConfirmationDialog' import {EphemeralAccountSwitcher} from '#/components/EphemeralAccountSwitcher' import { Check_Stroke2_Corner0_Rounded as Check, DoubleCheck_Stroke2_Corner0_Rounded as DoubleCheck, } from '#/components/icons/Check' import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' import {Link as InternalLink, type LinkProps} from '#/components/Link' import * as Pills from '#/components/Pills' import {ProfileBadges} from '#/components/ProfileBadges' import * as Prompt from '#/components/Prompt' import {RichText} from '#/components/RichText' import * as Toast from '#/components/Toast' import {Text} from '#/components/Typography' import {type Metrics} from '#/analytics' import {useActorStatus} from '#/features/liveNow' import type * as bsky from '#/types/bsky' import { useEphemeralFollowAction, useEphemeralFollowIntent, } from './hooks/useEphemeralFollowAction' export function Default({ profile, moderationOpts, logContext = 'ProfileCard', testID, position, contextProfileDid, onPress, }: { profile: bsky.profile.AnyProfileView moderationOpts: ModerationOpts logContext?: 'ProfileCard' | 'StarterPackProfilesList' testID?: string position?: number contextProfileDid?: string onPress?: (e: GestureResponderEvent) => void }) { return ( ) } export function Card({ profile, moderationOpts, logContext = 'ProfileCard', position, contextProfileDid, }: { profile: bsky.profile.AnyProfileView moderationOpts: ModerationOpts logContext?: 'ProfileCard' | 'StarterPackProfilesList' position?: number contextProfileDid?: string }) { return (
) } export function Outer({ children, }: { children: React.ReactNode | React.ReactNode[] }) { return {children} } export function Header({ children, }: { children: React.ReactNode | React.ReactNode[] }) { return {children} } export function Link({ profile, children, style, ...rest }: { profile: bsky.profile.AnyProfileView } & Omit) { const {t: l} = useLingui() const hideDisplayNames = useHideDisplayNames() const profileURL = makeProfileLink({ did: profile.did, handle: profile.handle, }) return ( {children} ) } export function Avatar({ profile, moderationOpts, onPress, disabledPreview, liveOverride, size = 40, }: { profile: bsky.profile.AnyProfileView moderationOpts: ModerationOpts onPress?: () => void disabledPreview?: boolean liveOverride?: boolean size?: number }) { const moderation = moderateProfile(profile, moderationOpts) const {isActive: live} = useActorStatus(profile) return disabledPreview ? ( ) : ( ) } export function AvatarPlaceholder({size = 40}: {size?: number}) { const t = useTheme() return ( ) } export function NameAndHandle({ profile, moderationOpts, inline = false, }: { profile: bsky.profile.AnyProfileView moderationOpts: ModerationOpts inline?: boolean }) { if (inline) { return ( ) } else { return ( ) } } function InlineNameAndHandle({ profile, moderationOpts, }: { profile: bsky.profile.AnyProfileView moderationOpts: ModerationOpts }) { const t = useTheme() const hideDisplayNames = useHideDisplayNames() const moderation = moderateProfile(profile, moderationOpts) const name = getAuthorPrimaryName(profile, { hideDisplayNames, moderation: moderation.ui('displayName'), }) const handle = sanitizeHandle(profile.handle, '@') return ( {forceLTR(name)} {!hideDisplayNames && ( {NON_BREAKING_SPACE + handle} )} ) } export function Name({ profile, moderationOpts, style, textStyle, }: { profile: bsky.profile.AnyProfileView moderationOpts: ModerationOpts style?: StyleProp textStyle?: StyleProp }) { const hideDisplayNames = useHideDisplayNames() const moderation = moderateProfile(profile, moderationOpts) const name = getAuthorPrimaryName(profile, { hideDisplayNames, moderation: moderation.ui('displayName'), }) return ( {name} ) } export function Handle({ profile, textStyle, }: { profile: bsky.profile.AnyProfileView textStyle?: StyleProp }) { const t = useTheme() const hideDisplayNames = useHideDisplayNames() if (hideDisplayNames) { return null } const handle = sanitizeHandle(profile.handle, '@') return ( {handle} ) } export function NameAndHandlePlaceholder() { const t = useTheme() return ( ) } export function NamePlaceholder({style}: ViewStyleProp) { const t = useTheme() return ( ) } export function Description({ profile: profileUnshadowed, numberOfLines = 3, style, }: { profile: bsky.profile.AnyProfileView numberOfLines?: number } & TextStyleProp) { const profile = useProfileShadow(profileUnshadowed) const rt = useMemo(() => { if (!('description' in profile)) return const rt = new RichTextApi({text: profile.description || ''}) rt.detectFacetsWithoutResolution() return rt }, [profile]) if (!rt) return null if ( profile.viewer && (profile.viewer.blockedBy || profile.viewer.blocking || profile.viewer.blockingByList) ) return null return ( ) } export function DescriptionPlaceholder({ numberOfLines = 3, }: { numberOfLines?: number }) { const t = useTheme() return ( {Array(numberOfLines) .fill(0) .map((_, i) => ( ))} ) } export type FollowButtonProps = { profile: bsky.profile.AnyProfileView moderationOpts: ModerationOpts logContext: Metrics['profile:follow']['logContext'] & Metrics['profile:unfollow']['logContext'] colorInverted?: boolean onFollow?: () => void withIcon?: boolean position?: number contextProfileDid?: string } & Partial export function FollowButton(props: FollowButtonProps) { const {currentAccount, hasSession} = useSession() const isMe = props.profile.did === currentAccount?.did return hasSession && !isMe ? : null } export function FollowButtonInner({ profile: profileUnshadowed, moderationOpts, logContext, onPress: onPressProp, onFollow, colorInverted, withIcon = true, position, contextProfileDid, ...rest }: FollowButtonProps) { const {currentAccount, accounts} = useSession() const {t: l} = useLingui() const profile = useProfileShadow(profileUnshadowed) const moderation = moderateProfile(profile, moderationOpts) const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue( profile, logContext, position, contextProfileDid, ) const isRound = Boolean(rest.shape && rest.shape === 'round') const onSelectEphemeralAccount = useEphemeralFollowAction({ profile, logContext, onFollow, }) const getEphemeralFollowAction = useEphemeralFollowIntent({profile}) const hasAlternateAccounts = accounts.some( account => account.did !== currentAccount?.did, ) const confirmFollowUnfollow = useConfirmFollowUnfollow() const hideDisplayNames = useHideDisplayNames() const authorName = getAuthorPrimaryName(profile, { hideDisplayNames, moderation: moderation.ui('displayName'), }) const promptControl = Prompt.usePromptControl() const [confirmationAction, setConfirmationAction] = useState< 'follow' | 'unfollow' >('follow') const [pendingEphemeralAccount, setPendingEphemeralAccount] = useState(null) const executeFollow = async (e: GestureResponderEvent) => { try { await queueFollow() Toast.show(l`Following ${authorName}`) onPressProp?.(e) onFollow?.() } catch (e) { const err = e as Error if (err?.name !== 'AbortError') { Toast.show(l`An issue occurred, please try again.`, { type: 'error', }) } } } const executeUnfollow = async (e: GestureResponderEvent) => { try { await queueUnfollow() Toast.show(l`No longer following ${authorName}`) onPressProp?.(e) } catch (e) { const err = e as Error if (err?.name !== 'AbortError') { Toast.show(l`An issue occurred, please try again.`, { type: 'error', }) } } } const onPressFollow = (e: GestureResponderEvent) => { e.preventDefault() e.stopPropagation() if (confirmFollowUnfollow) { setConfirmationAction('follow') promptControl.open() } else { void executeFollow(e) } } const onPressUnfollow = (e: GestureResponderEvent) => { e.preventDefault() e.stopPropagation() if (confirmFollowUnfollow) { setConfirmationAction('unfollow') promptControl.open() } else { void executeUnfollow(e) } } const onConfirm = (e: GestureResponderEvent) => { if (pendingEphemeralAccount) { void onSelectEphemeralAccount(pendingEphemeralAccount) setPendingEphemeralAccount(null) } else if (confirmationAction === 'follow') { void executeFollow(e) } else if (confirmationAction === 'unfollow') { void executeUnfollow(e) } } const unfollowLabel = profile.viewer?.followedBy ? l({ message: 'Mutuals', comment: 'User is following this account, click to unfollow', }) : l({ message: 'Following', comment: 'User is following this account, click to unfollow', }) const followLabel = profile.viewer?.followedBy ? l({ message: 'Follow back', comment: 'User is not following this account, click to follow back', }) : l({ message: 'Follow', comment: 'User is not following this account, click to follow', }) if (!profile.viewer) return null const viewer = profile.viewer const renderFollowButton = (onLongPress?: () => void) => viewer.following ? ( ) : ( ) return ( {currentAccount && hasAlternateAccounts ? ( { if (confirmFollowUnfollow) { setPendingEphemeralAccount(account) void (async () => { const action = await getEphemeralFollowAction(account) setConfirmationAction(action) promptControl.open() })() } else { void onSelectEphemeralAccount(account) } }} renderTrigger={({triggerProps}) => renderFollowButton(triggerProps.onLongPress) } /> ) : ( renderFollowButton() )} {confirmFollowUnfollow && ( )} ) } export function FollowButtonPlaceholder({style}: ViewStyleProp) { const t = useTheme() return ( ) } export function Labels({ profile, moderationOpts, }: { profile: bsky.profile.AnyProfileView moderationOpts: ModerationOpts }) { const moderation = moderateProfile(profile, moderationOpts) const modui = moderation.ui('profileList') const followedBy = profile.viewer?.followedBy const showFollowsYouBadge = useShowFollowsYouBadge() const mutedOnlyReposts = profile.viewer?.mutedOnlyReposts if ( !(followedBy && showFollowsYouBadge) && !mutedOnlyReposts && !modui.inform && !modui.alert ) { return null } return ( {followedBy && showFollowsYouBadge && } {modui.alerts.map(alert => ( ))} {modui.informs.map(inform => ( ))} {mutedOnlyReposts && } ) }