Add minimal header to profiles (#8936)
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
import {memo, useCallback, useMemo, useState} from 'react'
|
||||
import {memo, useMemo, useState} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {
|
||||
type AppBskyActorDefs,
|
||||
moderateProfile,
|
||||
type ModerationDecision,
|
||||
type ModerationOpts,
|
||||
type RichText as RichTextAPI,
|
||||
} from '@atproto/api'
|
||||
@@ -15,14 +16,13 @@ import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||
import {logger} from '#/logger'
|
||||
import {isIOS} from '#/platform/detection'
|
||||
import {useProfileShadow} from '#/state/cache/profile-shadow'
|
||||
import {type Shadow, useProfileShadow} from '#/state/cache/profile-shadow'
|
||||
import {
|
||||
useProfileBlockMutationQueue,
|
||||
useProfileFollowMutationQueue,
|
||||
} from '#/state/queries/profile'
|
||||
import {useRequireAuth, useSession} from '#/state/session'
|
||||
import {ProfileMenu} from '#/view/com/profile/ProfileMenu'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {atoms as a, platform, useBreakpoints, useTheme} from '#/alf'
|
||||
import {SubscribeProfileButton} from '#/components/activity-notifications/SubscribeProfileButton'
|
||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
} from '#/components/KnownFollowers'
|
||||
import * as Prompt from '#/components/Prompt'
|
||||
import {RichText} from '#/components/RichText'
|
||||
import * as Toast from '#/components/Toast'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {VerificationCheckButton} from '#/components/verification/VerificationCheckButton'
|
||||
import {EditProfileDialog} from './EditProfileDialog'
|
||||
@@ -63,107 +64,36 @@ let ProfileHeaderStandard = ({
|
||||
const {gtMobile} = useBreakpoints()
|
||||
const profile =
|
||||
useProfileShadow<AppBskyActorDefs.ProfileViewDetailed>(profileUnshadowed)
|
||||
const {currentAccount, hasSession} = useSession()
|
||||
const {currentAccount} = useSession()
|
||||
const {_} = useLingui()
|
||||
const moderation = useMemo(
|
||||
() => moderateProfile(profile, moderationOpts),
|
||||
[profile, moderationOpts],
|
||||
)
|
||||
const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(
|
||||
profile,
|
||||
'ProfileHeader',
|
||||
)
|
||||
const [_queueBlock, queueUnblock] = useProfileBlockMutationQueue(profile)
|
||||
const [, queueUnblock] = useProfileBlockMutationQueue(profile)
|
||||
const unblockPromptControl = Prompt.usePromptControl()
|
||||
const requireAuth = useRequireAuth()
|
||||
const [showSuggestedFollows, setShowSuggestedFollows] = useState(false)
|
||||
const isBlockedUser =
|
||||
profile.viewer?.blocking ||
|
||||
profile.viewer?.blockedBy ||
|
||||
profile.viewer?.blockingByList
|
||||
const playHaptic = useHaptics()
|
||||
|
||||
const editProfileControl = useDialogControl()
|
||||
|
||||
const onPressFollow = () => {
|
||||
playHaptic()
|
||||
requireAuth(async () => {
|
||||
setShowSuggestedFollows(true)
|
||||
try {
|
||||
await queueFollow()
|
||||
Toast.show(
|
||||
_(
|
||||
msg`Following ${sanitizeDisplayName(
|
||||
profile.displayName || profile.handle,
|
||||
moderation.ui('displayName'),
|
||||
)}`,
|
||||
),
|
||||
)
|
||||
} catch (e: any) {
|
||||
if (e?.name !== 'AbortError') {
|
||||
logger.error('Failed to follow', {message: String(e)})
|
||||
Toast.show(_(msg`There was an issue! ${e.toString()}`), 'xmark')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const onPressUnfollow = () => {
|
||||
playHaptic()
|
||||
setShowSuggestedFollows(false)
|
||||
requireAuth(async () => {
|
||||
try {
|
||||
await queueUnfollow()
|
||||
Toast.show(
|
||||
_(
|
||||
msg`No longer following ${sanitizeDisplayName(
|
||||
profile.displayName || profile.handle,
|
||||
moderation.ui('displayName'),
|
||||
)}`,
|
||||
),
|
||||
)
|
||||
} catch (e: any) {
|
||||
if (e?.name !== 'AbortError') {
|
||||
logger.error('Failed to unfollow', {message: String(e)})
|
||||
Toast.show(_(msg`There was an issue! ${e.toString()}`), 'xmark')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const unblockAccount = useCallback(async () => {
|
||||
playHaptic()
|
||||
const unblockAccount = async () => {
|
||||
try {
|
||||
await queueUnblock()
|
||||
Toast.show(_(msg({message: 'Account unblocked', context: 'toast'})))
|
||||
} catch (e: any) {
|
||||
if (e?.name !== 'AbortError') {
|
||||
logger.error('Failed to unblock account', {message: e})
|
||||
Toast.show(_(msg`There was an issue! ${e.toString()}`), 'xmark')
|
||||
Toast.show(_(msg`There was an issue! ${e.toString()}`), {type: 'error'})
|
||||
}
|
||||
}
|
||||
}, [_, queueUnblock, playHaptic])
|
||||
}
|
||||
|
||||
const isMe = useMemo(
|
||||
() => currentAccount?.did === profile.did,
|
||||
[currentAccount, profile],
|
||||
)
|
||||
const isMe = currentAccount?.did === profile.did
|
||||
|
||||
const {isActive: live} = useActorStatus(profile)
|
||||
|
||||
const subscriptionsAllowed = useMemo(() => {
|
||||
switch (profile.associated?.activitySubscription?.allowSubscriptions) {
|
||||
case 'followers':
|
||||
case undefined:
|
||||
return !!profile.viewer?.following
|
||||
case 'mutuals':
|
||||
return !!profile.viewer?.following && !!profile.viewer.followedBy
|
||||
case 'none':
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}, [profile])
|
||||
|
||||
return (
|
||||
<>
|
||||
<ProfileHeaderShell
|
||||
@@ -185,83 +115,13 @@ let ProfileHeaderStandard = ({
|
||||
a.flex_wrap,
|
||||
]}
|
||||
pointerEvents={isIOS ? 'auto' : 'box-none'}>
|
||||
{isMe ? (
|
||||
<>
|
||||
<Button
|
||||
testID="profileHeaderEditProfileButton"
|
||||
size="small"
|
||||
color="secondary"
|
||||
variant="solid"
|
||||
onPress={editProfileControl.open}
|
||||
label={_(msg`Edit profile`)}
|
||||
style={[a.rounded_full]}>
|
||||
<ButtonText>
|
||||
<Trans>Edit Profile</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
<EditProfileDialog
|
||||
profile={profile}
|
||||
control={editProfileControl}
|
||||
/>
|
||||
</>
|
||||
) : profile.viewer?.blocking ? (
|
||||
profile.viewer?.blockingByList ? null : (
|
||||
<Button
|
||||
testID="unblockBtn"
|
||||
size="small"
|
||||
color="secondary"
|
||||
variant="solid"
|
||||
label={_(msg`Unblock`)}
|
||||
disabled={!hasSession}
|
||||
onPress={() => unblockPromptControl.open()}
|
||||
style={[a.rounded_full]}>
|
||||
<ButtonText>
|
||||
<Trans context="action">Unblock</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
)
|
||||
) : !profile.viewer?.blockedBy ? (
|
||||
<>
|
||||
{hasSession && subscriptionsAllowed && (
|
||||
<SubscribeProfileButton
|
||||
profile={profile}
|
||||
moderationOpts={moderationOpts}
|
||||
/>
|
||||
)}
|
||||
{hasSession && <MessageProfileButton profile={profile} />}
|
||||
|
||||
<Button
|
||||
testID={
|
||||
profile.viewer?.following ? 'unfollowBtn' : 'followBtn'
|
||||
}
|
||||
size="small"
|
||||
color={profile.viewer?.following ? 'secondary' : 'primary'}
|
||||
variant="solid"
|
||||
label={
|
||||
profile.viewer?.following
|
||||
? _(msg`Unfollow ${profile.handle}`)
|
||||
: _(msg`Follow ${profile.handle}`)
|
||||
}
|
||||
onPress={
|
||||
profile.viewer?.following ? onPressUnfollow : onPressFollow
|
||||
}
|
||||
style={[a.rounded_full]}>
|
||||
{!profile.viewer?.following && (
|
||||
<ButtonIcon position="left" icon={Plus} />
|
||||
)}
|
||||
<ButtonText>
|
||||
{profile.viewer?.following ? (
|
||||
<Trans>Following</Trans>
|
||||
) : profile.viewer?.followedBy ? (
|
||||
<Trans>Follow back</Trans>
|
||||
) : (
|
||||
<Trans>Follow</Trans>
|
||||
)}
|
||||
</ButtonText>
|
||||
</Button>
|
||||
</>
|
||||
) : null}
|
||||
<ProfileMenu profile={profile} />
|
||||
<HeaderStandardButtons
|
||||
profile={profile}
|
||||
moderation={moderation}
|
||||
moderationOpts={moderationOpts}
|
||||
onFollow={() => setShowSuggestedFollows(true)}
|
||||
onUnfollow={() => setShowSuggestedFollows(false)}
|
||||
/>
|
||||
</View>
|
||||
<View
|
||||
style={[a.flex_col, a.gap_xs, a.pb_sm, live ? a.pt_sm : a.pt_2xs]}>
|
||||
@@ -280,13 +140,7 @@ let ProfileHeaderStandard = ({
|
||||
profile.displayName || sanitizeHandle(profile.handle),
|
||||
moderation.ui('displayName'),
|
||||
)}
|
||||
<View
|
||||
style={[
|
||||
a.pl_xs,
|
||||
{
|
||||
marginTop: platform({ios: 2}),
|
||||
},
|
||||
]}>
|
||||
<View style={[a.pl_xs, {marginTop: platform({ios: 2})}]}>
|
||||
<VerificationCheckButton profile={profile} size="lg" />
|
||||
</View>
|
||||
</Text>
|
||||
@@ -349,3 +203,197 @@ let ProfileHeaderStandard = ({
|
||||
|
||||
ProfileHeaderStandard = memo(ProfileHeaderStandard)
|
||||
export {ProfileHeaderStandard}
|
||||
|
||||
export function HeaderStandardButtons({
|
||||
profile,
|
||||
moderation,
|
||||
moderationOpts,
|
||||
onFollow,
|
||||
onUnfollow,
|
||||
minimal,
|
||||
}: {
|
||||
profile: Shadow<AppBskyActorDefs.ProfileViewDetailed>
|
||||
moderation: ModerationDecision
|
||||
moderationOpts: ModerationOpts
|
||||
onFollow?: () => void
|
||||
onUnfollow?: () => void
|
||||
minimal?: boolean
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
const {hasSession, currentAccount} = useSession()
|
||||
const playHaptic = useHaptics()
|
||||
const requireAuth = useRequireAuth()
|
||||
const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(
|
||||
profile,
|
||||
'ProfileHeader',
|
||||
)
|
||||
const [, queueUnblock] = useProfileBlockMutationQueue(profile)
|
||||
const editProfileControl = useDialogControl()
|
||||
const unblockPromptControl = Prompt.usePromptControl()
|
||||
|
||||
const isMe = currentAccount?.did === profile.did
|
||||
|
||||
const onPressFollow = () => {
|
||||
playHaptic()
|
||||
requireAuth(async () => {
|
||||
try {
|
||||
await queueFollow()
|
||||
onFollow?.()
|
||||
Toast.show(
|
||||
_(
|
||||
msg`Following ${sanitizeDisplayName(
|
||||
profile.displayName || profile.handle,
|
||||
moderation.ui('displayName'),
|
||||
)}`,
|
||||
),
|
||||
)
|
||||
} catch (e: any) {
|
||||
if (e?.name !== 'AbortError') {
|
||||
logger.error('Failed to follow', {message: String(e)})
|
||||
Toast.show(_(msg`There was an issue! ${e.toString()}`), {
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const onPressUnfollow = () => {
|
||||
playHaptic()
|
||||
requireAuth(async () => {
|
||||
try {
|
||||
await queueUnfollow()
|
||||
onUnfollow?.()
|
||||
Toast.show(
|
||||
_(
|
||||
msg`No longer following ${sanitizeDisplayName(
|
||||
profile.displayName || profile.handle,
|
||||
moderation.ui('displayName'),
|
||||
)}`,
|
||||
),
|
||||
{type: 'default'},
|
||||
)
|
||||
} catch (e: any) {
|
||||
if (e?.name !== 'AbortError') {
|
||||
logger.error('Failed to unfollow', {message: String(e)})
|
||||
Toast.show(_(msg`There was an issue! ${e.toString()}`), {
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const unblockAccount = async () => {
|
||||
try {
|
||||
await queueUnblock()
|
||||
Toast.show(_(msg({message: 'Account unblocked', context: 'toast'})))
|
||||
} catch (e: any) {
|
||||
if (e?.name !== 'AbortError') {
|
||||
logger.error('Failed to unblock account', {message: e})
|
||||
Toast.show(_(msg`There was an issue! ${e.toString()}`), {type: 'error'})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const subscriptionsAllowed = useMemo(() => {
|
||||
switch (profile.associated?.activitySubscription?.allowSubscriptions) {
|
||||
case 'followers':
|
||||
case undefined:
|
||||
return !!profile.viewer?.following
|
||||
case 'mutuals':
|
||||
return !!profile.viewer?.following && !!profile.viewer.followedBy
|
||||
case 'none':
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}, [profile])
|
||||
|
||||
return (
|
||||
<>
|
||||
{isMe ? (
|
||||
<>
|
||||
<Button
|
||||
testID="profileHeaderEditProfileButton"
|
||||
size="small"
|
||||
color="secondary"
|
||||
onPress={editProfileControl.open}
|
||||
label={_(msg`Edit profile`)}>
|
||||
<ButtonText>
|
||||
<Trans>Edit Profile</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
<EditProfileDialog profile={profile} control={editProfileControl} />
|
||||
</>
|
||||
) : profile.viewer?.blocking ? (
|
||||
profile.viewer?.blockingByList ? null : (
|
||||
<Button
|
||||
testID="unblockBtn"
|
||||
size="small"
|
||||
color="secondary"
|
||||
label={_(msg`Unblock`)}
|
||||
disabled={!hasSession}
|
||||
onPress={() => unblockPromptControl.open()}>
|
||||
<ButtonText>
|
||||
<Trans context="action">Unblock</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
)
|
||||
) : !profile.viewer?.blockedBy ? (
|
||||
<>
|
||||
{hasSession && (!minimal || profile.viewer?.following) && (
|
||||
<>
|
||||
{subscriptionsAllowed && (
|
||||
<SubscribeProfileButton
|
||||
profile={profile}
|
||||
moderationOpts={moderationOpts}
|
||||
disableHint={minimal}
|
||||
/>
|
||||
)}
|
||||
|
||||
<MessageProfileButton profile={profile} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{(!minimal || !profile.viewer?.following) && (
|
||||
<Button
|
||||
testID={profile.viewer?.following ? 'unfollowBtn' : 'followBtn'}
|
||||
size="small"
|
||||
color={profile.viewer?.following ? 'secondary' : 'primary'}
|
||||
label={
|
||||
profile.viewer?.following
|
||||
? _(msg`Unfollow ${profile.handle}`)
|
||||
: _(msg`Follow ${profile.handle}`)
|
||||
}
|
||||
onPress={
|
||||
profile.viewer?.following ? onPressUnfollow : onPressFollow
|
||||
}>
|
||||
{!profile.viewer?.following && <ButtonIcon icon={Plus} />}
|
||||
<ButtonText>
|
||||
{profile.viewer?.following ? (
|
||||
<Trans>Following</Trans>
|
||||
) : profile.viewer?.followedBy ? (
|
||||
<Trans>Follow back</Trans>
|
||||
) : (
|
||||
<Trans>Follow</Trans>
|
||||
)}
|
||||
</ButtonText>
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
) : null}
|
||||
<ProfileMenu profile={profile} />
|
||||
|
||||
<Prompt.Basic
|
||||
control={unblockPromptControl}
|
||||
title={_(msg`Unblock Account?`)}
|
||||
description={_(
|
||||
msg`The account will be able to interact with you after unblocking.`,
|
||||
)}
|
||||
onConfirm={unblockAccount}
|
||||
confirmButtonCta={_(msg`Unblock`)}
|
||||
confirmButtonColor="negative"
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user