Give explicit names to MobX observer components (#1413)
* Consider observer(...) as components * Add display names to MobX observers * Temporarily suppress nested components * Suppress new false positives for react/prop-types
This commit is contained in:
parent
69209c988f
commit
8a93321fb1
72 changed files with 2868 additions and 2836 deletions
|
@ -6,56 +6,54 @@ import {useStores} from 'state/index'
|
|||
import * as Toast from '../util/Toast'
|
||||
import {FollowState} from 'state/models/cache/my-follows'
|
||||
|
||||
export const FollowButton = observer(
|
||||
({
|
||||
unfollowedType = 'inverted',
|
||||
followedType = 'default',
|
||||
did,
|
||||
onToggleFollow,
|
||||
}: {
|
||||
unfollowedType?: ButtonType
|
||||
followedType?: ButtonType
|
||||
did: string
|
||||
onToggleFollow?: (v: boolean) => void
|
||||
}) => {
|
||||
const store = useStores()
|
||||
const followState = store.me.follows.getFollowState(did)
|
||||
export const FollowButton = observer(function FollowButtonImpl({
|
||||
unfollowedType = 'inverted',
|
||||
followedType = 'default',
|
||||
did,
|
||||
onToggleFollow,
|
||||
}: {
|
||||
unfollowedType?: ButtonType
|
||||
followedType?: ButtonType
|
||||
did: string
|
||||
onToggleFollow?: (v: boolean) => void
|
||||
}) {
|
||||
const store = useStores()
|
||||
const followState = store.me.follows.getFollowState(did)
|
||||
|
||||
if (followState === FollowState.Unknown) {
|
||||
return <View />
|
||||
}
|
||||
if (followState === FollowState.Unknown) {
|
||||
return <View />
|
||||
}
|
||||
|
||||
const onToggleFollowInner = async () => {
|
||||
const updatedFollowState = await store.me.follows.fetchFollowState(did)
|
||||
if (updatedFollowState === FollowState.Following) {
|
||||
try {
|
||||
await store.agent.deleteFollow(store.me.follows.getFollowUri(did))
|
||||
store.me.follows.removeFollow(did)
|
||||
onToggleFollow?.(false)
|
||||
} catch (e: any) {
|
||||
store.log.error('Failed to delete follow', e)
|
||||
Toast.show('An issue occurred, please try again.')
|
||||
}
|
||||
} else if (updatedFollowState === FollowState.NotFollowing) {
|
||||
try {
|
||||
const res = await store.agent.follow(did)
|
||||
store.me.follows.addFollow(did, res.uri)
|
||||
onToggleFollow?.(true)
|
||||
} catch (e: any) {
|
||||
store.log.error('Failed to create follow', e)
|
||||
Toast.show('An issue occurred, please try again.')
|
||||
}
|
||||
const onToggleFollowInner = async () => {
|
||||
const updatedFollowState = await store.me.follows.fetchFollowState(did)
|
||||
if (updatedFollowState === FollowState.Following) {
|
||||
try {
|
||||
await store.agent.deleteFollow(store.me.follows.getFollowUri(did))
|
||||
store.me.follows.removeFollow(did)
|
||||
onToggleFollow?.(false)
|
||||
} catch (e: any) {
|
||||
store.log.error('Failed to delete follow', e)
|
||||
Toast.show('An issue occurred, please try again.')
|
||||
}
|
||||
} else if (updatedFollowState === FollowState.NotFollowing) {
|
||||
try {
|
||||
const res = await store.agent.follow(did)
|
||||
store.me.follows.addFollow(did, res.uri)
|
||||
onToggleFollow?.(true)
|
||||
} catch (e: any) {
|
||||
store.log.error('Failed to create follow', e)
|
||||
Toast.show('An issue occurred, please try again.')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
type={
|
||||
followState === FollowState.Following ? followedType : unfollowedType
|
||||
}
|
||||
onPress={onToggleFollowInner}
|
||||
label={followState === FollowState.Following ? 'Unfollow' : 'Follow'}
|
||||
/>
|
||||
)
|
||||
},
|
||||
)
|
||||
return (
|
||||
<Button
|
||||
type={
|
||||
followState === FollowState.Following ? followedType : unfollowedType
|
||||
}
|
||||
onPress={onToggleFollowInner}
|
||||
label={followState === FollowState.Following ? 'Unfollow' : 'Follow'}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
|
|
@ -22,89 +22,82 @@ import {
|
|||
getModerationCauseKey,
|
||||
} from 'lib/moderation'
|
||||
|
||||
export const ProfileCard = observer(
|
||||
({
|
||||
testID,
|
||||
profile,
|
||||
noBg,
|
||||
noBorder,
|
||||
followers,
|
||||
renderButton,
|
||||
}: {
|
||||
testID?: string
|
||||
profile: AppBskyActorDefs.ProfileViewBasic
|
||||
noBg?: boolean
|
||||
noBorder?: boolean
|
||||
followers?: AppBskyActorDefs.ProfileView[] | undefined
|
||||
renderButton?: (
|
||||
profile: AppBskyActorDefs.ProfileViewBasic,
|
||||
) => React.ReactNode
|
||||
}) => {
|
||||
const store = useStores()
|
||||
const pal = usePalette('default')
|
||||
export const ProfileCard = observer(function ProfileCardImpl({
|
||||
testID,
|
||||
profile,
|
||||
noBg,
|
||||
noBorder,
|
||||
followers,
|
||||
renderButton,
|
||||
}: {
|
||||
testID?: string
|
||||
profile: AppBskyActorDefs.ProfileViewBasic
|
||||
noBg?: boolean
|
||||
noBorder?: boolean
|
||||
followers?: AppBskyActorDefs.ProfileView[] | undefined
|
||||
renderButton?: (profile: AppBskyActorDefs.ProfileViewBasic) => React.ReactNode
|
||||
}) {
|
||||
const store = useStores()
|
||||
const pal = usePalette('default')
|
||||
|
||||
const moderation = moderateProfile(
|
||||
profile,
|
||||
store.preferences.moderationOpts,
|
||||
)
|
||||
const moderation = moderateProfile(profile, store.preferences.moderationOpts)
|
||||
|
||||
return (
|
||||
<Link
|
||||
testID={testID}
|
||||
style={[
|
||||
styles.outer,
|
||||
pal.border,
|
||||
noBorder && styles.outerNoBorder,
|
||||
!noBg && pal.view,
|
||||
]}
|
||||
href={makeProfileLink(profile)}
|
||||
title={profile.handle}
|
||||
asAnchor
|
||||
anchorNoUnderline>
|
||||
<View style={styles.layout}>
|
||||
<View style={styles.layoutAvi}>
|
||||
<UserAvatar
|
||||
size={40}
|
||||
avatar={profile.avatar}
|
||||
moderation={moderation.avatar}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.layoutContent}>
|
||||
<Text
|
||||
type="lg"
|
||||
style={[s.bold, pal.text]}
|
||||
numberOfLines={1}
|
||||
lineHeight={1.2}>
|
||||
{sanitizeDisplayName(
|
||||
profile.displayName || sanitizeHandle(profile.handle),
|
||||
moderation.profile,
|
||||
)}
|
||||
</Text>
|
||||
<Text type="md" style={[pal.textLight]} numberOfLines={1}>
|
||||
{sanitizeHandle(profile.handle, '@')}
|
||||
</Text>
|
||||
<ProfileCardPills
|
||||
followedBy={!!profile.viewer?.followedBy}
|
||||
moderation={moderation}
|
||||
/>
|
||||
{!!profile.viewer?.followedBy && <View style={s.flexRow} />}
|
||||
</View>
|
||||
{renderButton ? (
|
||||
<View style={styles.layoutButton}>{renderButton(profile)}</View>
|
||||
) : undefined}
|
||||
return (
|
||||
<Link
|
||||
testID={testID}
|
||||
style={[
|
||||
styles.outer,
|
||||
pal.border,
|
||||
noBorder && styles.outerNoBorder,
|
||||
!noBg && pal.view,
|
||||
]}
|
||||
href={makeProfileLink(profile)}
|
||||
title={profile.handle}
|
||||
asAnchor
|
||||
anchorNoUnderline>
|
||||
<View style={styles.layout}>
|
||||
<View style={styles.layoutAvi}>
|
||||
<UserAvatar
|
||||
size={40}
|
||||
avatar={profile.avatar}
|
||||
moderation={moderation.avatar}
|
||||
/>
|
||||
</View>
|
||||
{profile.description ? (
|
||||
<View style={styles.details}>
|
||||
<Text style={pal.text} numberOfLines={4}>
|
||||
{profile.description as string}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.layoutContent}>
|
||||
<Text
|
||||
type="lg"
|
||||
style={[s.bold, pal.text]}
|
||||
numberOfLines={1}
|
||||
lineHeight={1.2}>
|
||||
{sanitizeDisplayName(
|
||||
profile.displayName || sanitizeHandle(profile.handle),
|
||||
moderation.profile,
|
||||
)}
|
||||
</Text>
|
||||
<Text type="md" style={[pal.textLight]} numberOfLines={1}>
|
||||
{sanitizeHandle(profile.handle, '@')}
|
||||
</Text>
|
||||
<ProfileCardPills
|
||||
followedBy={!!profile.viewer?.followedBy}
|
||||
moderation={moderation}
|
||||
/>
|
||||
{!!profile.viewer?.followedBy && <View style={s.flexRow} />}
|
||||
</View>
|
||||
{renderButton ? (
|
||||
<View style={styles.layoutButton}>{renderButton(profile)}</View>
|
||||
) : undefined}
|
||||
<FollowersList followers={followers} />
|
||||
</Link>
|
||||
)
|
||||
},
|
||||
)
|
||||
</View>
|
||||
{profile.description ? (
|
||||
<View style={styles.details}>
|
||||
<Text style={pal.text} numberOfLines={4}>
|
||||
{profile.description as string}
|
||||
</Text>
|
||||
</View>
|
||||
) : undefined}
|
||||
<FollowersList followers={followers} />
|
||||
</Link>
|
||||
)
|
||||
})
|
||||
|
||||
function ProfileCardPills({
|
||||
followedBy,
|
||||
|
@ -146,45 +139,47 @@ function ProfileCardPills({
|
|||
)
|
||||
}
|
||||
|
||||
const FollowersList = observer(
|
||||
({followers}: {followers?: AppBskyActorDefs.ProfileView[] | undefined}) => {
|
||||
const store = useStores()
|
||||
const pal = usePalette('default')
|
||||
if (!followers?.length) {
|
||||
return null
|
||||
}
|
||||
const FollowersList = observer(function FollowersListImpl({
|
||||
followers,
|
||||
}: {
|
||||
followers?: AppBskyActorDefs.ProfileView[] | undefined
|
||||
}) {
|
||||
const store = useStores()
|
||||
const pal = usePalette('default')
|
||||
if (!followers?.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
const followersWithMods = followers
|
||||
.map(f => ({
|
||||
f,
|
||||
mod: moderateProfile(f, store.preferences.moderationOpts),
|
||||
}))
|
||||
.filter(({mod}) => !mod.account.filter)
|
||||
const followersWithMods = followers
|
||||
.map(f => ({
|
||||
f,
|
||||
mod: moderateProfile(f, store.preferences.moderationOpts),
|
||||
}))
|
||||
.filter(({mod}) => !mod.account.filter)
|
||||
|
||||
return (
|
||||
<View style={styles.followedBy}>
|
||||
<Text
|
||||
type="sm"
|
||||
style={[styles.followsByDesc, pal.textLight]}
|
||||
numberOfLines={2}
|
||||
lineHeight={1.2}>
|
||||
Followed by{' '}
|
||||
{followersWithMods.map(({f}) => f.displayName || f.handle).join(', ')}
|
||||
</Text>
|
||||
{followersWithMods.slice(0, 3).map(({f, mod}) => (
|
||||
<View key={f.did} style={styles.followedByAviContainer}>
|
||||
<View style={[styles.followedByAvi, pal.view]}>
|
||||
<UserAvatar avatar={f.avatar} size={32} moderation={mod.avatar} />
|
||||
</View>
|
||||
return (
|
||||
<View style={styles.followedBy}>
|
||||
<Text
|
||||
type="sm"
|
||||
style={[styles.followsByDesc, pal.textLight]}
|
||||
numberOfLines={2}
|
||||
lineHeight={1.2}>
|
||||
Followed by{' '}
|
||||
{followersWithMods.map(({f}) => f.displayName || f.handle).join(', ')}
|
||||
</Text>
|
||||
{followersWithMods.slice(0, 3).map(({f, mod}) => (
|
||||
<View key={f.did} style={styles.followedByAviContainer}>
|
||||
<View style={[styles.followedByAvi, pal.view]}>
|
||||
<UserAvatar avatar={f.avatar} size={32} moderation={mod.avatar} />
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
)
|
||||
},
|
||||
)
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
)
|
||||
})
|
||||
|
||||
export const ProfileCardWithFollowBtn = observer(
|
||||
({
|
||||
function ProfileCardWithFollowBtnImpl({
|
||||
profile,
|
||||
noBg,
|
||||
noBorder,
|
||||
|
@ -194,7 +189,7 @@ export const ProfileCardWithFollowBtn = observer(
|
|||
noBg?: boolean
|
||||
noBorder?: boolean
|
||||
followers?: AppBskyActorDefs.ProfileView[] | undefined
|
||||
}) => {
|
||||
}) {
|
||||
const store = useStores()
|
||||
const isMe = store.me.did === profile.did
|
||||
|
||||
|
|
|
@ -78,6 +78,8 @@ export const ProfileFollowers = observer(function ProfileFollowers({
|
|||
onEndReached={onEndReached}
|
||||
renderItem={renderItem}
|
||||
initialNumToRender={15}
|
||||
// FIXME(dan)
|
||||
// eslint-disable-next-line react/no-unstable-nested-components
|
||||
ListFooterComponent={() => (
|
||||
<View style={styles.footer}>
|
||||
{view.isLoading && <ActivityIndicator />}
|
||||
|
|
|
@ -75,6 +75,8 @@ export const ProfileFollows = observer(function ProfileFollows({
|
|||
onEndReached={onEndReached}
|
||||
renderItem={renderItem}
|
||||
initialNumToRender={15}
|
||||
// FIXME(dan)
|
||||
// eslint-disable-next-line react/no-unstable-nested-components
|
||||
ListFooterComponent={() => (
|
||||
<View style={styles.footer}>
|
||||
{view.isLoading && <ActivityIndicator />}
|
||||
|
|
|
@ -45,510 +45,502 @@ interface Props {
|
|||
hideBackButton?: boolean
|
||||
}
|
||||
|
||||
export const ProfileHeader = observer(
|
||||
({view, onRefreshAll, hideBackButton = false}: Props) => {
|
||||
const pal = usePalette('default')
|
||||
|
||||
// loading
|
||||
// =
|
||||
if (!view || !view.hasLoaded) {
|
||||
return (
|
||||
<View style={pal.view}>
|
||||
<LoadingPlaceholder width="100%" height={120} />
|
||||
<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={100} height={31} style={styles.br50} />
|
||||
</View>
|
||||
<View>
|
||||
<Text type="title-2xl" style={[pal.text, styles.title]}>
|
||||
{sanitizeDisplayName(
|
||||
view.displayName || sanitizeHandle(view.handle),
|
||||
)}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
// error
|
||||
// =
|
||||
if (view.hasError) {
|
||||
return (
|
||||
<View testID="profileHeaderHasError">
|
||||
<Text>{view.error}</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
// loaded
|
||||
// =
|
||||
return (
|
||||
<ProfileHeaderLoaded
|
||||
view={view}
|
||||
onRefreshAll={onRefreshAll}
|
||||
hideBackButton={hideBackButton}
|
||||
/>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
const ProfileHeaderLoaded = observer(
|
||||
({view, onRefreshAll, hideBackButton = false}: Props) => {
|
||||
const pal = usePalette('default')
|
||||
const palInverted = usePalette('inverted')
|
||||
const store = useStores()
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
const {track} = useAnalytics()
|
||||
const invalidHandle = isInvalidHandle(view.handle)
|
||||
const {isDesktop} = useWebMediaQueries()
|
||||
|
||||
const onPressBack = React.useCallback(() => {
|
||||
navigation.goBack()
|
||||
}, [navigation])
|
||||
|
||||
const onPressAvi = React.useCallback(() => {
|
||||
if (
|
||||
view.avatar &&
|
||||
!(view.moderation.avatar.blur && view.moderation.avatar.noOverride)
|
||||
) {
|
||||
store.shell.openLightbox(new ProfileImageLightbox(view))
|
||||
}
|
||||
}, [store, view])
|
||||
|
||||
const onPressToggleFollow = React.useCallback(() => {
|
||||
track(
|
||||
view.viewer.following
|
||||
? 'ProfileHeader:FollowButtonClicked'
|
||||
: 'ProfileHeader:UnfollowButtonClicked',
|
||||
)
|
||||
view?.toggleFollowing().then(
|
||||
() => {
|
||||
Toast.show(
|
||||
`${
|
||||
view.viewer.following ? 'Following' : 'No longer following'
|
||||
} ${sanitizeDisplayName(view.displayName || view.handle)}`,
|
||||
)
|
||||
},
|
||||
err => store.log.error('Failed to toggle follow', err),
|
||||
)
|
||||
}, [track, view, store.log])
|
||||
|
||||
const onPressEditProfile = React.useCallback(() => {
|
||||
track('ProfileHeader:EditProfileButtonClicked')
|
||||
store.shell.openModal({
|
||||
name: 'edit-profile',
|
||||
profileView: view,
|
||||
onUpdate: onRefreshAll,
|
||||
})
|
||||
}, [track, store, view, onRefreshAll])
|
||||
|
||||
const onPressFollowers = React.useCallback(() => {
|
||||
track('ProfileHeader:FollowersButtonClicked')
|
||||
navigate('ProfileFollowers', {
|
||||
name: isInvalidHandle(view.handle) ? view.did : view.handle,
|
||||
})
|
||||
store.shell.closeAllActiveElements() // for when used in the profile preview modal
|
||||
}, [track, view, store.shell])
|
||||
|
||||
const onPressFollows = React.useCallback(() => {
|
||||
track('ProfileHeader:FollowsButtonClicked')
|
||||
navigate('ProfileFollows', {
|
||||
name: isInvalidHandle(view.handle) ? view.did : view.handle,
|
||||
})
|
||||
store.shell.closeAllActiveElements() // for when used in the profile preview modal
|
||||
}, [track, view, store.shell])
|
||||
|
||||
const onPressShare = React.useCallback(() => {
|
||||
track('ProfileHeader:ShareButtonClicked')
|
||||
const url = toShareUrl(makeProfileLink(view))
|
||||
shareUrl(url)
|
||||
}, [track, view])
|
||||
|
||||
const onPressAddRemoveLists = React.useCallback(() => {
|
||||
track('ProfileHeader:AddToListsButtonClicked')
|
||||
store.shell.openModal({
|
||||
name: 'list-add-remove-user',
|
||||
subject: view.did,
|
||||
displayName: view.displayName || view.handle,
|
||||
})
|
||||
}, [track, view, store])
|
||||
|
||||
const onPressMuteAccount = React.useCallback(async () => {
|
||||
track('ProfileHeader:MuteAccountButtonClicked')
|
||||
try {
|
||||
await view.muteAccount()
|
||||
Toast.show('Account muted')
|
||||
} catch (e: any) {
|
||||
store.log.error('Failed to mute account', e)
|
||||
Toast.show(`There was an issue! ${e.toString()}`)
|
||||
}
|
||||
}, [track, view, store])
|
||||
|
||||
const onPressUnmuteAccount = React.useCallback(async () => {
|
||||
track('ProfileHeader:UnmuteAccountButtonClicked')
|
||||
try {
|
||||
await view.unmuteAccount()
|
||||
Toast.show('Account unmuted')
|
||||
} catch (e: any) {
|
||||
store.log.error('Failed to unmute account', e)
|
||||
Toast.show(`There was an issue! ${e.toString()}`)
|
||||
}
|
||||
}, [track, view, store])
|
||||
|
||||
const onPressBlockAccount = React.useCallback(async () => {
|
||||
track('ProfileHeader:BlockAccountButtonClicked')
|
||||
store.shell.openModal({
|
||||
name: 'confirm',
|
||||
title: 'Block Account',
|
||||
message:
|
||||
'Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you.',
|
||||
onPressConfirm: async () => {
|
||||
try {
|
||||
await view.blockAccount()
|
||||
onRefreshAll()
|
||||
Toast.show('Account blocked')
|
||||
} catch (e: any) {
|
||||
store.log.error('Failed to block account', e)
|
||||
Toast.show(`There was an issue! ${e.toString()}`)
|
||||
}
|
||||
},
|
||||
})
|
||||
}, [track, view, store, onRefreshAll])
|
||||
|
||||
const onPressUnblockAccount = React.useCallback(async () => {
|
||||
track('ProfileHeader:UnblockAccountButtonClicked')
|
||||
store.shell.openModal({
|
||||
name: 'confirm',
|
||||
title: 'Unblock Account',
|
||||
message:
|
||||
'The account will be able to interact with you after unblocking.',
|
||||
onPressConfirm: async () => {
|
||||
try {
|
||||
await view.unblockAccount()
|
||||
onRefreshAll()
|
||||
Toast.show('Account unblocked')
|
||||
} catch (e: any) {
|
||||
store.log.error('Failed to unblock account', e)
|
||||
Toast.show(`There was an issue! ${e.toString()}`)
|
||||
}
|
||||
},
|
||||
})
|
||||
}, [track, view, store, onRefreshAll])
|
||||
|
||||
const onPressReportAccount = React.useCallback(() => {
|
||||
track('ProfileHeader:ReportAccountButtonClicked')
|
||||
store.shell.openModal({
|
||||
name: 'report',
|
||||
did: view.did,
|
||||
})
|
||||
}, [track, store, view])
|
||||
|
||||
const isMe = React.useMemo(
|
||||
() => store.me.did === view.did,
|
||||
[store.me.did, view.did],
|
||||
)
|
||||
const dropdownItems: DropdownItem[] = React.useMemo(() => {
|
||||
let items: DropdownItem[] = [
|
||||
{
|
||||
testID: 'profileHeaderDropdownShareBtn',
|
||||
label: 'Share',
|
||||
onPress: onPressShare,
|
||||
icon: {
|
||||
ios: {
|
||||
name: 'square.and.arrow.up',
|
||||
},
|
||||
android: 'ic_menu_share',
|
||||
web: 'share',
|
||||
},
|
||||
},
|
||||
]
|
||||
if (!isMe) {
|
||||
items.push({label: 'separator'})
|
||||
// Only add "Add to Lists" on other user's profiles, doesn't make sense to mute my own self!
|
||||
items.push({
|
||||
testID: 'profileHeaderDropdownListAddRemoveBtn',
|
||||
label: 'Add to Lists',
|
||||
onPress: onPressAddRemoveLists,
|
||||
icon: {
|
||||
ios: {
|
||||
name: 'list.bullet',
|
||||
},
|
||||
android: 'ic_menu_add',
|
||||
web: 'list',
|
||||
},
|
||||
})
|
||||
if (!view.viewer.blocking) {
|
||||
items.push({
|
||||
testID: 'profileHeaderDropdownMuteBtn',
|
||||
label: view.viewer.muted ? 'Unmute Account' : 'Mute Account',
|
||||
onPress: view.viewer.muted
|
||||
? onPressUnmuteAccount
|
||||
: onPressMuteAccount,
|
||||
icon: {
|
||||
ios: {
|
||||
name: 'speaker.slash',
|
||||
},
|
||||
android: 'ic_lock_silent_mode',
|
||||
web: 'comment-slash',
|
||||
},
|
||||
})
|
||||
}
|
||||
items.push({
|
||||
testID: 'profileHeaderDropdownBlockBtn',
|
||||
label: view.viewer.blocking ? 'Unblock Account' : 'Block Account',
|
||||
onPress: view.viewer.blocking
|
||||
? onPressUnblockAccount
|
||||
: onPressBlockAccount,
|
||||
icon: {
|
||||
ios: {
|
||||
name: 'person.fill.xmark',
|
||||
},
|
||||
android: 'ic_menu_close_clear_cancel',
|
||||
web: 'user-slash',
|
||||
},
|
||||
})
|
||||
items.push({
|
||||
testID: 'profileHeaderDropdownReportBtn',
|
||||
label: 'Report Account',
|
||||
onPress: onPressReportAccount,
|
||||
icon: {
|
||||
ios: {
|
||||
name: 'exclamationmark.triangle',
|
||||
},
|
||||
android: 'ic_menu_report_image',
|
||||
web: 'circle-exclamation',
|
||||
},
|
||||
})
|
||||
}
|
||||
return items
|
||||
}, [
|
||||
isMe,
|
||||
view.viewer.muted,
|
||||
view.viewer.blocking,
|
||||
onPressShare,
|
||||
onPressUnmuteAccount,
|
||||
onPressMuteAccount,
|
||||
onPressUnblockAccount,
|
||||
onPressBlockAccount,
|
||||
onPressReportAccount,
|
||||
onPressAddRemoveLists,
|
||||
])
|
||||
|
||||
const blockHide = !isMe && (view.viewer.blocking || view.viewer.blockedBy)
|
||||
const following = formatCount(view.followsCount)
|
||||
const followers = formatCount(view.followersCount)
|
||||
const pluralizedFollowers = pluralize(view.followersCount, 'follower')
|
||||
export const ProfileHeader = observer(function ProfileHeaderImpl({
|
||||
view,
|
||||
onRefreshAll,
|
||||
hideBackButton = false,
|
||||
}: Props) {
|
||||
const pal = usePalette('default')
|
||||
|
||||
// loading
|
||||
// =
|
||||
if (!view || !view.hasLoaded) {
|
||||
return (
|
||||
<View style={pal.view}>
|
||||
<UserBanner banner={view.banner} moderation={view.moderation.avatar} />
|
||||
<LoadingPlaceholder width="100%" height={120} />
|
||||
<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]}>
|
||||
{isMe ? (
|
||||
<TouchableOpacity
|
||||
testID="profileHeaderEditProfileButton"
|
||||
onPress={onPressEditProfile}
|
||||
style={[styles.btn, styles.mainBtn, pal.btn]}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Edit profile"
|
||||
accessibilityHint="Opens editor for profile display name, avatar, background image, and description">
|
||||
<Text type="button" style={pal.text}>
|
||||
Edit Profile
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
) : view.viewer.blocking ? (
|
||||
<TouchableOpacity
|
||||
testID="unblockBtn"
|
||||
onPress={onPressUnblockAccount}
|
||||
style={[styles.btn, styles.mainBtn, pal.btn]}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Unblock"
|
||||
accessibilityHint="">
|
||||
<Text type="button" style={[pal.text, s.bold]}>
|
||||
Unblock
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
) : !view.viewer.blockedBy ? (
|
||||
<>
|
||||
{store.me.follows.getFollowState(view.did) ===
|
||||
FollowState.Following ? (
|
||||
<TouchableOpacity
|
||||
testID="unfollowBtn"
|
||||
onPress={onPressToggleFollow}
|
||||
style={[styles.btn, styles.mainBtn, pal.btn]}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={`Unfollow ${view.handle}`}
|
||||
accessibilityHint={`Hides posts from ${view.handle} in your feed`}>
|
||||
<FontAwesomeIcon
|
||||
icon="check"
|
||||
style={[pal.text, s.mr5]}
|
||||
size={14}
|
||||
/>
|
||||
<Text type="button" style={pal.text}>
|
||||
Following
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
) : (
|
||||
<TouchableOpacity
|
||||
testID="followBtn"
|
||||
onPress={onPressToggleFollow}
|
||||
style={[styles.btn, styles.mainBtn, palInverted.view]}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={`Follow ${view.handle}`}
|
||||
accessibilityHint={`Shows posts from ${view.handle} in your feed`}>
|
||||
<FontAwesomeIcon
|
||||
icon="plus"
|
||||
style={[palInverted.text, s.mr5]}
|
||||
/>
|
||||
<Text type="button" style={[palInverted.text, s.bold]}>
|
||||
Follow
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</>
|
||||
) : null}
|
||||
{dropdownItems?.length ? (
|
||||
<NativeDropdown
|
||||
testID="profileHeaderDropdownBtn"
|
||||
items={dropdownItems}>
|
||||
<View style={[styles.btn, styles.secondaryBtn, pal.btn]}>
|
||||
<FontAwesomeIcon
|
||||
icon="ellipsis"
|
||||
size={20}
|
||||
style={[pal.text]}
|
||||
/>
|
||||
</View>
|
||||
</NativeDropdown>
|
||||
) : undefined}
|
||||
<LoadingPlaceholder width={100} height={31} style={styles.br50} />
|
||||
</View>
|
||||
<View>
|
||||
<Text
|
||||
testID="profileHeaderDisplayName"
|
||||
type="title-2xl"
|
||||
style={[pal.text, styles.title]}>
|
||||
<Text type="title-2xl" style={[pal.text, styles.title]}>
|
||||
{sanitizeDisplayName(
|
||||
view.displayName || sanitizeHandle(view.handle),
|
||||
view.moderation.profile,
|
||||
)}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.handleLine}>
|
||||
{view.viewer.followedBy && !blockHide ? (
|
||||
<View style={[styles.pill, pal.btn, s.mr5]}>
|
||||
<Text type="xs" style={[pal.text]}>
|
||||
Follows you
|
||||
</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' : `@${view.handle}`}
|
||||
</ThemedText>
|
||||
</View>
|
||||
{!blockHide && (
|
||||
<>
|
||||
<View style={styles.metricsLine}>
|
||||
<TouchableOpacity
|
||||
testID="profileHeaderFollowersButton"
|
||||
style={[s.flexRow, s.mr10]}
|
||||
onPress={onPressFollowers}
|
||||
accessibilityRole="button"
|
||||
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>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
testID="profileHeaderFollowsButton"
|
||||
style={[s.flexRow, s.mr10]}
|
||||
onPress={onPressFollows}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={`${following} following`}
|
||||
accessibilityHint={'Opens following list'}>
|
||||
<Text type="md" style={[s.bold, pal.text]}>
|
||||
{following}{' '}
|
||||
</Text>
|
||||
<Text type="md" style={[pal.textLight]}>
|
||||
following
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<Text type="md" style={[s.bold, pal.text]}>
|
||||
{formatCount(view.postsCount)}{' '}
|
||||
<Text type="md" style={[pal.textLight]}>
|
||||
{pluralize(view.postsCount, 'post')}
|
||||
</Text>
|
||||
</Text>
|
||||
</View>
|
||||
{view.description &&
|
||||
view.descriptionRichText &&
|
||||
!view.moderation.profile.blur ? (
|
||||
<RichText
|
||||
testID="profileHeaderDescription"
|
||||
style={[styles.description, pal.text]}
|
||||
numberOfLines={15}
|
||||
richText={view.descriptionRichText}
|
||||
/>
|
||||
) : undefined}
|
||||
</>
|
||||
)}
|
||||
<ProfileHeaderAlerts moderation={view.moderation} />
|
||||
</View>
|
||||
{!isDesktop && !hideBackButton && (
|
||||
<TouchableWithoutFeedback
|
||||
onPress={onPressBack}
|
||||
hitSlop={BACK_HITSLOP}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="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 ${view.handle}'s avatar`}
|
||||
accessibilityHint="">
|
||||
<View
|
||||
style={[
|
||||
pal.view,
|
||||
{borderColor: pal.colors.background},
|
||||
styles.avi,
|
||||
]}>
|
||||
<UserAvatar
|
||||
size={80}
|
||||
avatar={view.avatar}
|
||||
moderation={view.moderation.avatar}
|
||||
/>
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
</View>
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// error
|
||||
// =
|
||||
if (view.hasError) {
|
||||
return (
|
||||
<View testID="profileHeaderHasError">
|
||||
<Text>{view.error}</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
// loaded
|
||||
// =
|
||||
return (
|
||||
<ProfileHeaderLoaded
|
||||
view={view}
|
||||
onRefreshAll={onRefreshAll}
|
||||
hideBackButton={hideBackButton}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
|
||||
view,
|
||||
onRefreshAll,
|
||||
hideBackButton = false,
|
||||
}: Props) {
|
||||
const pal = usePalette('default')
|
||||
const palInverted = usePalette('inverted')
|
||||
const store = useStores()
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
const {track} = useAnalytics()
|
||||
const invalidHandle = isInvalidHandle(view.handle)
|
||||
const {isDesktop} = useWebMediaQueries()
|
||||
|
||||
const onPressBack = React.useCallback(() => {
|
||||
navigation.goBack()
|
||||
}, [navigation])
|
||||
|
||||
const onPressAvi = React.useCallback(() => {
|
||||
if (
|
||||
view.avatar &&
|
||||
!(view.moderation.avatar.blur && view.moderation.avatar.noOverride)
|
||||
) {
|
||||
store.shell.openLightbox(new ProfileImageLightbox(view))
|
||||
}
|
||||
}, [store, view])
|
||||
|
||||
const onPressToggleFollow = React.useCallback(() => {
|
||||
track(
|
||||
view.viewer.following
|
||||
? 'ProfileHeader:FollowButtonClicked'
|
||||
: 'ProfileHeader:UnfollowButtonClicked',
|
||||
)
|
||||
view?.toggleFollowing().then(
|
||||
() => {
|
||||
Toast.show(
|
||||
`${
|
||||
view.viewer.following ? 'Following' : 'No longer following'
|
||||
} ${sanitizeDisplayName(view.displayName || view.handle)}`,
|
||||
)
|
||||
},
|
||||
err => store.log.error('Failed to toggle follow', err),
|
||||
)
|
||||
}, [track, view, store.log])
|
||||
|
||||
const onPressEditProfile = React.useCallback(() => {
|
||||
track('ProfileHeader:EditProfileButtonClicked')
|
||||
store.shell.openModal({
|
||||
name: 'edit-profile',
|
||||
profileView: view,
|
||||
onUpdate: onRefreshAll,
|
||||
})
|
||||
}, [track, store, view, onRefreshAll])
|
||||
|
||||
const onPressFollowers = React.useCallback(() => {
|
||||
track('ProfileHeader:FollowersButtonClicked')
|
||||
navigate('ProfileFollowers', {
|
||||
name: isInvalidHandle(view.handle) ? view.did : view.handle,
|
||||
})
|
||||
store.shell.closeAllActiveElements() // for when used in the profile preview modal
|
||||
}, [track, view, store.shell])
|
||||
|
||||
const onPressFollows = React.useCallback(() => {
|
||||
track('ProfileHeader:FollowsButtonClicked')
|
||||
navigate('ProfileFollows', {
|
||||
name: isInvalidHandle(view.handle) ? view.did : view.handle,
|
||||
})
|
||||
store.shell.closeAllActiveElements() // for when used in the profile preview modal
|
||||
}, [track, view, store.shell])
|
||||
|
||||
const onPressShare = React.useCallback(() => {
|
||||
track('ProfileHeader:ShareButtonClicked')
|
||||
const url = toShareUrl(makeProfileLink(view))
|
||||
shareUrl(url)
|
||||
}, [track, view])
|
||||
|
||||
const onPressAddRemoveLists = React.useCallback(() => {
|
||||
track('ProfileHeader:AddToListsButtonClicked')
|
||||
store.shell.openModal({
|
||||
name: 'list-add-remove-user',
|
||||
subject: view.did,
|
||||
displayName: view.displayName || view.handle,
|
||||
})
|
||||
}, [track, view, store])
|
||||
|
||||
const onPressMuteAccount = React.useCallback(async () => {
|
||||
track('ProfileHeader:MuteAccountButtonClicked')
|
||||
try {
|
||||
await view.muteAccount()
|
||||
Toast.show('Account muted')
|
||||
} catch (e: any) {
|
||||
store.log.error('Failed to mute account', e)
|
||||
Toast.show(`There was an issue! ${e.toString()}`)
|
||||
}
|
||||
}, [track, view, store])
|
||||
|
||||
const onPressUnmuteAccount = React.useCallback(async () => {
|
||||
track('ProfileHeader:UnmuteAccountButtonClicked')
|
||||
try {
|
||||
await view.unmuteAccount()
|
||||
Toast.show('Account unmuted')
|
||||
} catch (e: any) {
|
||||
store.log.error('Failed to unmute account', e)
|
||||
Toast.show(`There was an issue! ${e.toString()}`)
|
||||
}
|
||||
}, [track, view, store])
|
||||
|
||||
const onPressBlockAccount = React.useCallback(async () => {
|
||||
track('ProfileHeader:BlockAccountButtonClicked')
|
||||
store.shell.openModal({
|
||||
name: 'confirm',
|
||||
title: 'Block Account',
|
||||
message:
|
||||
'Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you.',
|
||||
onPressConfirm: async () => {
|
||||
try {
|
||||
await view.blockAccount()
|
||||
onRefreshAll()
|
||||
Toast.show('Account blocked')
|
||||
} catch (e: any) {
|
||||
store.log.error('Failed to block account', e)
|
||||
Toast.show(`There was an issue! ${e.toString()}`)
|
||||
}
|
||||
},
|
||||
})
|
||||
}, [track, view, store, onRefreshAll])
|
||||
|
||||
const onPressUnblockAccount = React.useCallback(async () => {
|
||||
track('ProfileHeader:UnblockAccountButtonClicked')
|
||||
store.shell.openModal({
|
||||
name: 'confirm',
|
||||
title: 'Unblock Account',
|
||||
message:
|
||||
'The account will be able to interact with you after unblocking.',
|
||||
onPressConfirm: async () => {
|
||||
try {
|
||||
await view.unblockAccount()
|
||||
onRefreshAll()
|
||||
Toast.show('Account unblocked')
|
||||
} catch (e: any) {
|
||||
store.log.error('Failed to unblock account', e)
|
||||
Toast.show(`There was an issue! ${e.toString()}`)
|
||||
}
|
||||
},
|
||||
})
|
||||
}, [track, view, store, onRefreshAll])
|
||||
|
||||
const onPressReportAccount = React.useCallback(() => {
|
||||
track('ProfileHeader:ReportAccountButtonClicked')
|
||||
store.shell.openModal({
|
||||
name: 'report',
|
||||
did: view.did,
|
||||
})
|
||||
}, [track, store, view])
|
||||
|
||||
const isMe = React.useMemo(
|
||||
() => store.me.did === view.did,
|
||||
[store.me.did, view.did],
|
||||
)
|
||||
const dropdownItems: DropdownItem[] = React.useMemo(() => {
|
||||
let items: DropdownItem[] = [
|
||||
{
|
||||
testID: 'profileHeaderDropdownShareBtn',
|
||||
label: 'Share',
|
||||
onPress: onPressShare,
|
||||
icon: {
|
||||
ios: {
|
||||
name: 'square.and.arrow.up',
|
||||
},
|
||||
android: 'ic_menu_share',
|
||||
web: 'share',
|
||||
},
|
||||
},
|
||||
]
|
||||
if (!isMe) {
|
||||
items.push({label: 'separator'})
|
||||
// Only add "Add to Lists" on other user's profiles, doesn't make sense to mute my own self!
|
||||
items.push({
|
||||
testID: 'profileHeaderDropdownListAddRemoveBtn',
|
||||
label: 'Add to Lists',
|
||||
onPress: onPressAddRemoveLists,
|
||||
icon: {
|
||||
ios: {
|
||||
name: 'list.bullet',
|
||||
},
|
||||
android: 'ic_menu_add',
|
||||
web: 'list',
|
||||
},
|
||||
})
|
||||
if (!view.viewer.blocking) {
|
||||
items.push({
|
||||
testID: 'profileHeaderDropdownMuteBtn',
|
||||
label: view.viewer.muted ? 'Unmute Account' : 'Mute Account',
|
||||
onPress: view.viewer.muted
|
||||
? onPressUnmuteAccount
|
||||
: onPressMuteAccount,
|
||||
icon: {
|
||||
ios: {
|
||||
name: 'speaker.slash',
|
||||
},
|
||||
android: 'ic_lock_silent_mode',
|
||||
web: 'comment-slash',
|
||||
},
|
||||
})
|
||||
}
|
||||
items.push({
|
||||
testID: 'profileHeaderDropdownBlockBtn',
|
||||
label: view.viewer.blocking ? 'Unblock Account' : 'Block Account',
|
||||
onPress: view.viewer.blocking
|
||||
? onPressUnblockAccount
|
||||
: onPressBlockAccount,
|
||||
icon: {
|
||||
ios: {
|
||||
name: 'person.fill.xmark',
|
||||
},
|
||||
android: 'ic_menu_close_clear_cancel',
|
||||
web: 'user-slash',
|
||||
},
|
||||
})
|
||||
items.push({
|
||||
testID: 'profileHeaderDropdownReportBtn',
|
||||
label: 'Report Account',
|
||||
onPress: onPressReportAccount,
|
||||
icon: {
|
||||
ios: {
|
||||
name: 'exclamationmark.triangle',
|
||||
},
|
||||
android: 'ic_menu_report_image',
|
||||
web: 'circle-exclamation',
|
||||
},
|
||||
})
|
||||
}
|
||||
return items
|
||||
}, [
|
||||
isMe,
|
||||
view.viewer.muted,
|
||||
view.viewer.blocking,
|
||||
onPressShare,
|
||||
onPressUnmuteAccount,
|
||||
onPressMuteAccount,
|
||||
onPressUnblockAccount,
|
||||
onPressBlockAccount,
|
||||
onPressReportAccount,
|
||||
onPressAddRemoveLists,
|
||||
])
|
||||
|
||||
const blockHide = !isMe && (view.viewer.blocking || view.viewer.blockedBy)
|
||||
const following = formatCount(view.followsCount)
|
||||
const followers = formatCount(view.followersCount)
|
||||
const pluralizedFollowers = pluralize(view.followersCount, 'follower')
|
||||
|
||||
return (
|
||||
<View style={pal.view}>
|
||||
<UserBanner banner={view.banner} moderation={view.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="Edit profile"
|
||||
accessibilityHint="Opens editor for profile display name, avatar, background image, and description">
|
||||
<Text type="button" style={pal.text}>
|
||||
Edit Profile
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
) : view.viewer.blocking ? (
|
||||
<TouchableOpacity
|
||||
testID="unblockBtn"
|
||||
onPress={onPressUnblockAccount}
|
||||
style={[styles.btn, styles.mainBtn, pal.btn]}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Unblock"
|
||||
accessibilityHint="">
|
||||
<Text type="button" style={[pal.text, s.bold]}>
|
||||
Unblock
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
) : !view.viewer.blockedBy ? (
|
||||
<>
|
||||
{store.me.follows.getFollowState(view.did) ===
|
||||
FollowState.Following ? (
|
||||
<TouchableOpacity
|
||||
testID="unfollowBtn"
|
||||
onPress={onPressToggleFollow}
|
||||
style={[styles.btn, styles.mainBtn, pal.btn]}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={`Unfollow ${view.handle}`}
|
||||
accessibilityHint={`Hides posts from ${view.handle} in your feed`}>
|
||||
<FontAwesomeIcon
|
||||
icon="check"
|
||||
style={[pal.text, s.mr5]}
|
||||
size={14}
|
||||
/>
|
||||
<Text type="button" style={pal.text}>
|
||||
Following
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
) : (
|
||||
<TouchableOpacity
|
||||
testID="followBtn"
|
||||
onPress={onPressToggleFollow}
|
||||
style={[styles.btn, styles.mainBtn, palInverted.view]}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={`Follow ${view.handle}`}
|
||||
accessibilityHint={`Shows posts from ${view.handle} in your feed`}>
|
||||
<FontAwesomeIcon
|
||||
icon="plus"
|
||||
style={[palInverted.text, s.mr5]}
|
||||
/>
|
||||
<Text type="button" style={[palInverted.text, s.bold]}>
|
||||
Follow
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</>
|
||||
) : null}
|
||||
{dropdownItems?.length ? (
|
||||
<NativeDropdown
|
||||
testID="profileHeaderDropdownBtn"
|
||||
items={dropdownItems}>
|
||||
<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(
|
||||
view.displayName || sanitizeHandle(view.handle),
|
||||
view.moderation.profile,
|
||||
)}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.handleLine}>
|
||||
{view.viewer.followedBy && !blockHide ? (
|
||||
<View style={[styles.pill, pal.btn, s.mr5]}>
|
||||
<Text type="xs" style={[pal.text]}>
|
||||
Follows you
|
||||
</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' : `@${view.handle}`}
|
||||
</ThemedText>
|
||||
</View>
|
||||
{!blockHide && (
|
||||
<>
|
||||
<View style={styles.metricsLine}>
|
||||
<TouchableOpacity
|
||||
testID="profileHeaderFollowersButton"
|
||||
style={[s.flexRow, s.mr10]}
|
||||
onPress={onPressFollowers}
|
||||
accessibilityRole="button"
|
||||
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>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
testID="profileHeaderFollowsButton"
|
||||
style={[s.flexRow, s.mr10]}
|
||||
onPress={onPressFollows}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={`${following} following`}
|
||||
accessibilityHint={'Opens following list'}>
|
||||
<Text type="md" style={[s.bold, pal.text]}>
|
||||
{following}{' '}
|
||||
</Text>
|
||||
<Text type="md" style={[pal.textLight]}>
|
||||
following
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<Text type="md" style={[s.bold, pal.text]}>
|
||||
{formatCount(view.postsCount)}{' '}
|
||||
<Text type="md" style={[pal.textLight]}>
|
||||
{pluralize(view.postsCount, 'post')}
|
||||
</Text>
|
||||
</Text>
|
||||
</View>
|
||||
{view.description &&
|
||||
view.descriptionRichText &&
|
||||
!view.moderation.profile.blur ? (
|
||||
<RichText
|
||||
testID="profileHeaderDescription"
|
||||
style={[styles.description, pal.text]}
|
||||
numberOfLines={15}
|
||||
richText={view.descriptionRichText}
|
||||
/>
|
||||
) : undefined}
|
||||
</>
|
||||
)}
|
||||
<ProfileHeaderAlerts moderation={view.moderation} />
|
||||
</View>
|
||||
{!isDesktop && !hideBackButton && (
|
||||
<TouchableWithoutFeedback
|
||||
onPress={onPressBack}
|
||||
hitSlop={BACK_HITSLOP}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="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 ${view.handle}'s avatar`}
|
||||
accessibilityHint="">
|
||||
<View
|
||||
style={[pal.view, {borderColor: pal.colors.background}, styles.avi]}>
|
||||
<UserAvatar
|
||||
size={80}
|
||||
avatar={view.avatar}
|
||||
moderation={view.moderation.avatar}
|
||||
/>
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
</View>
|
||||
)
|
||||
})
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
banner: {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue