import React from 'react' import {GestureResponderEvent, View} from 'react-native' import { AppBskyActorDefs, moderateProfile, ModerationOpts, RichText as RichTextApi, } from '@atproto/api' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {LogEvents} from '#/lib/statsig/statsig' import {sanitizeDisplayName} from '#/lib/strings/display-names' import {sanitizeHandle} from '#/lib/strings/handles' import {useProfileShadow} from '#/state/cache/profile-shadow' import {useProfileFollowMutationQueue} from '#/state/queries/profile' import {useSession} from '#/state/session' import {ProfileCardPills} from '#/view/com/profile/ProfileCard' import * as Toast from '#/view/com/util/Toast' import {UserAvatar} from '#/view/com/util/UserAvatar' import {atoms as a, useTheme} from '#/alf' import {Button, ButtonIcon, ButtonProps, ButtonText} from '#/components/Button' import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check' import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' import {Link as InternalLink, LinkProps} from '#/components/Link' import {RichText} from '#/components/RichText' import {Text} from '#/components/Typography' export function Default({ profile, moderationOpts, logContext = 'ProfileCard', }: { profile: AppBskyActorDefs.ProfileViewDetailed moderationOpts: ModerationOpts logContext?: 'ProfileCard' | 'StarterPackProfilesList' }) { return ( ) } export function Card({ profile, moderationOpts, logContext = 'ProfileCard', }: { profile: AppBskyActorDefs.ProfileViewDetailed moderationOpts: ModerationOpts logContext?: 'ProfileCard' | 'StarterPackProfilesList' }) { const moderation = moderateProfile(profile, moderationOpts) return (
) } export function Outer({ children, }: { children: React.ReactElement | React.ReactElement[] }) { return {children} } export function Header({ children, }: { children: React.ReactElement | React.ReactElement[] }) { return {children} } export function Link({ profile, children, style, ...rest }: { profile: AppBskyActorDefs.ProfileViewDetailed } & Omit) { const {_} = useLingui() return ( {children} ) } export function Avatar({ profile, moderationOpts, }: { profile: AppBskyActorDefs.ProfileViewDetailed moderationOpts: ModerationOpts }) { const moderation = moderateProfile(profile, moderationOpts) return ( ) } export function AvatarPlaceholder() { const t = useTheme() return ( ) } export function NameAndHandle({ profile, moderationOpts, }: { profile: AppBskyActorDefs.ProfileViewDetailed moderationOpts: ModerationOpts }) { const t = useTheme() const moderation = moderateProfile(profile, moderationOpts) const name = sanitizeDisplayName( profile.displayName || sanitizeHandle(profile.handle), moderation.ui('displayName'), ) const handle = sanitizeHandle(profile.handle, '@') return ( {name} {handle} ) } export function NameAndHandlePlaceholder() { const t = useTheme() return ( ) } export function Description({ profile: profileUnshadowed, numberOfLines = 3, }: { profile: AppBskyActorDefs.ProfileViewDetailed numberOfLines?: number }) { const profile = useProfileShadow(profileUnshadowed) const {description} = profile const rt = React.useMemo(() => { if (!description) return const rt = new RichTextApi({text: description || ''}) rt.detectFacetsWithoutResolution() return rt }, [description]) 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: AppBskyActorDefs.ProfileViewBasic moderationOpts: ModerationOpts logContext: LogEvents['profile:follow']['logContext'] & LogEvents['profile:unfollow']['logContext'] colorInverted?: boolean onFollow?: () => void } & 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, ...rest }: FollowButtonProps) { const {_} = useLingui() const profile = useProfileShadow(profileUnshadowed) const moderation = moderateProfile(profile, moderationOpts) const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue( profile, logContext, ) const isRound = Boolean(rest.shape && rest.shape === 'round') const onPressFollow = async (e: GestureResponderEvent) => { e.preventDefault() e.stopPropagation() try { await queueFollow() Toast.show( _( msg`Following ${sanitizeDisplayName( profile.displayName || profile.handle, moderation.ui('displayName'), )}`, ), ) onPressProp?.(e) onFollow?.() } catch (err: any) { if (err?.name !== 'AbortError') { Toast.show(_(msg`An issue occurred, please try again.`), 'xmark') } } } const onPressUnfollow = async (e: GestureResponderEvent) => { e.preventDefault() e.stopPropagation() try { await queueUnfollow() Toast.show( _( msg`No longer following ${sanitizeDisplayName( profile.displayName || profile.handle, moderation.ui('displayName'), )}`, ), ) onPressProp?.(e) } catch (err: any) { if (err?.name !== 'AbortError') { Toast.show(_(msg`An issue occurred, please try again.`), 'xmark') } } } const unfollowLabel = _( msg({ message: 'Following', comment: 'User is following this account, click to unfollow', }), ) const followLabel = _( msg({ message: 'Follow', comment: 'User is not following this account, click to follow', }), ) if (!profile.viewer) return null if ( profile.viewer.blockedBy || profile.viewer.blocking || profile.viewer.blockingByList ) return null return ( {profile.viewer.following ? ( ) : ( )} ) }