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 ? ( + + ) : ( + + )} + ) }