768 lines
24 KiB
TypeScript
768 lines
24 KiB
TypeScript
import React, {memo} from 'react'
|
|
import {
|
|
StyleSheet,
|
|
TouchableOpacity,
|
|
TouchableWithoutFeedback,
|
|
View,
|
|
} from 'react-native'
|
|
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
|
import {useNavigation} from '@react-navigation/native'
|
|
import {useQueryClient} from '@tanstack/react-query'
|
|
import {
|
|
AppBskyActorDefs,
|
|
ProfileModeration,
|
|
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, isWeb} 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 '../util/text/RichText'
|
|
import {UserAvatar} from '../util/UserAvatar'
|
|
import {UserBanner} from '../util/UserBanner'
|
|
import {ProfileHeaderAlerts} from '../util/moderation/ProfileHeaderAlerts'
|
|
import {formatCount} from '../util/numeric/format'
|
|
import {NativeDropdown, DropdownItem} from '../util/forms/NativeDropdown'
|
|
import {Link} from '../util/Link'
|
|
import {ProfileHeaderSuggestedFollows} from './ProfileHeaderSuggestedFollows'
|
|
import {useModalControls} from '#/state/modals'
|
|
import {useLightboxControls, ProfileImageLightbox} from '#/state/lightbox'
|
|
import {
|
|
RQKEY as profileQueryKey,
|
|
useProfileMuteMutationQueue,
|
|
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} from 'lib/strings/handles'
|
|
import {makeProfileLink} from 'lib/routes/links'
|
|
import {pluralize} from 'lib/strings/helpers'
|
|
import {toShareUrl} from 'lib/strings/url-helpers'
|
|
import {sanitizeDisplayName} from 'lib/strings/display-names'
|
|
import {sanitizeHandle} from 'lib/strings/handles'
|
|
import {shareUrl} from 'lib/sharing'
|
|
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'
|
|
|
|
interface Props {
|
|
profile: Shadow<AppBskyActorDefs.ProfileViewDetailed> | null
|
|
moderation: ProfileModeration | null
|
|
hideBackButton?: boolean
|
|
isProfilePreview?: boolean
|
|
}
|
|
|
|
export function ProfileHeader({
|
|
profile,
|
|
moderation,
|
|
hideBackButton = false,
|
|
isProfilePreview,
|
|
}: Props) {
|
|
const pal = usePalette('default')
|
|
|
|
// loading
|
|
// =
|
|
if (!profile || !moderation) {
|
|
return (
|
|
<View style={pal.view}>
|
|
<LoadingPlaceholder width="100%" height={153} />
|
|
<View
|
|
style={[pal.view, {borderColor: pal.colors.background}, styles.avi]}>
|
|
<LoadingPlaceholder width={80} height={80} style={styles.br40} />
|
|
</View>
|
|
<View style={styles.content}>
|
|
<View style={[styles.buttonsLine]}>
|
|
<LoadingPlaceholder width={167} height={31} style={styles.br50} />
|
|
</View>
|
|
</View>
|
|
</View>
|
|
)
|
|
}
|
|
|
|
// loaded
|
|
// =
|
|
return (
|
|
<ProfileHeaderLoaded
|
|
profile={profile}
|
|
moderation={moderation}
|
|
hideBackButton={hideBackButton}
|
|
isProfilePreview={isProfilePreview}
|
|
/>
|
|
)
|
|
}
|
|
|
|
interface LoadedProps {
|
|
profile: Shadow<AppBskyActorDefs.ProfileViewDetailed>
|
|
moderation: ProfileModeration
|
|
hideBackButton?: boolean
|
|
isProfilePreview?: boolean
|
|
}
|
|
|
|
let ProfileHeaderLoaded = ({
|
|
profile,
|
|
moderation,
|
|
hideBackButton = false,
|
|
isProfilePreview,
|
|
}: LoadedProps): React.ReactNode => {
|
|
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<NavigationProp>()
|
|
const {track} = useAnalytics()
|
|
const invalidHandle = isInvalidHandle(profile.handle)
|
|
const {isDesktop} = useWebMediaQueries()
|
|
const [showSuggestedFollows, setShowSuggestedFollows] = React.useState(false)
|
|
const descriptionRT = React.useMemo(
|
|
() =>
|
|
profile.description
|
|
? new RichTextAPI({text: profile.description})
|
|
: undefined,
|
|
[profile],
|
|
)
|
|
const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(profile)
|
|
const [queueMute, queueUnmute] = useProfileMuteMutationQueue(profile)
|
|
const [queueBlock, queueUnblock] = useProfileBlockMutationQueue(profile)
|
|
const queryClient = useQueryClient()
|
|
|
|
const invalidateProfileQuery = React.useCallback(() => {
|
|
queryClient.invalidateQueries({
|
|
queryKey: profileQueryKey(profile.did),
|
|
})
|
|
}, [queryClient, profile.did])
|
|
|
|
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(
|
|
`Following ${sanitizeDisplayName(
|
|
profile.displayName || profile.handle,
|
|
)}`,
|
|
)
|
|
} catch (e: any) {
|
|
if (e?.name !== 'AbortError') {
|
|
logger.error('Failed to follow', {error: String(e)})
|
|
Toast.show(`There was an issue! ${e.toString()}`)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
const onPressUnfollow = () => {
|
|
requireAuth(async () => {
|
|
try {
|
|
track('ProfileHeader:UnfollowButtonClicked')
|
|
await queueUnfollow()
|
|
Toast.show(
|
|
`No longer following ${sanitizeDisplayName(
|
|
profile.displayName || profile.handle,
|
|
)}`,
|
|
)
|
|
} catch (e: any) {
|
|
if (e?.name !== 'AbortError') {
|
|
logger.error('Failed to unfollow', {error: String(e)})
|
|
Toast.show(`There was an issue! ${e.toString()}`)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
const onPressEditProfile = React.useCallback(() => {
|
|
track('ProfileHeader:EditProfileButtonClicked')
|
|
openModal({
|
|
name: 'edit-profile',
|
|
profile,
|
|
})
|
|
}, [track, openModal, profile])
|
|
|
|
const onPressShare = React.useCallback(() => {
|
|
track('ProfileHeader:ShareButtonClicked')
|
|
shareUrl(toShareUrl(makeProfileLink(profile)))
|
|
}, [track, profile])
|
|
|
|
const onPressAddRemoveLists = React.useCallback(() => {
|
|
track('ProfileHeader:AddToListsButtonClicked')
|
|
openModal({
|
|
name: 'user-add-remove-lists',
|
|
subject: profile.did,
|
|
displayName: profile.displayName || profile.handle,
|
|
onAdd: invalidateProfileQuery,
|
|
onRemove: invalidateProfileQuery,
|
|
})
|
|
}, [track, profile, openModal, invalidateProfileQuery])
|
|
|
|
const onPressMuteAccount = React.useCallback(async () => {
|
|
track('ProfileHeader:MuteAccountButtonClicked')
|
|
try {
|
|
await queueMute()
|
|
Toast.show('Account muted')
|
|
} catch (e: any) {
|
|
if (e?.name !== 'AbortError') {
|
|
logger.error('Failed to mute account', {error: e})
|
|
Toast.show(`There was an issue! ${e.toString()}`)
|
|
}
|
|
}
|
|
}, [track, queueMute])
|
|
|
|
const onPressUnmuteAccount = React.useCallback(async () => {
|
|
track('ProfileHeader:UnmuteAccountButtonClicked')
|
|
try {
|
|
await queueUnmute()
|
|
Toast.show('Account unmuted')
|
|
} catch (e: any) {
|
|
if (e?.name !== 'AbortError') {
|
|
logger.error('Failed to unmute account', {error: e})
|
|
Toast.show(`There was an issue! ${e.toString()}`)
|
|
}
|
|
}
|
|
}, [track, queueUnmute])
|
|
|
|
const onPressBlockAccount = React.useCallback(async () => {
|
|
track('ProfileHeader:BlockAccountButtonClicked')
|
|
openModal({
|
|
name: 'confirm',
|
|
title: _(msg`Block Account`),
|
|
message: _(
|
|
msg`Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you.`,
|
|
),
|
|
onPressConfirm: async () => {
|
|
try {
|
|
await queueBlock()
|
|
Toast.show('Account blocked')
|
|
} catch (e: any) {
|
|
if (e?.name !== 'AbortError') {
|
|
logger.error('Failed to block account', {error: e})
|
|
Toast.show(`There was an issue! ${e.toString()}`)
|
|
}
|
|
}
|
|
},
|
|
})
|
|
}, [track, queueBlock, openModal, _])
|
|
|
|
const onPressUnblockAccount = React.useCallback(async () => {
|
|
track('ProfileHeader:UnblockAccountButtonClicked')
|
|
openModal({
|
|
name: 'confirm',
|
|
title: _(msg`Unblock Account`),
|
|
message: _(
|
|
msg`The account will be able to interact with you after unblocking.`,
|
|
),
|
|
onPressConfirm: async () => {
|
|
try {
|
|
await queueUnblock()
|
|
Toast.show('Account unblocked')
|
|
} catch (e: any) {
|
|
if (e?.name !== 'AbortError') {
|
|
logger.error('Failed to unblock account', {error: e})
|
|
Toast.show(`There was an issue! ${e.toString()}`)
|
|
}
|
|
}
|
|
},
|
|
})
|
|
}, [track, queueUnblock, openModal, _])
|
|
|
|
const onPressReportAccount = React.useCallback(() => {
|
|
track('ProfileHeader:ReportAccountButtonClicked')
|
|
openModal({
|
|
name: 'report',
|
|
did: profile.did,
|
|
})
|
|
}, [track, openModal, profile])
|
|
|
|
const isMe = React.useMemo(
|
|
() => currentAccount?.did === profile.did,
|
|
[currentAccount, profile],
|
|
)
|
|
const dropdownItems: DropdownItem[] = React.useMemo(() => {
|
|
let items: DropdownItem[] = [
|
|
{
|
|
testID: 'profileHeaderDropdownShareBtn',
|
|
label: isWeb ? _(msg`Copy link to profile`) : _(msg`Share`),
|
|
onPress: onPressShare,
|
|
icon: {
|
|
ios: {
|
|
name: 'square.and.arrow.up',
|
|
},
|
|
android: 'ic_menu_share',
|
|
web: 'share',
|
|
},
|
|
},
|
|
]
|
|
if (hasSession) {
|
|
items.push({label: 'separator'})
|
|
items.push({
|
|
testID: 'profileHeaderDropdownListAddRemoveBtn',
|
|
label: _(msg`Add to Lists`),
|
|
onPress: onPressAddRemoveLists,
|
|
icon: {
|
|
ios: {
|
|
name: 'list.bullet',
|
|
},
|
|
android: 'ic_menu_add',
|
|
web: 'list',
|
|
},
|
|
})
|
|
if (!isMe) {
|
|
if (!profile.viewer?.blocking) {
|
|
if (!profile.viewer?.mutedByList) {
|
|
items.push({
|
|
testID: 'profileHeaderDropdownMuteBtn',
|
|
label: profile.viewer?.muted
|
|
? _(msg`Unmute Account`)
|
|
: _(msg`Mute Account`),
|
|
onPress: profile.viewer?.muted
|
|
? onPressUnmuteAccount
|
|
: onPressMuteAccount,
|
|
icon: {
|
|
ios: {
|
|
name: 'speaker.slash',
|
|
},
|
|
android: 'ic_lock_silent_mode',
|
|
web: 'comment-slash',
|
|
},
|
|
})
|
|
}
|
|
}
|
|
if (!profile.viewer?.blockingByList) {
|
|
items.push({
|
|
testID: 'profileHeaderDropdownBlockBtn',
|
|
label: profile.viewer?.blocking
|
|
? _(msg`Unblock Account`)
|
|
: _(msg`Block Account`),
|
|
onPress: profile.viewer?.blocking
|
|
? onPressUnblockAccount
|
|
: onPressBlockAccount,
|
|
icon: {
|
|
ios: {
|
|
name: 'person.fill.xmark',
|
|
},
|
|
android: 'ic_menu_close_clear_cancel',
|
|
web: 'user-slash',
|
|
},
|
|
})
|
|
}
|
|
items.push({
|
|
testID: 'profileHeaderDropdownReportBtn',
|
|
label: _(msg`Report Account`),
|
|
onPress: onPressReportAccount,
|
|
icon: {
|
|
ios: {
|
|
name: 'exclamationmark.triangle',
|
|
},
|
|
android: 'ic_menu_report_image',
|
|
web: 'circle-exclamation',
|
|
},
|
|
})
|
|
}
|
|
}
|
|
return items
|
|
}, [
|
|
isMe,
|
|
hasSession,
|
|
profile.viewer?.muted,
|
|
profile.viewer?.mutedByList,
|
|
profile.viewer?.blocking,
|
|
profile.viewer?.blockingByList,
|
|
onPressShare,
|
|
onPressUnmuteAccount,
|
|
onPressMuteAccount,
|
|
onPressUnblockAccount,
|
|
onPressBlockAccount,
|
|
onPressReportAccount,
|
|
onPressAddRemoveLists,
|
|
_,
|
|
])
|
|
|
|
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 (
|
|
<View style={pal.view}>
|
|
<UserBanner banner={profile.banner} moderation={moderation.avatar} />
|
|
<View style={styles.content}>
|
|
<View style={[styles.buttonsLine]}>
|
|
{isMe ? (
|
|
<TouchableOpacity
|
|
testID="profileHeaderEditProfileButton"
|
|
onPress={onPressEditProfile}
|
|
style={[styles.btn, styles.mainBtn, pal.btn]}
|
|
accessibilityRole="button"
|
|
accessibilityLabel={_(msg`Edit profile`)}
|
|
accessibilityHint="Opens editor for profile display name, avatar, background image, and description">
|
|
<Text type="button" style={pal.text}>
|
|
<Trans>Edit Profile</Trans>
|
|
</Text>
|
|
</TouchableOpacity>
|
|
) : profile.viewer?.blocking ? (
|
|
profile.viewer?.blockingByList ? null : (
|
|
<TouchableOpacity
|
|
testID="unblockBtn"
|
|
onPress={onPressUnblockAccount}
|
|
style={[styles.btn, styles.mainBtn, pal.btn]}
|
|
accessibilityRole="button"
|
|
accessibilityLabel={_(msg`Unblock`)}
|
|
accessibilityHint="">
|
|
<Text type="button" style={[pal.text, s.bold]}>
|
|
<Trans>Unblock</Trans>
|
|
</Text>
|
|
</TouchableOpacity>
|
|
)
|
|
) : !profile.viewer?.blockedBy ? (
|
|
<>
|
|
{!isProfilePreview && hasSession && (
|
|
<TouchableOpacity
|
|
testID="suggestedFollowsBtn"
|
|
onPress={() => setShowSuggestedFollows(!showSuggestedFollows)}
|
|
style={[
|
|
styles.btn,
|
|
styles.mainBtn,
|
|
pal.btn,
|
|
{
|
|
paddingHorizontal: 10,
|
|
backgroundColor: showSuggestedFollows
|
|
? pal.colors.text
|
|
: pal.colors.backgroundLight,
|
|
},
|
|
]}
|
|
accessibilityRole="button"
|
|
accessibilityLabel={`Show follows similar to ${profile.handle}`}
|
|
accessibilityHint={`Shows a list of users similar to this user.`}>
|
|
<FontAwesomeIcon
|
|
icon="user-plus"
|
|
style={[
|
|
pal.text,
|
|
{
|
|
color: showSuggestedFollows
|
|
? pal.textInverted.color
|
|
: pal.text.color,
|
|
},
|
|
]}
|
|
size={14}
|
|
/>
|
|
</TouchableOpacity>
|
|
)}
|
|
|
|
{profile.viewer?.following ? (
|
|
<TouchableOpacity
|
|
testID="unfollowBtn"
|
|
onPress={onPressUnfollow}
|
|
style={[styles.btn, styles.mainBtn, pal.btn]}
|
|
accessibilityRole="button"
|
|
accessibilityLabel={`Unfollow ${profile.handle}`}
|
|
accessibilityHint={`Hides posts from ${profile.handle} in your feed`}>
|
|
<FontAwesomeIcon
|
|
icon="check"
|
|
style={[pal.text, s.mr5]}
|
|
size={14}
|
|
/>
|
|
<Text type="button" style={pal.text}>
|
|
<Trans>Following</Trans>
|
|
</Text>
|
|
</TouchableOpacity>
|
|
) : (
|
|
<TouchableOpacity
|
|
testID="followBtn"
|
|
onPress={onPressFollow}
|
|
style={[styles.btn, styles.mainBtn, palInverted.view]}
|
|
accessibilityRole="button"
|
|
accessibilityLabel={`Follow ${profile.handle}`}
|
|
accessibilityHint={`Shows posts from ${profile.handle} in your feed`}>
|
|
<FontAwesomeIcon
|
|
icon="plus"
|
|
style={[palInverted.text, s.mr5]}
|
|
/>
|
|
<Text type="button" style={[palInverted.text, s.bold]}>
|
|
<Trans>Follow</Trans>
|
|
</Text>
|
|
</TouchableOpacity>
|
|
)}
|
|
</>
|
|
) : null}
|
|
{dropdownItems?.length ? (
|
|
<NativeDropdown
|
|
testID="profileHeaderDropdownBtn"
|
|
items={dropdownItems}
|
|
accessibilityLabel={_(msg`More options`)}
|
|
accessibilityHint="">
|
|
<View style={[styles.btn, styles.secondaryBtn, pal.btn]}>
|
|
<FontAwesomeIcon icon="ellipsis" size={20} style={[pal.text]} />
|
|
</View>
|
|
</NativeDropdown>
|
|
) : undefined}
|
|
</View>
|
|
<View>
|
|
<Text
|
|
testID="profileHeaderDisplayName"
|
|
type="title-2xl"
|
|
style={[pal.text, styles.title]}>
|
|
{sanitizeDisplayName(
|
|
profile.displayName || sanitizeHandle(profile.handle),
|
|
moderation.profile,
|
|
)}
|
|
</Text>
|
|
</View>
|
|
<View style={styles.handleLine}>
|
|
{profile.viewer?.followedBy && !blockHide ? (
|
|
<View style={[styles.pill, pal.btn, s.mr5]}>
|
|
<Text type="xs" style={[pal.text]}>
|
|
<Trans>Follows you</Trans>
|
|
</Text>
|
|
</View>
|
|
) : undefined}
|
|
<ThemedText
|
|
type={invalidHandle ? 'xs' : 'md'}
|
|
fg={invalidHandle ? 'error' : 'light'}
|
|
border={invalidHandle ? 'error' : undefined}
|
|
style={[
|
|
invalidHandle ? styles.invalidHandle : undefined,
|
|
styles.handle,
|
|
]}>
|
|
{invalidHandle ? '⚠Invalid Handle' : `@${profile.handle}`}
|
|
</ThemedText>
|
|
</View>
|
|
{!blockHide && (
|
|
<>
|
|
<View style={styles.metricsLine}>
|
|
<Link
|
|
testID="profileHeaderFollowersButton"
|
|
style={[s.flexRow, s.mr10]}
|
|
href={makeProfileLink(profile, 'followers')}
|
|
onPressOut={() =>
|
|
track(`ProfileHeader:FollowersButtonClicked`, {
|
|
handle: profile.handle,
|
|
})
|
|
}
|
|
asAnchor
|
|
accessibilityLabel={`${followers} ${pluralizedFollowers}`}
|
|
accessibilityHint={'Opens followers list'}>
|
|
<Text type="md" style={[s.bold, pal.text]}>
|
|
{followers}{' '}
|
|
</Text>
|
|
<Text type="md" style={[pal.textLight]}>
|
|
{pluralizedFollowers}
|
|
</Text>
|
|
</Link>
|
|
<Link
|
|
testID="profileHeaderFollowsButton"
|
|
style={[s.flexRow, s.mr10]}
|
|
href={makeProfileLink(profile, 'follows')}
|
|
onPressOut={() =>
|
|
track(`ProfileHeader:FollowsButtonClicked`, {
|
|
handle: profile.handle,
|
|
})
|
|
}
|
|
asAnchor
|
|
accessibilityLabel={`${following} following`}
|
|
accessibilityHint={'Opens following list'}>
|
|
<Text type="md" style={[s.bold, pal.text]}>
|
|
{following}{' '}
|
|
</Text>
|
|
<Text type="md" style={[pal.textLight]}>
|
|
<Trans>following</Trans>
|
|
</Text>
|
|
</Link>
|
|
<Text type="md" style={[s.bold, pal.text]}>
|
|
{formatCount(profile.postsCount || 0)}{' '}
|
|
<Text type="md" style={[pal.textLight]}>
|
|
{pluralize(profile.postsCount || 0, 'post')}
|
|
</Text>
|
|
</Text>
|
|
</View>
|
|
{descriptionRT && !moderation.profile.blur ? (
|
|
<RichText
|
|
testID="profileHeaderDescription"
|
|
style={[styles.description, pal.text]}
|
|
numberOfLines={15}
|
|
richText={descriptionRT}
|
|
/>
|
|
) : undefined}
|
|
</>
|
|
)}
|
|
<ProfileHeaderAlerts moderation={moderation} />
|
|
</View>
|
|
|
|
{!isProfilePreview && (
|
|
<ProfileHeaderSuggestedFollows
|
|
actorDid={profile.did}
|
|
active={showSuggestedFollows}
|
|
requestDismiss={() => setShowSuggestedFollows(!showSuggestedFollows)}
|
|
/>
|
|
)}
|
|
|
|
{!isDesktop && !hideBackButton && (
|
|
<TouchableWithoutFeedback
|
|
testID="profileHeaderBackBtn"
|
|
onPress={onPressBack}
|
|
hitSlop={BACK_HITSLOP}
|
|
accessibilityRole="button"
|
|
accessibilityLabel={_(msg`Back`)}
|
|
accessibilityHint="">
|
|
<View style={styles.backBtnWrapper}>
|
|
<BlurView style={styles.backBtn} blurType="dark">
|
|
<FontAwesomeIcon size={18} icon="angle-left" style={s.white} />
|
|
</BlurView>
|
|
</View>
|
|
</TouchableWithoutFeedback>
|
|
)}
|
|
<TouchableWithoutFeedback
|
|
testID="profileHeaderAviButton"
|
|
onPress={onPressAvi}
|
|
accessibilityRole="image"
|
|
accessibilityLabel={`View ${profile.handle}'s avatar`}
|
|
accessibilityHint="">
|
|
<View
|
|
style={[pal.view, {borderColor: pal.colors.background}, styles.avi]}>
|
|
<UserAvatar
|
|
size={80}
|
|
avatar={profile.avatar}
|
|
moderation={moderation.avatar}
|
|
/>
|
|
</View>
|
|
</TouchableWithoutFeedback>
|
|
</View>
|
|
)
|
|
}
|
|
ProfileHeaderLoaded = memo(ProfileHeaderLoaded)
|
|
|
|
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},
|
|
})
|