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: '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 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 => (
))}
)
}