import React, {memo, useMemo} from 'react' import { StyleSheet, TouchableOpacity, TouchableWithoutFeedback, View, } from 'react-native' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {useNavigation} from '@react-navigation/native' import { AppBskyActorDefs, ModerationOpts, moderateProfile, RichText as RichTextAPI, } from '@atproto/api' import {Trans, msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {NavigationProp} from 'lib/routes/types' import {isNative} from 'platform/detection' import {BlurView} from '../util/BlurView' import * as Toast from '../util/Toast' import {LoadingPlaceholder} from '../util/LoadingPlaceholder' import {Text} from '../util/text/Text' import {ThemedText} from '../util/text/ThemedText' import {RichText} from '#/components/RichText' import {UserAvatar} from '../util/UserAvatar' import {UserBanner} from '../util/UserBanner' import {ProfileHeaderAlerts} from '../util/moderation/ProfileHeaderAlerts' import {formatCount} from '../util/numeric/format' import {Link} from '../util/Link' import {ProfileHeaderSuggestedFollows} from './ProfileHeaderSuggestedFollows' import {useModalControls} from '#/state/modals' import {useLightboxControls, ProfileImageLightbox} from '#/state/lightbox' import { useProfileBlockMutationQueue, useProfileFollowMutationQueue, } from '#/state/queries/profile' import {usePalette} from 'lib/hooks/usePalette' import {useAnalytics} from 'lib/analytics/analytics' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {BACK_HITSLOP} from 'lib/constants' import {isInvalidHandle, sanitizeHandle} from 'lib/strings/handles' import {makeProfileLink} from 'lib/routes/links' import {pluralize} from 'lib/strings/helpers' import {sanitizeDisplayName} from 'lib/strings/display-names' import {s, colors} from 'lib/styles' import {logger} from '#/logger' import {useSession} from '#/state/session' import {Shadow} from '#/state/cache/types' import {useRequireAuth} from '#/state/session' import {LabelInfo} from '../util/moderation/LabelInfo' import {useProfileShadow} from 'state/cache/profile-shadow' import {atoms as a} from '#/alf' import {ProfileMenu} from 'view/com/profile/ProfileMenu' import * as Prompt from '#/components/Prompt' let ProfileHeaderLoading = (_props: {}): React.ReactNode => { const pal = usePalette('default') return ( ) } ProfileHeaderLoading = memo(ProfileHeaderLoading) export {ProfileHeaderLoading} interface Props { profile: AppBskyActorDefs.ProfileViewDetailed descriptionRT: RichTextAPI | null moderationOpts: ModerationOpts hideBackButton?: boolean isPlaceholderProfile?: boolean } let ProfileHeader = ({ profile: profileUnshadowed, descriptionRT, moderationOpts, hideBackButton = false, isPlaceholderProfile, }: Props): React.ReactNode => { const profile: Shadow = useProfileShadow(profileUnshadowed) const pal = usePalette('default') const palInverted = usePalette('inverted') const {currentAccount, hasSession} = useSession() const requireAuth = useRequireAuth() const {_} = useLingui() const {openModal} = useModalControls() const {openLightbox} = useLightboxControls() const navigation = useNavigation() const {track} = useAnalytics() const invalidHandle = isInvalidHandle(profile.handle) const {isDesktop} = useWebMediaQueries() const [showSuggestedFollows, setShowSuggestedFollows] = React.useState(false) const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue( profile, 'ProfileHeader', ) const [__, queueUnblock] = useProfileBlockMutationQueue(profile) const unblockPromptControl = Prompt.usePromptControl() const moderation = useMemo( () => moderateProfile(profile, moderationOpts), [profile, moderationOpts], ) const onPressBack = React.useCallback(() => { if (navigation.canGoBack()) { navigation.goBack() } else { navigation.navigate('Home') } }, [navigation]) const onPressAvi = React.useCallback(() => { if ( profile.avatar && !(moderation.avatar.blur && moderation.avatar.noOverride) ) { openLightbox(new ProfileImageLightbox(profile)) } }, [openLightbox, profile, moderation]) const onPressFollow = () => { requireAuth(async () => { try { track('ProfileHeader:FollowButtonClicked') await queueFollow() Toast.show( _( msg`Following ${sanitizeDisplayName( profile.displayName || profile.handle, )}`, ), ) } catch (e: any) { if (e?.name !== 'AbortError') { logger.error('Failed to follow', {message: String(e)}) Toast.show(_(msg`There was an issue! ${e.toString()}`)) } } }) } const onPressUnfollow = () => { requireAuth(async () => { try { track('ProfileHeader:UnfollowButtonClicked') await queueUnfollow() Toast.show( _( msg`No longer following ${sanitizeDisplayName( profile.displayName || profile.handle, )}`, ), ) } catch (e: any) { if (e?.name !== 'AbortError') { logger.error('Failed to unfollow', {message: String(e)}) Toast.show(_(msg`There was an issue! ${e.toString()}`)) } } }) } const onPressEditProfile = React.useCallback(() => { track('ProfileHeader:EditProfileButtonClicked') openModal({ name: 'edit-profile', profile, }) }, [track, openModal, profile]) const unblockAccount = React.useCallback(async () => { track('ProfileHeader:UnblockAccountButtonClicked') try { await queueUnblock() Toast.show(_(msg`Account unblocked`)) } catch (e: any) { if (e?.name !== 'AbortError') { logger.error('Failed to unblock account', {message: e}) Toast.show(_(msg`There was an issue! ${e.toString()}`)) } } }, [_, queueUnblock, track]) const isMe = React.useMemo( () => currentAccount?.did === profile.did, [currentAccount, profile], ) const blockHide = !isMe && (profile.viewer?.blocking || profile.viewer?.blockedBy) const following = formatCount(profile.followsCount || 0) const followers = formatCount(profile.followersCount || 0) const pluralizedFollowers = pluralize(profile.followersCount || 0, 'follower') return ( {isPlaceholderProfile ? ( ) : ( )} {isMe ? ( Edit Profile ) : profile.viewer?.blocking ? ( profile.viewer?.blockingByList ? null : ( unblockPromptControl.open()} style={[styles.btn, styles.mainBtn, pal.btn]} accessibilityRole="button" accessibilityLabel={_(msg`Unblock`)} accessibilityHint=""> Unblock ) ) : !profile.viewer?.blockedBy ? ( <> {hasSession && ( setShowSuggestedFollows(!showSuggestedFollows)} style={[ styles.btn, styles.mainBtn, pal.btn, { paddingHorizontal: 10, backgroundColor: showSuggestedFollows ? pal.colors.text : pal.colors.backgroundLight, }, ]} accessibilityRole="button" accessibilityLabel={_( msg`Show follows similar to ${profile.handle}`, )} accessibilityHint={_( msg`Shows a list of users similar to this user.`, )}> )} {profile.viewer?.following ? ( Following ) : ( Follow )} ) : null} {sanitizeDisplayName( profile.displayName || sanitizeHandle(profile.handle), moderation.profile, )} {profile.viewer?.followedBy && !blockHide ? ( Follows you ) : undefined} {invalidHandle ? _(msg`⚠Invalid Handle`) : `@${profile.handle}`} {!isPlaceholderProfile && !blockHide && ( <> track(`ProfileHeader:FollowersButtonClicked`, { handle: profile.handle, }) } asAnchor accessibilityLabel={`${followers} ${pluralizedFollowers}`} accessibilityHint={_(msg`Opens followers list`)}> {followers}{' '} {pluralizedFollowers} track(`ProfileHeader:FollowsButtonClicked`, { handle: profile.handle, }) } asAnchor accessibilityLabel={_(msg`${following} following`)} accessibilityHint={_(msg`Opens following list`)}> {following}{' '} following {formatCount(profile.postsCount || 0)}{' '} {pluralize(profile.postsCount || 0, 'post')} {descriptionRT && !moderation.profile.blur ? ( ) : undefined} )} {isMe && ( )} {showSuggestedFollows && ( { if (showSuggestedFollows) { setShowSuggestedFollows(false) } else { track('ProfileHeader:SuggestedFollowsOpened') setShowSuggestedFollows(true) } }} /> )} {!isDesktop && !hideBackButton && ( )} ) } ProfileHeader = memo(ProfileHeader) export {ProfileHeader} const styles = StyleSheet.create({ banner: { width: '100%', height: 120, }, backBtnWrapper: { position: 'absolute', top: 10, left: 10, width: 30, height: 30, overflow: 'hidden', borderRadius: 15, // @ts-ignore web only cursor: 'pointer', }, backBtn: { width: 30, height: 30, borderRadius: 15, alignItems: 'center', justifyContent: 'center', }, avi: { position: 'absolute', top: 110, left: 10, width: 84, height: 84, borderRadius: 42, borderWidth: 2, }, content: { paddingTop: 8, paddingHorizontal: 14, paddingBottom: 4, }, buttonsLine: { flexDirection: 'row', marginLeft: 'auto', marginBottom: 12, }, primaryBtn: { backgroundColor: colors.blue3, paddingHorizontal: 24, paddingVertical: 6, }, mainBtn: { paddingHorizontal: 24, }, secondaryBtn: { paddingHorizontal: 14, }, btn: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', paddingVertical: 7, borderRadius: 50, marginLeft: 6, }, title: {lineHeight: 38}, // Word wrapping appears fine on // mobile but overflows on desktop handle: isNative ? {} : { // @ts-ignore web only -prf wordBreak: 'break-all', }, invalidHandle: { borderWidth: 1, borderRadius: 4, paddingHorizontal: 4, }, handleLine: { flexDirection: 'row', marginBottom: 8, }, metricsLine: { flexDirection: 'row', marginBottom: 8, }, description: { marginBottom: 8, }, detailLine: { flexDirection: 'row', alignItems: 'center', marginBottom: 5, }, pill: { borderRadius: 4, paddingHorizontal: 6, paddingVertical: 2, }, br40: {borderRadius: 40}, br50: {borderRadius: 50}, })