diff --git a/src/components/ProfileCard.tsx b/src/components/ProfileCard.tsx
index a0d22285..a6ca7627 100644
--- a/src/components/ProfileCard.tsx
+++ b/src/components/ProfileCard.tsx
@@ -1,20 +1,32 @@
import React from 'react'
-import {View} from 'react-native'
-import {AppBskyActorDefs, moderateProfile, ModerationOpts} from '@atproto/api'
+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 {createSanitizedDisplayName} from 'lib/moderation/create-sanitized-display-name'
+import {sanitizeDisplayName} from '#/lib/strings/display-names'
+import {useProfileFollowMutationQueue} from '#/state/queries/profile'
import {sanitizeHandle} from 'lib/strings/handles'
import {useProfileShadow} from 'state/cache/profile-shadow'
import {useSession} from 'state/session'
-import {FollowButton} from 'view/com/profile/FollowButton'
+import * as Toast from '#/view/com/util/Toast'
import {ProfileCardPills} from 'view/com/profile/ProfileCard'
import {UserAvatar} from 'view/com/util/UserAvatar'
import {atoms as a, useTheme} from '#/alf'
-import {Link} from '#/components/Link'
+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: profileUnshadowed,
+ profile,
moderationOpts,
logContext = 'ProfileCard',
}: {
@@ -22,70 +34,249 @@ export function Default({
moderationOpts: ModerationOpts
logContext?: 'ProfileCard' | 'StarterPackProfilesList'
}) {
- const t = useTheme()
- const {currentAccount, hasSession} = useSession()
-
- const profile = useProfileShadow(profileUnshadowed)
- const name = createSanitizedDisplayName(profile)
- const handle = `@${sanitizeHandle(profile.handle)}`
- const moderation = moderateProfile(profile, moderationOpts)
-
return (
-
-
-
-
-
- {name}
-
-
- {handle}
-
-
- {hasSession && profile.did !== currentAccount?.did && (
-
-
-
- )}
-
-
-
-
- {profile.description && (
-
- {profile.description}
-
- )}
-
+
+
+
)
}
-function Wrapper({did, children}: {did: string; children: React.ReactNode}) {
+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({did, children}: {did: string} & Omit) {
+ return (
+
- {children}
-
+ {children}
+
+ )
+}
+
+export function Avatar({
+ profile,
+ moderationOpts,
+}: {
+ profile: AppBskyActorDefs.ProfileViewDetailed
+ moderationOpts: ModerationOpts
+}) {
+ const moderation = moderateProfile(profile, moderationOpts)
+
+ 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 Description({
+ profile: profileUnshadowed,
+}: {
+ profile: AppBskyActorDefs.ProfileViewDetailed
+}) {
+ 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 type FollowButtonProps = {
+ profile: AppBskyActorDefs.ProfileViewBasic
+ logContext: 'ProfileCard' | 'StarterPackProfilesList'
+} & 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,
+ logContext,
+ ...rest
+}: FollowButtonProps) {
+ const {_} = useLingui()
+ const profile = useProfileShadow(profileUnshadowed)
+ 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()
+ } catch (e: any) {
+ if (e?.name !== 'AbortError') {
+ Toast.show(_(msg`An issue occurred, please try again.`))
+ }
+ }
+ }
+
+ const onPressUnfollow = async (e: GestureResponderEvent) => {
+ e.preventDefault()
+ e.stopPropagation()
+ try {
+ await queueUnfollow()
+ } catch (e: any) {
+ if (e?.name !== 'AbortError') {
+ Toast.show(_(msg`An issue occurred, please try again.`))
+ }
+ }
+ }
+
+ 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 ? (
+
+ ) : (
+
+ )}
+
)
}