import {useMemo} from 'react' import {type GestureResponderEvent, View} from 'react-native' import { moderateProfile, type ModerationOpts, RichText as RichTextApi, } from '@atproto/api' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useActorStatus} from '#/lib/actor-status' import {getModerationCauseKey} from '#/lib/moderation' import {type LogEvents} from '#/lib/statsig/statsig' import {forceLTR} from '#/lib/strings/bidi' import {NON_BREAKING_SPACE} from '#/lib/strings/constants' 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 * as Toast from '#/view/com/util/Toast' import {PreviewableUserAvatar, UserAvatar} from '#/view/com/util/UserAvatar' import { atoms as a, platform, type TextStyleProp, useTheme, type ViewStyleProp, } from '#/alf' import { Button, ButtonIcon, type 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, type LinkProps} from '#/components/Link' import * as Pills from '#/components/Pills' import {RichText} from '#/components/RichText' import {Text} from '#/components/Typography' import {useSimpleVerificationState} from '#/components/verification' import {VerificationCheck} from '#/components/verification/VerificationCheck' import type * as bsky from '#/types/bsky' export function Default({ profile, moderationOpts, logContext = 'ProfileCard', testID, }: { profile: bsky.profile.AnyProfileView moderationOpts: ModerationOpts logContext?: 'ProfileCard' | 'StarterPackProfilesList' testID?: string }) { return ( ) } export function Card({ profile, moderationOpts, logContext = 'ProfileCard', }: { profile: bsky.profile.AnyProfileView moderationOpts: ModerationOpts logContext?: 'ProfileCard' | 'StarterPackProfilesList' }) { 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 {_} = useLingui() 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 verification = useSimpleVerificationState({profile}) const moderation = moderateProfile(profile, moderationOpts) const name = sanitizeDisplayName( profile.displayName || sanitizeHandle(profile.handle), moderation.ui('displayName'), ) const handle = sanitizeHandle(profile.handle, '@') return ( {forceLTR(name)} {verification.showBadge && ( )} {NON_BREAKING_SPACE + handle} ) } export function Name({ profile, moderationOpts, }: { profile: bsky.profile.AnyProfileView moderationOpts: ModerationOpts }) { const moderation = moderateProfile(profile, moderationOpts) const name = sanitizeDisplayName( profile.displayName || sanitizeHandle(profile.handle), moderation.ui('displayName'), ) const verification = useSimpleVerificationState({profile}) return ( {name} {verification.showBadge && ( )} ) } export function Handle({profile}: {profile: bsky.profile.AnyProfileView}) { const t = useTheme() 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: LogEvents['profile:follow']['logContext'] & LogEvents['profile:unfollow']['logContext'] colorInverted?: boolean onFollow?: () => void withIcon?: boolean } & 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, ...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: profile.viewer?.followedBy ? 'Follow Back' : '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 ? ( ) : ( )} ) } 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 if (!followedBy && !modui.inform && !modui.alert) { return null } return ( {followedBy && } {modui.alerts.map(alert => ( ))} {modui.informs.map(inform => ( ))} ) }